@agent-native/core 0.12.16 → 0.12.17
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/dist/agent/production-agent.d.ts +1 -1
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +1 -1
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/cli/create.js +1 -1
- package/dist/cli/create.js.map +1 -1
- package/dist/cli/index.js +13 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/workspace-dev.d.ts +40 -1
- package/dist/cli/workspace-dev.d.ts.map +1 -1
- package/dist/cli/workspace-dev.js +506 -384
- package/dist/cli/workspace-dev.js.map +1 -1
- package/dist/client/AgentPanel.d.ts +16 -0
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +30 -9
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/AssistantChat.d.ts +4 -0
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +16 -8
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/client/MultiTabAssistantChat.js +10 -1
- package/dist/client/MultiTabAssistantChat.js.map +1 -1
- package/dist/client/NewWorkspaceAppFlow.d.ts.map +1 -1
- package/dist/client/NewWorkspaceAppFlow.js +2 -1
- package/dist/client/NewWorkspaceAppFlow.js.map +1 -1
- package/dist/client/agent-chat.d.ts +1 -1
- package/dist/client/agent-chat.js.map +1 -1
- package/dist/client/components/CodeRequiredDialog.d.ts +3 -2
- package/dist/client/components/CodeRequiredDialog.d.ts.map +1 -1
- package/dist/client/components/CodeRequiredDialog.js +4 -3
- package/dist/client/components/CodeRequiredDialog.js.map +1 -1
- package/dist/client/composer/PromptComposer.d.ts +2 -0
- package/dist/client/composer/PromptComposer.d.ts.map +1 -1
- package/dist/client/composer/PromptComposer.js +2 -2
- package/dist/client/composer/PromptComposer.js.map +1 -1
- package/dist/client/composer/TiptapComposer.d.ts +6 -1
- package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
- package/dist/client/composer/TiptapComposer.js +21 -12
- package/dist/client/composer/TiptapComposer.js.map +1 -1
- package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
- package/dist/client/settings/SettingsPanel.js +3 -0
- package/dist/client/settings/SettingsPanel.js.map +1 -1
- package/dist/client/use-send-to-agent-chat.d.ts +3 -3
- package/dist/client/use-send-to-agent-chat.js +3 -3
- package/dist/client/use-send-to-agent-chat.js.map +1 -1
- package/dist/deploy/workspace-deploy.js +4 -1
- package/dist/deploy/workspace-deploy.js.map +1 -1
- package/dist/templates/workspace-root/AGENTS.md +3 -1
- package/dist/templates/workspace-root/README.md +4 -4
- package/dist/vite/client.d.ts.map +1 -1
- package/dist/vite/client.js +34 -6
- package/dist/vite/client.js.map +1 -1
- package/docs/content/multi-app-workspace.md +1 -1
- package/package.json +1 -1
- package/src/templates/workspace-root/AGENTS.md +3 -1
- package/src/templates/workspace-root/README.md +4 -4
- package/dist/templates/workspace-root/netlify.toml +0 -11
- package/src/templates/workspace-root/netlify.toml +0 -11
|
@@ -4,16 +4,26 @@ import fs from "node:fs";
|
|
|
4
4
|
import http from "node:http";
|
|
5
5
|
import net from "node:net";
|
|
6
6
|
import path from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
7
8
|
import * as Sentry from "@sentry/node";
|
|
8
9
|
import { extractOAuthStateAppId } from "../shared/oauth-state.js";
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
const DEFAULT_GATEWAY_HOST = "127.0.0.1";
|
|
11
|
+
const DEFAULT_GATEWAY_PORT = 8080;
|
|
12
|
+
const DEFAULT_APP_PORT_START = 8100;
|
|
13
|
+
const PROXY_READY_RETRY_DELAY_MS = 250;
|
|
14
|
+
const APP_RESTART_MAX_DELAY_MS = 10_000;
|
|
15
|
+
export function shouldEagerStartWorkspaceApps(args = [], env = process.env) {
|
|
16
|
+
return (args.includes("--eager") ||
|
|
17
|
+
env.WORKSPACE_EAGER === "1" ||
|
|
18
|
+
env.WORKSPACE_EAGER === "true");
|
|
19
|
+
}
|
|
20
|
+
export function initialWorkspaceAppIds(apps, defaultApp, eager, startDefault = true) {
|
|
21
|
+
if (eager)
|
|
22
|
+
return apps.map((app) => app.id);
|
|
23
|
+
if (!startDefault)
|
|
24
|
+
return [];
|
|
25
|
+
return apps.some((app) => app.id === defaultApp) ? [defaultApp] : [];
|
|
26
|
+
}
|
|
17
27
|
function readJson(file) {
|
|
18
28
|
try {
|
|
19
29
|
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
@@ -22,13 +32,11 @@ function readJson(file) {
|
|
|
22
32
|
return null;
|
|
23
33
|
}
|
|
24
34
|
}
|
|
25
|
-
function discoverApps() {
|
|
35
|
+
function discoverApps(appsDir, appPortStart) {
|
|
26
36
|
if (!fs.existsSync(appsDir))
|
|
27
37
|
return [];
|
|
28
|
-
// existsSync
|
|
29
|
-
//
|
|
30
|
-
// Treat ENOENT as "no apps right now" and let the next 2s sync recover.
|
|
31
|
-
// Other errors get surfaced to Sentry so we learn about new failure modes.
|
|
38
|
+
// existsSync -> readdirSync is a TOCTOU race. Treat ENOENT as "no apps
|
|
39
|
+
// right now" and let the polling sync recover.
|
|
32
40
|
let entries;
|
|
33
41
|
try {
|
|
34
42
|
entries = fs.readdirSync(appsDir, { withFileTypes: true });
|
|
@@ -60,29 +68,18 @@ function discoverApps() {
|
|
|
60
68
|
};
|
|
61
69
|
})
|
|
62
70
|
.filter((app) => !!app)
|
|
63
|
-
.sort(
|
|
64
|
-
if (a.id === "dispatch")
|
|
65
|
-
return -1;
|
|
66
|
-
if (b.id === "dispatch")
|
|
67
|
-
return 1;
|
|
68
|
-
return a.id.localeCompare(b.id);
|
|
69
|
-
})
|
|
71
|
+
.sort(compareApps)
|
|
70
72
|
.map((app, index) => ({ ...app, port: appPortStart + index }));
|
|
71
73
|
}
|
|
72
|
-
|
|
73
|
-
if (
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
function compareApps(a, b) {
|
|
75
|
+
if (a.id === "dispatch")
|
|
76
|
+
return -1;
|
|
77
|
+
if (b.id === "dispatch")
|
|
78
|
+
return 1;
|
|
79
|
+
return a.id.localeCompare(b.id);
|
|
76
80
|
}
|
|
77
|
-
const appById = new Map(apps.map((app) => [app.id, app]));
|
|
78
|
-
const defaultApp = process.env.WORKSPACE_DEFAULT_APP &&
|
|
79
|
-
appById.has(process.env.WORKSPACE_DEFAULT_APP)
|
|
80
|
-
? process.env.WORKSPACE_DEFAULT_APP
|
|
81
|
-
: appById.has("dispatch")
|
|
82
|
-
? "dispatch"
|
|
83
|
-
: apps[0].id;
|
|
84
81
|
function isChildDevServerUrlLine(line) {
|
|
85
|
-
return /^\s
|
|
82
|
+
return /^\s*->\s+(?:Local|Network):\s+https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\]):\d+(?:\/\S*)?\s*$/i.test(line.replace(/\u279c/g, "->"));
|
|
86
83
|
}
|
|
87
84
|
function pipeAppOutput(prefix, chunk, write) {
|
|
88
85
|
const lines = String(chunk)
|
|
@@ -93,39 +90,6 @@ function pipeAppOutput(prefix, chunk, write) {
|
|
|
93
90
|
return;
|
|
94
91
|
write(lines.map((line) => `${prefix} ${line}`).join("\n") + "\n");
|
|
95
92
|
}
|
|
96
|
-
function syncApps() {
|
|
97
|
-
const discovered = discoverApps();
|
|
98
|
-
for (const app of discovered) {
|
|
99
|
-
const existing = appById.get(app.id);
|
|
100
|
-
if (existing) {
|
|
101
|
-
existing.name = app.name;
|
|
102
|
-
existing.dir = app.dir;
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
const usedPorts = new Set(apps.map((existing) => existing.port));
|
|
106
|
-
let port = appPortStart;
|
|
107
|
-
while (usedPorts.has(port))
|
|
108
|
-
port++;
|
|
109
|
-
const next = { ...app, port };
|
|
110
|
-
apps.push(next);
|
|
111
|
-
apps.sort((a, b) => {
|
|
112
|
-
if (a.id === "dispatch")
|
|
113
|
-
return -1;
|
|
114
|
-
if (b.id === "dispatch")
|
|
115
|
-
return 1;
|
|
116
|
-
return a.id.localeCompare(b.id);
|
|
117
|
-
});
|
|
118
|
-
appById.set(next.id, next);
|
|
119
|
-
console.log(`[workspace] Detected new app: /${next.id}`);
|
|
120
|
-
startApp(next);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
let syncTimer;
|
|
124
|
-
function scheduleSync() {
|
|
125
|
-
if (syncTimer)
|
|
126
|
-
clearTimeout(syncTimer);
|
|
127
|
-
syncTimer = setTimeout(syncApps, 400);
|
|
128
|
-
}
|
|
129
93
|
function firstPathSegment(url) {
|
|
130
94
|
if (!url)
|
|
131
95
|
return null;
|
|
@@ -138,128 +102,6 @@ function firstPathSegment(url) {
|
|
|
138
102
|
return null;
|
|
139
103
|
}
|
|
140
104
|
}
|
|
141
|
-
function appForRequest(req) {
|
|
142
|
-
const params = new URL(req.url || "/", "http://workspace.local").searchParams;
|
|
143
|
-
const explicit = params.get("_app");
|
|
144
|
-
if (explicit && appById.has(explicit))
|
|
145
|
-
return appById.get(explicit) ?? null;
|
|
146
|
-
const direct = firstPathSegment(req.url);
|
|
147
|
-
if (direct && appById.has(direct))
|
|
148
|
-
return appById.get(direct) ?? null;
|
|
149
|
-
const fromState = extractOAuthStateAppId(params.get("state"));
|
|
150
|
-
if (fromState && appById.has(fromState)) {
|
|
151
|
-
return appById.get(fromState) ?? null;
|
|
152
|
-
}
|
|
153
|
-
const referer = req.headers.referer;
|
|
154
|
-
const fromReferer = typeof referer === "string" ? firstPathSegment(referer) : null;
|
|
155
|
-
return fromReferer && appById.has(fromReferer)
|
|
156
|
-
? (appById.get(fromReferer) ?? null)
|
|
157
|
-
: null;
|
|
158
|
-
}
|
|
159
|
-
function startApp(app) {
|
|
160
|
-
if (app.process && !app.process.killed)
|
|
161
|
-
return;
|
|
162
|
-
if (app.restartTimer) {
|
|
163
|
-
clearTimeout(app.restartTimer);
|
|
164
|
-
app.restartTimer = undefined;
|
|
165
|
-
}
|
|
166
|
-
const basePath = `/${app.id}`;
|
|
167
|
-
const workspaceAppsJson = JSON.stringify(apps.map((workspaceApp) => ({
|
|
168
|
-
id: workspaceApp.id,
|
|
169
|
-
name: workspaceApp.name,
|
|
170
|
-
path: `/${workspaceApp.id}`,
|
|
171
|
-
})));
|
|
172
|
-
const child = spawn("pnpm", [
|
|
173
|
-
"--dir",
|
|
174
|
-
app.dir,
|
|
175
|
-
"exec",
|
|
176
|
-
"vite",
|
|
177
|
-
"--host",
|
|
178
|
-
"127.0.0.1",
|
|
179
|
-
"--port",
|
|
180
|
-
String(app.port),
|
|
181
|
-
"--strictPort",
|
|
182
|
-
...(forceVite ? ["--force"] : []),
|
|
183
|
-
], {
|
|
184
|
-
cwd: root,
|
|
185
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
186
|
-
env: {
|
|
187
|
-
...process.env,
|
|
188
|
-
APP_NAME: app.id,
|
|
189
|
-
AGENT_NATIVE_WORKSPACE: "1",
|
|
190
|
-
AGENT_NATIVE_WORKSPACE_APPS_JSON: workspaceAppsJson,
|
|
191
|
-
APP_BASE_PATH: basePath,
|
|
192
|
-
VITE_AGENT_NATIVE_WORKSPACE: "1",
|
|
193
|
-
VITE_APP_BASE_PATH: basePath,
|
|
194
|
-
PORT: String(app.port),
|
|
195
|
-
WORKSPACE_GATEWAY_URL: gatewayUrl,
|
|
196
|
-
},
|
|
197
|
-
});
|
|
198
|
-
app.process = child;
|
|
199
|
-
const prefix = `[${app.id}]`;
|
|
200
|
-
const stableTimer = setTimeout(() => {
|
|
201
|
-
app.restartAttempts = 0;
|
|
202
|
-
}, 5_000);
|
|
203
|
-
stableTimer.unref();
|
|
204
|
-
child.stdout?.on("data", (chunk) => {
|
|
205
|
-
pipeAppOutput(prefix, chunk, (value) => process.stdout.write(value));
|
|
206
|
-
});
|
|
207
|
-
child.stderr?.on("data", (chunk) => {
|
|
208
|
-
pipeAppOutput(prefix, chunk, (value) => process.stderr.write(value));
|
|
209
|
-
});
|
|
210
|
-
child.on("exit", (code) => {
|
|
211
|
-
clearTimeout(stableTimer);
|
|
212
|
-
app.process = undefined;
|
|
213
|
-
app.ready = false;
|
|
214
|
-
if (code === 0 || shuttingDown)
|
|
215
|
-
return;
|
|
216
|
-
app.restartAttempts = (app.restartAttempts ?? 0) + 1;
|
|
217
|
-
const delay = appRestartDelay(app.restartAttempts);
|
|
218
|
-
console.error(`${prefix} exited with code ${code}; retrying in ${Math.round(delay / 1000)}s`);
|
|
219
|
-
app.restartTimer = setTimeout(() => {
|
|
220
|
-
app.restartTimer = undefined;
|
|
221
|
-
startApp(app);
|
|
222
|
-
}, delay);
|
|
223
|
-
app.restartTimer.unref();
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
function renderIndex() {
|
|
227
|
-
return `<!doctype html>
|
|
228
|
-
<html>
|
|
229
|
-
<head>
|
|
230
|
-
<meta charset="utf-8" />
|
|
231
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
232
|
-
<title>Agent-Native Workspace</title>
|
|
233
|
-
<style>
|
|
234
|
-
body { font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; margin: 0; padding: 32px; background: #fafafa; color: #171717; }
|
|
235
|
-
main { max-width: 760px; margin: 0 auto; }
|
|
236
|
-
a { color: inherit; text-decoration: none; }
|
|
237
|
-
.grid { display: grid; gap: 12px; margin-top: 20px; }
|
|
238
|
-
.card { display: flex; justify-content: space-between; border: 1px solid #d4d4d4; border-radius: 8px; padding: 14px 16px; background: white; }
|
|
239
|
-
.muted { color: #737373; }
|
|
240
|
-
</style>
|
|
241
|
-
</head>
|
|
242
|
-
<body>
|
|
243
|
-
<main>
|
|
244
|
-
<h1>Agent-Native Workspace</h1>
|
|
245
|
-
<p class="muted">Open an app below. Dispatch is the workspace control plane.</p>
|
|
246
|
-
<div class="grid">
|
|
247
|
-
${apps
|
|
248
|
-
.map((app) => `<a class="card" href="/${app.id}"><strong>${app.name}</strong><span class="muted">/${app.id}</span></a>`)
|
|
249
|
-
.join("")}
|
|
250
|
-
</div>
|
|
251
|
-
</main>
|
|
252
|
-
</body>
|
|
253
|
-
</html>`;
|
|
254
|
-
}
|
|
255
|
-
// On `pnpm dev` the gateway answers requests immediately, but each app's vite
|
|
256
|
-
// server takes a beat to bind its port. Without retry, the user sees an
|
|
257
|
-
// "App is not ready yet: ECONNREFUSED" banner on the first page load and has
|
|
258
|
-
// to refresh manually. We do a quick pre-flight TCP connect with retry so
|
|
259
|
-
// startup is invisible for the common case (small/no body, slow boot).
|
|
260
|
-
const PROXY_READY_TIMEOUT_MS = Number(process.env.WORKSPACE_PROXY_READY_TIMEOUT_MS ?? 30_000);
|
|
261
|
-
const PROXY_READY_RETRY_DELAY_MS = 250;
|
|
262
|
-
const APP_RESTART_MAX_DELAY_MS = 10_000;
|
|
263
105
|
function appRestartDelay(attempts) {
|
|
264
106
|
return Math.min(1_000 * 2 ** Math.max(0, attempts - 1), APP_RESTART_MAX_DELAY_MS);
|
|
265
107
|
}
|
|
@@ -288,230 +130,510 @@ function firstHeaderValue(value) {
|
|
|
288
130
|
return undefined;
|
|
289
131
|
return String(value);
|
|
290
132
|
}
|
|
291
|
-
function
|
|
292
|
-
|
|
293
|
-
|
|
133
|
+
function wantsHtml(req) {
|
|
134
|
+
if (req.method !== "GET" && req.method !== "HEAD")
|
|
135
|
+
return false;
|
|
136
|
+
const accept = firstHeaderValue(req.headers.accept);
|
|
137
|
+
if (!accept)
|
|
138
|
+
return false;
|
|
139
|
+
return accept.includes("text/html");
|
|
294
140
|
}
|
|
295
|
-
function
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
141
|
+
function renderStartingApp(app) {
|
|
142
|
+
const escapedName = escapeHtml(app.name || app.id);
|
|
143
|
+
return `<!doctype html>
|
|
144
|
+
<html>
|
|
145
|
+
<head>
|
|
146
|
+
<meta charset="utf-8" />
|
|
147
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
148
|
+
<meta http-equiv="refresh" content="1" />
|
|
149
|
+
<title>Starting ${escapedName}</title>
|
|
150
|
+
<style>
|
|
151
|
+
body { min-height: 100vh; margin: 0; display: grid; place-items: center; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #fafafa; color: #171717; }
|
|
152
|
+
main { width: min(420px, calc(100vw - 48px)); }
|
|
153
|
+
.bar { height: 3px; overflow: hidden; border-radius: 999px; background: #e5e5e5; }
|
|
154
|
+
.bar::before { content: ""; display: block; height: 100%; width: 42%; border-radius: inherit; background: #171717; animation: load 1s ease-in-out infinite; }
|
|
155
|
+
p { color: #737373; }
|
|
156
|
+
@keyframes load { 0% { transform: translateX(-105%); } 100% { transform: translateX(245%); } }
|
|
157
|
+
</style>
|
|
158
|
+
<script>setTimeout(() => window.location.reload(), 900);</script>
|
|
159
|
+
</head>
|
|
160
|
+
<body>
|
|
161
|
+
<main>
|
|
162
|
+
<div class="bar"></div>
|
|
163
|
+
<h1>Starting ${escapedName}</h1>
|
|
164
|
+
<p>The workspace gateway is waking this app's dev server.</p>
|
|
165
|
+
</main>
|
|
166
|
+
</body>
|
|
167
|
+
</html>`;
|
|
299
168
|
}
|
|
300
|
-
function
|
|
301
|
-
return {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
169
|
+
function escapeHtml(value) {
|
|
170
|
+
return value.replace(/[&<>"']/g, (char) => {
|
|
171
|
+
switch (char) {
|
|
172
|
+
case "&":
|
|
173
|
+
return "&";
|
|
174
|
+
case "<":
|
|
175
|
+
return "<";
|
|
176
|
+
case ">":
|
|
177
|
+
return ">";
|
|
178
|
+
case '"':
|
|
179
|
+
return """;
|
|
180
|
+
default:
|
|
181
|
+
return "'";
|
|
182
|
+
}
|
|
183
|
+
});
|
|
307
184
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
185
|
+
function renderIndex(apps) {
|
|
186
|
+
return `<!doctype html>
|
|
187
|
+
<html>
|
|
188
|
+
<head>
|
|
189
|
+
<meta charset="utf-8" />
|
|
190
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
191
|
+
<title>Agent-Native Workspace</title>
|
|
192
|
+
<style>
|
|
193
|
+
body { font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; margin: 0; padding: 32px; background: #fafafa; color: #171717; }
|
|
194
|
+
main { max-width: 760px; margin: 0 auto; }
|
|
195
|
+
a { color: inherit; text-decoration: none; }
|
|
196
|
+
.grid { display: grid; gap: 12px; margin-top: 20px; }
|
|
197
|
+
.card { display: flex; justify-content: space-between; border: 1px solid #d4d4d4; border-radius: 8px; padding: 14px 16px; background: white; }
|
|
198
|
+
.muted { color: #737373; }
|
|
199
|
+
</style>
|
|
200
|
+
</head>
|
|
201
|
+
<body>
|
|
202
|
+
<main>
|
|
203
|
+
<h1>Agent-Native Workspace</h1>
|
|
204
|
+
<p class="muted">Open an app below. Dispatch is the workspace control plane when installed.</p>
|
|
205
|
+
<div class="grid">
|
|
206
|
+
${apps
|
|
207
|
+
.map((app) => `<a class="card" href="/${app.id}"><strong>${escapeHtml(app.name)}</strong><span class="muted">/${escapeHtml(app.id)}</span></a>`)
|
|
208
|
+
.join("")}
|
|
209
|
+
</div>
|
|
210
|
+
</main>
|
|
211
|
+
</body>
|
|
212
|
+
</html>`;
|
|
315
213
|
}
|
|
316
|
-
function
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
214
|
+
export function runWorkspaceDev(options = {}) {
|
|
215
|
+
const args = options.args ?? process.argv.slice(2);
|
|
216
|
+
const env = options.env ?? process.env;
|
|
217
|
+
const root = options.root ?? process.cwd();
|
|
218
|
+
const appsDir = path.join(root, "apps");
|
|
219
|
+
const spawnProcess = options.spawnProcess ?? spawn;
|
|
220
|
+
const stdout = options.stdout ?? process.stdout;
|
|
221
|
+
const stderr = options.stderr ?? process.stderr;
|
|
222
|
+
fs.mkdirSync(path.join(root, "data"), { recursive: true });
|
|
223
|
+
const gatewayHost = env.WORKSPACE_HOST || DEFAULT_GATEWAY_HOST;
|
|
224
|
+
const requestedPort = Number(env.WORKSPACE_PORT || env.PORT || DEFAULT_GATEWAY_PORT);
|
|
225
|
+
const appPortStart = Number(env.WORKSPACE_APP_PORT_START || DEFAULT_APP_PORT_START);
|
|
226
|
+
const forceVite = env.WORKSPACE_VITE_FORCE === "1";
|
|
227
|
+
const eager = shouldEagerStartWorkspaceApps(args, env);
|
|
228
|
+
const proxyReadyTimeoutMs = Number(env.WORKSPACE_PROXY_READY_TIMEOUT_MS ?? 30_000);
|
|
229
|
+
let gatewayUrl = `http://${gatewayHost}:${requestedPort}`;
|
|
230
|
+
const apps = discoverApps(appsDir, appPortStart);
|
|
231
|
+
if (apps.length === 0) {
|
|
232
|
+
throw new Error("[workspace] No apps found under ./apps");
|
|
233
|
+
}
|
|
234
|
+
const appById = new Map(apps.map((app) => [app.id, app]));
|
|
235
|
+
const explicitDefaultApp = env.WORKSPACE_DEFAULT_APP && appById.has(env.WORKSPACE_DEFAULT_APP)
|
|
236
|
+
? env.WORKSPACE_DEFAULT_APP
|
|
237
|
+
: null;
|
|
238
|
+
const hasDispatch = appById.has("dispatch");
|
|
239
|
+
const defaultApp = explicitDefaultApp ?? (hasDispatch ? "dispatch" : apps[0].id);
|
|
240
|
+
const redirectRootToDefault = Boolean(explicitDefaultApp || hasDispatch);
|
|
241
|
+
let syncTimer;
|
|
242
|
+
let shuttingDown = false;
|
|
243
|
+
let workspaceStarted = false;
|
|
244
|
+
let readyResolve;
|
|
245
|
+
const ready = new Promise((resolve) => {
|
|
246
|
+
readyResolve = resolve;
|
|
247
|
+
});
|
|
248
|
+
function workspaceAppsJson() {
|
|
249
|
+
return JSON.stringify(apps.map((workspaceApp) => ({
|
|
250
|
+
id: workspaceApp.id,
|
|
251
|
+
name: workspaceApp.name,
|
|
252
|
+
path: `/${workspaceApp.id}`,
|
|
253
|
+
})));
|
|
254
|
+
}
|
|
255
|
+
function syncApps() {
|
|
256
|
+
const discovered = discoverApps(appsDir, appPortStart);
|
|
257
|
+
for (const app of discovered) {
|
|
258
|
+
const existing = appById.get(app.id);
|
|
259
|
+
if (existing) {
|
|
260
|
+
existing.name = app.name;
|
|
261
|
+
existing.dir = app.dir;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const usedPorts = new Set(apps.map((existingApp) => existingApp.port));
|
|
265
|
+
let port = appPortStart;
|
|
266
|
+
while (usedPorts.has(port))
|
|
267
|
+
port++;
|
|
268
|
+
const next = { ...app, port };
|
|
269
|
+
apps.push(next);
|
|
270
|
+
apps.sort(compareApps);
|
|
271
|
+
appById.set(next.id, next);
|
|
272
|
+
stdout.write(`[workspace] Detected new app: /${next.id}\n`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function scheduleSync() {
|
|
276
|
+
if (syncTimer)
|
|
277
|
+
clearTimeout(syncTimer);
|
|
278
|
+
syncTimer = setTimeout(syncApps, 400);
|
|
279
|
+
}
|
|
280
|
+
function appForRequest(req) {
|
|
281
|
+
const params = new URL(req.url || "/", "http://workspace.local")
|
|
282
|
+
.searchParams;
|
|
283
|
+
const explicit = params.get("_app");
|
|
284
|
+
if (explicit && appById.has(explicit))
|
|
285
|
+
return appById.get(explicit) ?? null;
|
|
286
|
+
const direct = firstPathSegment(req.url);
|
|
287
|
+
if (direct && appById.has(direct))
|
|
288
|
+
return appById.get(direct) ?? null;
|
|
289
|
+
const fromState = extractOAuthStateAppId(params.get("state"));
|
|
290
|
+
if (fromState && appById.has(fromState)) {
|
|
291
|
+
return appById.get(fromState) ?? null;
|
|
292
|
+
}
|
|
293
|
+
const referer = req.headers.referer;
|
|
294
|
+
const fromReferer = typeof referer === "string" ? firstPathSegment(referer) : null;
|
|
295
|
+
return fromReferer && appById.has(fromReferer)
|
|
296
|
+
? (appById.get(fromReferer) ?? null)
|
|
297
|
+
: null;
|
|
298
|
+
}
|
|
299
|
+
function startApp(app) {
|
|
300
|
+
if (app.process && !app.process.killed)
|
|
301
|
+
return;
|
|
302
|
+
if (app.restartTimer) {
|
|
303
|
+
clearTimeout(app.restartTimer);
|
|
304
|
+
app.restartTimer = undefined;
|
|
305
|
+
}
|
|
306
|
+
const basePath = `/${app.id}`;
|
|
307
|
+
const child = spawnProcess("pnpm", [
|
|
308
|
+
"--dir",
|
|
309
|
+
app.dir,
|
|
310
|
+
"exec",
|
|
311
|
+
"vite",
|
|
312
|
+
"--host",
|
|
313
|
+
"127.0.0.1",
|
|
314
|
+
"--port",
|
|
315
|
+
String(app.port),
|
|
316
|
+
"--strictPort",
|
|
317
|
+
...(forceVite ? ["--force"] : []),
|
|
318
|
+
], {
|
|
319
|
+
cwd: root,
|
|
320
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
321
|
+
env: {
|
|
322
|
+
...env,
|
|
323
|
+
APP_NAME: app.id,
|
|
324
|
+
AGENT_NATIVE_WORKSPACE: "1",
|
|
325
|
+
AGENT_NATIVE_WORKSPACE_APPS_JSON: workspaceAppsJson(),
|
|
326
|
+
APP_BASE_PATH: basePath,
|
|
327
|
+
VITE_AGENT_NATIVE_WORKSPACE: "1",
|
|
328
|
+
VITE_APP_BASE_PATH: basePath,
|
|
329
|
+
PORT: String(app.port),
|
|
330
|
+
WORKSPACE_GATEWAY_URL: gatewayUrl,
|
|
331
|
+
},
|
|
329
332
|
});
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
+
app.process = child;
|
|
334
|
+
const prefix = `[${app.id}]`;
|
|
335
|
+
const stableTimer = setTimeout(() => {
|
|
336
|
+
app.restartAttempts = 0;
|
|
337
|
+
}, 5_000);
|
|
338
|
+
stableTimer.unref();
|
|
339
|
+
child.stdout?.on("data", (chunk) => {
|
|
340
|
+
pipeAppOutput(prefix, chunk, (value) => stdout.write(value));
|
|
341
|
+
});
|
|
342
|
+
child.stderr?.on("data", (chunk) => {
|
|
343
|
+
pipeAppOutput(prefix, chunk, (value) => stderr.write(value));
|
|
344
|
+
});
|
|
345
|
+
child.on("exit", (code) => {
|
|
346
|
+
clearTimeout(stableTimer);
|
|
347
|
+
app.process = undefined;
|
|
348
|
+
app.ready = false;
|
|
349
|
+
if (code === 0 || shuttingDown)
|
|
333
350
|
return;
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
351
|
+
app.restartAttempts = (app.restartAttempts ?? 0) + 1;
|
|
352
|
+
const delay = appRestartDelay(app.restartAttempts);
|
|
353
|
+
stderr.write(`${prefix} exited with code ${code}; retrying in ${Math.round(delay / 1000)}s\n`);
|
|
354
|
+
app.restartTimer = setTimeout(() => {
|
|
355
|
+
app.restartTimer = undefined;
|
|
356
|
+
startApp(app);
|
|
357
|
+
}, delay);
|
|
358
|
+
app.restartTimer.unref();
|
|
337
359
|
});
|
|
338
|
-
req.pipe(proxyReq);
|
|
339
|
-
};
|
|
340
|
-
// Fast path: the upstream has accepted at least one request before, so it's
|
|
341
|
-
// listening. Skip the probe so steady-state requests stay zero-latency.
|
|
342
|
-
if (app.ready) {
|
|
343
|
-
dispatch();
|
|
344
|
-
return;
|
|
345
360
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
361
|
+
function forwardedProto(req) {
|
|
362
|
+
return (firstHeaderValue(req.headers["x-forwarded-proto"]) ||
|
|
363
|
+
(req.socket.encrypted ? "https" : "http"));
|
|
364
|
+
}
|
|
365
|
+
function forwardedHost(req) {
|
|
366
|
+
return (firstHeaderValue(req.headers["x-forwarded-host"]) ||
|
|
367
|
+
firstHeaderValue(req.headers.host) ||
|
|
368
|
+
new URL(gatewayUrl).host);
|
|
369
|
+
}
|
|
370
|
+
function proxyHeaders(req, targetHost) {
|
|
371
|
+
return {
|
|
372
|
+
...req.headers,
|
|
373
|
+
"x-forwarded-host": forwardedHost(req),
|
|
374
|
+
"x-forwarded-proto": forwardedProto(req),
|
|
375
|
+
host: targetHost,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
async function waitForPort(port, deadline) {
|
|
379
|
+
while (Date.now() < deadline) {
|
|
380
|
+
if (await probePort(port))
|
|
381
|
+
return true;
|
|
382
|
+
await new Promise((r) => setTimeout(r, PROXY_READY_RETRY_DELAY_MS));
|
|
383
|
+
}
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
function proxyHttp(app, req, res) {
|
|
387
|
+
const cold = !app.process || app.process.killed;
|
|
388
|
+
startApp(app);
|
|
389
|
+
if (!app.ready && wantsHtml(req)) {
|
|
390
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
391
|
+
if (req.method === "HEAD") {
|
|
392
|
+
res.end();
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
res.end(renderStartingApp(app));
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
const dispatch = () => {
|
|
399
|
+
const headers = proxyHeaders(req, `127.0.0.1:${app.port}`);
|
|
400
|
+
const proxyReq = http.request({
|
|
401
|
+
hostname: "127.0.0.1",
|
|
402
|
+
port: app.port,
|
|
403
|
+
method: req.method,
|
|
404
|
+
path: req.url,
|
|
405
|
+
headers,
|
|
406
|
+
}, (proxyRes) => {
|
|
407
|
+
app.ready = true;
|
|
408
|
+
res.writeHead(proxyRes.statusCode ?? 502, proxyRes.headers);
|
|
409
|
+
proxyRes.pipe(res);
|
|
410
|
+
});
|
|
411
|
+
proxyReq.on("error", (err) => {
|
|
412
|
+
if (res.headersSent) {
|
|
413
|
+
res.end();
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
352
416
|
res.writeHead(502, { "content-type": "text/plain" });
|
|
353
|
-
res.end(`App "${app.id}" is not ready yet:
|
|
417
|
+
res.end(`App "${app.id}" is not ready yet: ${err.message}`);
|
|
418
|
+
});
|
|
419
|
+
req.pipe(proxyReq);
|
|
420
|
+
};
|
|
421
|
+
// Fast path: the upstream has accepted at least one request before, so
|
|
422
|
+
// it's listening. Skip the probe so steady-state requests stay zero-latency.
|
|
423
|
+
if (app.ready && !cold) {
|
|
424
|
+
dispatch();
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
// Cold path: hold non-HTML requests open while the child server boots.
|
|
428
|
+
// Node keeps the request body paused until pipe() attaches.
|
|
429
|
+
void waitForPort(app.port, Date.now() + proxyReadyTimeoutMs).then((ready) => {
|
|
430
|
+
if (!ready) {
|
|
431
|
+
if (!res.headersSent) {
|
|
432
|
+
res.writeHead(502, { "content-type": "text/plain" });
|
|
433
|
+
res.end(`App "${app.id}" is not ready yet: connect ECONNREFUSED 127.0.0.1:${app.port}`);
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
res.end();
|
|
437
|
+
}
|
|
438
|
+
return;
|
|
354
439
|
}
|
|
355
|
-
|
|
356
|
-
|
|
440
|
+
app.ready = true;
|
|
441
|
+
dispatch();
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
function proxyUpgrade(app, req, socket, head) {
|
|
445
|
+
startApp(app);
|
|
446
|
+
void waitForPort(app.port, Date.now() + proxyReadyTimeoutMs).then((ready) => {
|
|
447
|
+
if (!ready) {
|
|
448
|
+
socket.destroy();
|
|
449
|
+
return;
|
|
357
450
|
}
|
|
451
|
+
app.ready = true;
|
|
452
|
+
const target = net.connect(app.port, "127.0.0.1", () => {
|
|
453
|
+
const headers = Object.entries(proxyHeaders(req, `127.0.0.1:${app.port}`))
|
|
454
|
+
.flatMap(([key, value]) => Array.isArray(value)
|
|
455
|
+
? value.map((item) => `${key}: ${item}`)
|
|
456
|
+
: [`${key}: ${value ?? ""}`])
|
|
457
|
+
.join("\r\n");
|
|
458
|
+
target.write(`${req.method} ${req.url} HTTP/${req.httpVersion}\r\n${headers}\r\n\r\n`);
|
|
459
|
+
if (head.length)
|
|
460
|
+
target.write(head);
|
|
461
|
+
socket.pipe(target).pipe(socket);
|
|
462
|
+
});
|
|
463
|
+
target.on("error", () => socket.destroy());
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
function handleWatcherError(err) {
|
|
467
|
+
if (err.code === "ENOSPC") {
|
|
468
|
+
stderr.write(`[workspace] Recursive file watcher hit the system limit (ENOSPC). ` +
|
|
469
|
+
`New apps will still be detected via polling every ~2s. ` +
|
|
470
|
+
`On Linux you can raise the limit with ` +
|
|
471
|
+
`\`sudo sysctl fs.inotify.max_user_watches=524288\` ` +
|
|
472
|
+
`(persist via /etc/sysctl.d/*.conf). On macOS/Windows this usually ` +
|
|
473
|
+
`means too many other watchers are running.\n`);
|
|
474
|
+
Sentry.captureException(err, {
|
|
475
|
+
tags: { handled: "dev-watch-enospc" },
|
|
476
|
+
level: "warning",
|
|
477
|
+
});
|
|
358
478
|
return;
|
|
359
479
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
const target = net.connect(app.port, "127.0.0.1", () => {
|
|
366
|
-
const headers = Object.entries(proxyHeaders(req, `127.0.0.1:${app.port}`))
|
|
367
|
-
.flatMap(([key, value]) => Array.isArray(value)
|
|
368
|
-
? value.map((item) => `${key}: ${item}`)
|
|
369
|
-
: [`${key}: ${value ?? ""}`])
|
|
370
|
-
.join("\r\n");
|
|
371
|
-
target.write(`${req.method} ${req.url} HTTP/${req.httpVersion}\r\n${headers}\r\n\r\n`);
|
|
372
|
-
if (head.length)
|
|
373
|
-
target.write(head);
|
|
374
|
-
socket.pipe(target).pipe(socket);
|
|
375
|
-
});
|
|
376
|
-
target.on("error", () => socket.destroy());
|
|
377
|
-
}
|
|
378
|
-
let shuttingDown = false;
|
|
379
|
-
let workspaceStarted = false;
|
|
380
|
-
function handleWatcherError(err) {
|
|
381
|
-
// ENOSPC: system inotify watcher limit hit (Linux). Userland-fixable;
|
|
382
|
-
// capture as a warning so we still see frequency in Sentry but don't get
|
|
383
|
-
// paged. Print actionable guidance and continue without watching — the
|
|
384
|
-
// 2s polling interval below keeps app discovery working.
|
|
385
|
-
if (err.code === "ENOSPC") {
|
|
386
|
-
console.warn(`[workspace] Recursive file watcher hit the system limit (ENOSPC). ` +
|
|
387
|
-
`New apps will still be detected via polling every ~2s. ` +
|
|
388
|
-
`On Linux you can raise the limit with ` +
|
|
389
|
-
`\`sudo sysctl fs.inotify.max_user_watches=524288\` ` +
|
|
390
|
-
`(persist via /etc/sysctl.d/*.conf). On macOS/Windows this usually ` +
|
|
391
|
-
`means too many other watchers are running.`);
|
|
480
|
+
if (err.code === "ENOENT") {
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
stderr.write(`[workspace] Recursive file watcher failed (${err.code ?? "unknown"}): ${err.message}. ` +
|
|
484
|
+
`Falling back to polling.\n`);
|
|
392
485
|
Sentry.captureException(err, {
|
|
393
|
-
tags: { handled: "dev-watch-
|
|
486
|
+
tags: { handled: "dev-watch-unknown" },
|
|
394
487
|
level: "warning",
|
|
395
488
|
});
|
|
396
|
-
return;
|
|
397
489
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
}
|
|
415
|
-
function startWorkspaceProcesses() {
|
|
416
|
-
if (workspaceStarted)
|
|
417
|
-
return;
|
|
418
|
-
workspaceStarted = true;
|
|
419
|
-
for (const app of apps)
|
|
420
|
-
startApp(app);
|
|
421
|
-
try {
|
|
422
|
-
const watcher = fs.watch(appsDir, { recursive: true }, scheduleSync);
|
|
423
|
-
// Async errors (e.g. ENOENT when a subdir vanishes mid-watch) surface on
|
|
424
|
-
// the watcher rather than the original call site. Without an `error`
|
|
425
|
-
// listener, Node would treat them as uncaught and crash the dev process.
|
|
426
|
-
watcher.on("error", (err) => {
|
|
490
|
+
function startWorkspaceProcesses() {
|
|
491
|
+
if (workspaceStarted)
|
|
492
|
+
return;
|
|
493
|
+
workspaceStarted = true;
|
|
494
|
+
for (const id of initialWorkspaceAppIds(apps, defaultApp, eager, redirectRootToDefault)) {
|
|
495
|
+
const app = appById.get(id);
|
|
496
|
+
if (app)
|
|
497
|
+
startApp(app);
|
|
498
|
+
}
|
|
499
|
+
try {
|
|
500
|
+
const watcher = fs.watch(appsDir, { recursive: true }, scheduleSync);
|
|
501
|
+
watcher.on("error", (err) => {
|
|
502
|
+
handleWatcherError(err);
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
catch (err) {
|
|
427
506
|
handleWatcherError(err);
|
|
428
|
-
}
|
|
507
|
+
}
|
|
508
|
+
setInterval(syncApps, 2_000).unref();
|
|
429
509
|
}
|
|
430
|
-
|
|
431
|
-
|
|
510
|
+
function openBrowser(url) {
|
|
511
|
+
if (options.openBrowser === false || env.WORKSPACE_NO_OPEN === "1")
|
|
512
|
+
return;
|
|
513
|
+
const command = process.platform === "darwin"
|
|
514
|
+
? "open"
|
|
515
|
+
: process.platform === "win32"
|
|
516
|
+
? "cmd"
|
|
517
|
+
: "xdg-open";
|
|
518
|
+
const openArgs = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
519
|
+
const child = spawnProcess(command, openArgs, {
|
|
520
|
+
stdio: "ignore",
|
|
521
|
+
detached: true,
|
|
522
|
+
});
|
|
523
|
+
child.unref();
|
|
432
524
|
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
525
|
+
const server = http.createServer((req, res) => {
|
|
526
|
+
if (req.url === "/" || req.url === "/index.html") {
|
|
527
|
+
if (redirectRootToDefault) {
|
|
528
|
+
res.writeHead(302, { location: `/${defaultApp}` });
|
|
529
|
+
res.end();
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
533
|
+
res.end(renderIndex(apps));
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
if (req.url === "/_workspace/apps") {
|
|
537
|
+
syncApps();
|
|
538
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
539
|
+
res.end(JSON.stringify(apps.map((app) => ({
|
|
540
|
+
id: app.id,
|
|
541
|
+
name: app.name,
|
|
542
|
+
path: `/${app.id}`,
|
|
543
|
+
port: app.port,
|
|
544
|
+
running: Boolean(app.process && !app.process.killed),
|
|
545
|
+
}))));
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
let app = appForRequest(req);
|
|
549
|
+
if (!app) {
|
|
550
|
+
syncApps();
|
|
551
|
+
app = appForRequest(req);
|
|
552
|
+
}
|
|
553
|
+
if (!app) {
|
|
554
|
+
res.writeHead(404, { "content-type": "text/html; charset=utf-8" });
|
|
555
|
+
res.end(renderIndex(apps));
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
proxyHttp(app, req, res);
|
|
447
559
|
});
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
res.writeHead(302, { location: `/${defaultApp}` });
|
|
453
|
-
res.end();
|
|
454
|
-
return;
|
|
455
|
-
}
|
|
456
|
-
if (req.url === "/_workspace/apps") {
|
|
457
|
-
res.writeHead(200, { "content-type": "application/json" });
|
|
458
|
-
res.end(JSON.stringify(apps.map((app) => ({
|
|
459
|
-
id: app.id,
|
|
460
|
-
name: app.name,
|
|
461
|
-
path: `/${app.id}`,
|
|
462
|
-
port: app.port,
|
|
463
|
-
}))));
|
|
464
|
-
return;
|
|
465
|
-
}
|
|
466
|
-
const app = appForRequest(req);
|
|
467
|
-
if (!app) {
|
|
468
|
-
res.writeHead(404, { "content-type": "text/html" });
|
|
469
|
-
res.end(renderIndex());
|
|
470
|
-
return;
|
|
471
|
-
}
|
|
472
|
-
proxyHttp(app, req, res);
|
|
473
|
-
});
|
|
474
|
-
server.on("upgrade", (req, socket, head) => {
|
|
475
|
-
const app = appForRequest(req);
|
|
476
|
-
if (!app) {
|
|
477
|
-
socket.destroy();
|
|
478
|
-
return;
|
|
479
|
-
}
|
|
480
|
-
proxyUpgrade(app, req, socket, head);
|
|
481
|
-
});
|
|
482
|
-
function listen(port, attempts = 20) {
|
|
483
|
-
server.once("error", (err) => {
|
|
484
|
-
if (err.code === "EADDRINUSE" && attempts > 0) {
|
|
485
|
-
listen(port + 1, attempts - 1);
|
|
560
|
+
server.on("upgrade", (req, socket, head) => {
|
|
561
|
+
const app = appForRequest(req);
|
|
562
|
+
if (!app) {
|
|
563
|
+
socket.destroy();
|
|
486
564
|
return;
|
|
487
565
|
}
|
|
488
|
-
|
|
489
|
-
process.exit(1);
|
|
566
|
+
proxyUpgrade(app, req, socket, head);
|
|
490
567
|
});
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
568
|
+
function listen(port, attempts = 20) {
|
|
569
|
+
server.once("error", (err) => {
|
|
570
|
+
if (err.code === "EADDRINUSE" && attempts > 0) {
|
|
571
|
+
listen(port + 1, attempts - 1);
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
stderr.write(`[workspace] Could not start gateway: ${err.message}\n`);
|
|
575
|
+
throw err;
|
|
576
|
+
});
|
|
577
|
+
server.listen(port, gatewayHost, () => {
|
|
578
|
+
const address = server.address();
|
|
579
|
+
const actualPort = typeof address === "object" && address ? address.port : port;
|
|
580
|
+
gatewayUrl = `http://${gatewayHost}:${actualPort}`;
|
|
581
|
+
stdout.write(`[workspace] Default: ${redirectRootToDefault ? `${gatewayUrl}/${defaultApp}` : gatewayUrl}\n`);
|
|
582
|
+
stdout.write(`[workspace] Gateway: ${gatewayUrl}\n`);
|
|
583
|
+
stdout.write(`[workspace] Mode: ${eager ? "eager" : "lazy"}\n`);
|
|
584
|
+
for (const app of apps) {
|
|
585
|
+
stdout.write(`[workspace] ${app.id}: /${app.id} -> 127.0.0.1:${app.port}\n`);
|
|
586
|
+
}
|
|
587
|
+
startWorkspaceProcesses();
|
|
588
|
+
openBrowser(redirectRootToDefault ? `${gatewayUrl}/${defaultApp}` : gatewayUrl);
|
|
589
|
+
readyResolve({ port: actualPort, url: gatewayUrl });
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
function shutdown() {
|
|
593
|
+
if (shuttingDown)
|
|
594
|
+
return;
|
|
595
|
+
shuttingDown = true;
|
|
596
|
+
server.close();
|
|
497
597
|
for (const app of apps) {
|
|
498
|
-
|
|
598
|
+
app.process?.kill("SIGTERM");
|
|
499
599
|
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
600
|
+
if (syncTimer)
|
|
601
|
+
clearTimeout(syncTimer);
|
|
602
|
+
process.off("SIGINT", handleSigint);
|
|
603
|
+
process.off("SIGTERM", handleSigterm);
|
|
604
|
+
}
|
|
605
|
+
const handleSigint = () => shutdown();
|
|
606
|
+
const handleSigterm = () => shutdown();
|
|
607
|
+
process.once("SIGINT", handleSigint);
|
|
608
|
+
process.once("SIGTERM", handleSigterm);
|
|
609
|
+
listen(requestedPort);
|
|
610
|
+
return {
|
|
611
|
+
apps,
|
|
612
|
+
defaultApp,
|
|
613
|
+
gatewayUrl: () => gatewayUrl,
|
|
614
|
+
ready,
|
|
615
|
+
server,
|
|
616
|
+
shutdown,
|
|
617
|
+
};
|
|
503
618
|
}
|
|
504
|
-
function
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
619
|
+
function isDirectRun() {
|
|
620
|
+
const entry = process.argv[1];
|
|
621
|
+
if (!entry)
|
|
622
|
+
return false;
|
|
623
|
+
try {
|
|
624
|
+
return path.resolve(entry) === fileURLToPath(import.meta.url);
|
|
625
|
+
}
|
|
626
|
+
catch {
|
|
627
|
+
return false;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
if (isDirectRun()) {
|
|
631
|
+
try {
|
|
632
|
+
runWorkspaceDev({ args: process.argv.slice(2) });
|
|
633
|
+
}
|
|
634
|
+
catch (err) {
|
|
635
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
636
|
+
process.exit(1);
|
|
511
637
|
}
|
|
512
|
-
setTimeout(() => process.exit(0), 300).unref();
|
|
513
638
|
}
|
|
514
|
-
process.on("SIGINT", shutdown);
|
|
515
|
-
process.on("SIGTERM", shutdown);
|
|
516
|
-
listen(requestedPort);
|
|
517
639
|
//# sourceMappingURL=workspace-dev.js.map
|