@agent-native/core 0.12.15 → 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/agent/run-manager.d.ts.map +1 -1
- package/dist/agent/run-manager.js +56 -42
- package/dist/agent/run-manager.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 -363
- 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 +49 -14
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/client/MultiTabAssistantChat.js +17 -3
- package/dist/client/MultiTabAssistantChat.js.map +1 -1
- package/dist/client/NewWorkspaceAppFlow.d.ts.map +1 -1
- package/dist/client/NewWorkspaceAppFlow.js +4 -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/sharing/ShareButton.js +6 -1
- package/dist/client/sharing/ShareButton.js.map +1 -1
- package/dist/client/sharing/ShareButton.spec.d.ts +2 -0
- package/dist/client/sharing/ShareButton.spec.d.ts.map +1 -0
- package/dist/client/sharing/ShareButton.spec.js +90 -0
- package/dist/client/sharing/ShareButton.spec.js.map +1 -0
- package/dist/client/sse-event-processor.d.ts.map +1 -1
- package/dist/client/sse-event-processor.js +10 -2
- package/dist/client/sse-event-processor.js.map +1 -1
- package/dist/client/use-chat-threads.d.ts.map +1 -1
- package/dist/client/use-chat-threads.js +19 -2
- package/dist/client/use-chat-threads.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/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +11 -7
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/templates/default/AGENTS.md +7 -1
- package/dist/templates/default/DEVELOPING.md +12 -0
- package/dist/templates/default/app/hooks/use-navigation-state.ts +81 -0
- package/dist/templates/default/app/root.tsx +11 -5
- 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/default/AGENTS.md +7 -1
- package/src/templates/default/DEVELOPING.md +12 -0
- package/src/templates/default/app/hooks/use-navigation-state.ts +81 -0
- package/src/templates/default/app/root.tsx +11 -5
- 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,92 +102,87 @@ function firstPathSegment(url) {
|
|
|
138
102
|
return null;
|
|
139
103
|
}
|
|
140
104
|
}
|
|
141
|
-
function
|
|
142
|
-
|
|
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;
|
|
105
|
+
function appRestartDelay(attempts) {
|
|
106
|
+
return Math.min(1_000 * 2 ** Math.max(0, attempts - 1), APP_RESTART_MAX_DELAY_MS);
|
|
158
107
|
}
|
|
159
|
-
function
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
"
|
|
174
|
-
|
|
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));
|
|
108
|
+
function probePort(port, timeoutMs = 1_000) {
|
|
109
|
+
return new Promise((resolve) => {
|
|
110
|
+
const socket = new net.Socket();
|
|
111
|
+
let settled = false;
|
|
112
|
+
const finish = (ok) => {
|
|
113
|
+
if (settled)
|
|
114
|
+
return;
|
|
115
|
+
settled = true;
|
|
116
|
+
socket.destroy();
|
|
117
|
+
resolve(ok);
|
|
118
|
+
};
|
|
119
|
+
socket.setTimeout(timeoutMs);
|
|
120
|
+
socket.once("connect", () => finish(true));
|
|
121
|
+
socket.once("error", () => finish(false));
|
|
122
|
+
socket.once("timeout", () => finish(false));
|
|
123
|
+
socket.connect(port, "127.0.0.1");
|
|
209
124
|
});
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
125
|
+
}
|
|
126
|
+
function firstHeaderValue(value) {
|
|
127
|
+
if (Array.isArray(value))
|
|
128
|
+
return value[0];
|
|
129
|
+
if (value === undefined)
|
|
130
|
+
return undefined;
|
|
131
|
+
return String(value);
|
|
132
|
+
}
|
|
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");
|
|
140
|
+
}
|
|
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>`;
|
|
168
|
+
}
|
|
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
|
+
}
|
|
224
183
|
});
|
|
225
184
|
}
|
|
226
|
-
function renderIndex() {
|
|
185
|
+
function renderIndex(apps) {
|
|
227
186
|
return `<!doctype html>
|
|
228
187
|
<html>
|
|
229
188
|
<head>
|
|
@@ -242,255 +201,439 @@ function renderIndex() {
|
|
|
242
201
|
<body>
|
|
243
202
|
<main>
|
|
244
203
|
<h1>Agent-Native Workspace</h1>
|
|
245
|
-
<p class="muted">Open an app below. Dispatch is the workspace control plane.</p>
|
|
204
|
+
<p class="muted">Open an app below. Dispatch is the workspace control plane when installed.</p>
|
|
246
205
|
<div class="grid">
|
|
247
206
|
${apps
|
|
248
|
-
.map((app) => `<a class="card" href="/${app.id}"><strong>${app.name}</strong><span class="muted">/${app.id}</span></a>`)
|
|
207
|
+
.map((app) => `<a class="card" href="/${app.id}"><strong>${escapeHtml(app.name)}</strong><span class="muted">/${escapeHtml(app.id)}</span></a>`)
|
|
249
208
|
.join("")}
|
|
250
209
|
</div>
|
|
251
210
|
</main>
|
|
252
211
|
</body>
|
|
253
212
|
</html>`;
|
|
254
213
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const
|
|
261
|
-
const
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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;
|
|
282
247
|
});
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
248
|
+
function workspaceAppsJson() {
|
|
249
|
+
return JSON.stringify(apps.map((workspaceApp) => ({
|
|
250
|
+
id: workspaceApp.id,
|
|
251
|
+
name: workspaceApp.name,
|
|
252
|
+
path: `/${workspaceApp.id}`,
|
|
253
|
+
})));
|
|
289
254
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
+
},
|
|
305
332
|
});
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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)
|
|
309
350
|
return;
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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();
|
|
313
359
|
});
|
|
314
|
-
req.pipe(proxyReq);
|
|
315
|
-
};
|
|
316
|
-
// Fast path: the upstream has accepted at least one request before, so it's
|
|
317
|
-
// listening. Skip the probe so steady-state requests stay zero-latency.
|
|
318
|
-
if (app.ready) {
|
|
319
|
-
dispatch();
|
|
320
|
-
return;
|
|
321
360
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
+
}
|
|
328
416
|
res.writeHead(502, { "content-type": "text/plain" });
|
|
329
|
-
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;
|
|
330
439
|
}
|
|
331
|
-
|
|
332
|
-
|
|
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;
|
|
333
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
|
+
});
|
|
334
478
|
return;
|
|
335
479
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
const target = net.connect(app.port, "127.0.0.1", () => {
|
|
342
|
-
const headers = Object.entries({
|
|
343
|
-
...req.headers,
|
|
344
|
-
host: `127.0.0.1:${app.port}`,
|
|
345
|
-
})
|
|
346
|
-
.flatMap(([key, value]) => Array.isArray(value)
|
|
347
|
-
? value.map((item) => `${key}: ${item}`)
|
|
348
|
-
: [`${key}: ${value ?? ""}`])
|
|
349
|
-
.join("\r\n");
|
|
350
|
-
target.write(`${req.method} ${req.url} HTTP/${req.httpVersion}\r\n${headers}\r\n\r\n`);
|
|
351
|
-
if (head.length)
|
|
352
|
-
target.write(head);
|
|
353
|
-
socket.pipe(target).pipe(socket);
|
|
354
|
-
});
|
|
355
|
-
target.on("error", () => socket.destroy());
|
|
356
|
-
}
|
|
357
|
-
let shuttingDown = false;
|
|
358
|
-
let workspaceStarted = false;
|
|
359
|
-
function handleWatcherError(err) {
|
|
360
|
-
// ENOSPC: system inotify watcher limit hit (Linux). Userland-fixable;
|
|
361
|
-
// capture as a warning so we still see frequency in Sentry but don't get
|
|
362
|
-
// paged. Print actionable guidance and continue without watching — the
|
|
363
|
-
// 2s polling interval below keeps app discovery working.
|
|
364
|
-
if (err.code === "ENOSPC") {
|
|
365
|
-
console.warn(`[workspace] Recursive file watcher hit the system limit (ENOSPC). ` +
|
|
366
|
-
`New apps will still be detected via polling every ~2s. ` +
|
|
367
|
-
`On Linux you can raise the limit with ` +
|
|
368
|
-
`\`sudo sysctl fs.inotify.max_user_watches=524288\` ` +
|
|
369
|
-
`(persist via /etc/sysctl.d/*.conf). On macOS/Windows this usually ` +
|
|
370
|
-
`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`);
|
|
371
485
|
Sentry.captureException(err, {
|
|
372
|
-
tags: { handled: "dev-watch-
|
|
486
|
+
tags: { handled: "dev-watch-unknown" },
|
|
373
487
|
level: "warning",
|
|
374
488
|
});
|
|
375
|
-
return;
|
|
376
|
-
}
|
|
377
|
-
// ENOENT: a watched directory disappeared (or a transient subdir under
|
|
378
|
-
// appsDir vanished mid-enumeration). Benign — the polling fallback and
|
|
379
|
-
// future scheduleSync calls will re-establish state. Don't capture.
|
|
380
|
-
if (err.code === "ENOENT") {
|
|
381
|
-
console.debug(`[workspace] Recursive file watcher saw a directory disappear ` +
|
|
382
|
-
`(ENOENT: ${err.path ?? "unknown"}). Polling fallback will recover.`);
|
|
383
|
-
return;
|
|
384
489
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
const watcher = fs.watch(appsDir, { recursive: true }, scheduleSync);
|
|
402
|
-
// Async errors (e.g. ENOENT when a subdir vanishes mid-watch) surface on
|
|
403
|
-
// the watcher rather than the original call site. Without an `error`
|
|
404
|
-
// listener, Node would treat them as uncaught and crash the dev process.
|
|
405
|
-
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) {
|
|
406
506
|
handleWatcherError(err);
|
|
407
|
-
}
|
|
507
|
+
}
|
|
508
|
+
setInterval(syncApps, 2_000).unref();
|
|
408
509
|
}
|
|
409
|
-
|
|
410
|
-
|
|
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();
|
|
411
524
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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);
|
|
426
559
|
});
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
res.writeHead(302, { location: `/${defaultApp}` });
|
|
432
|
-
res.end();
|
|
433
|
-
return;
|
|
434
|
-
}
|
|
435
|
-
if (req.url === "/_workspace/apps") {
|
|
436
|
-
res.writeHead(200, { "content-type": "application/json" });
|
|
437
|
-
res.end(JSON.stringify(apps.map((app) => ({
|
|
438
|
-
id: app.id,
|
|
439
|
-
name: app.name,
|
|
440
|
-
path: `/${app.id}`,
|
|
441
|
-
port: app.port,
|
|
442
|
-
}))));
|
|
443
|
-
return;
|
|
444
|
-
}
|
|
445
|
-
const app = appForRequest(req);
|
|
446
|
-
if (!app) {
|
|
447
|
-
res.writeHead(404, { "content-type": "text/html" });
|
|
448
|
-
res.end(renderIndex());
|
|
449
|
-
return;
|
|
450
|
-
}
|
|
451
|
-
proxyHttp(app, req, res);
|
|
452
|
-
});
|
|
453
|
-
server.on("upgrade", (req, socket, head) => {
|
|
454
|
-
const app = appForRequest(req);
|
|
455
|
-
if (!app) {
|
|
456
|
-
socket.destroy();
|
|
457
|
-
return;
|
|
458
|
-
}
|
|
459
|
-
proxyUpgrade(app, req, socket, head);
|
|
460
|
-
});
|
|
461
|
-
function listen(port, attempts = 20) {
|
|
462
|
-
server.once("error", (err) => {
|
|
463
|
-
if (err.code === "EADDRINUSE" && attempts > 0) {
|
|
464
|
-
listen(port + 1, attempts - 1);
|
|
560
|
+
server.on("upgrade", (req, socket, head) => {
|
|
561
|
+
const app = appForRequest(req);
|
|
562
|
+
if (!app) {
|
|
563
|
+
socket.destroy();
|
|
465
564
|
return;
|
|
466
565
|
}
|
|
467
|
-
|
|
468
|
-
process.exit(1);
|
|
566
|
+
proxyUpgrade(app, req, socket, head);
|
|
469
567
|
});
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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();
|
|
476
597
|
for (const app of apps) {
|
|
477
|
-
|
|
598
|
+
app.process?.kill("SIGTERM");
|
|
478
599
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
+
};
|
|
482
618
|
}
|
|
483
|
-
function
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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);
|
|
490
637
|
}
|
|
491
|
-
setTimeout(() => process.exit(0), 300).unref();
|
|
492
638
|
}
|
|
493
|
-
process.on("SIGINT", shutdown);
|
|
494
|
-
process.on("SIGTERM", shutdown);
|
|
495
|
-
listen(requestedPort);
|
|
496
639
|
//# sourceMappingURL=workspace-dev.js.map
|