conductor-remote 1.117.0 → 1.118.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -8
- package/dist/assets/{PierrePatch-D3_YBIAr.js → PierrePatch-D4YbKD8Q.js} +1 -1
- package/dist/assets/index-BRzsSW8g.css +1 -0
- package/dist/assets/index-BaS6qXUW.js +65 -0
- package/dist/index.html +2 -2
- package/dist/push-sw.js +2 -2
- package/dist/sw.js +1 -1
- package/dist-node/scripts/nosleep-setup.js +2 -2
- package/dist-node/scripts/nosleep.js +3 -3
- package/dist-node/scripts/service/commands.js +32 -0
- package/dist-node/scripts/service/configuration.js +205 -0
- package/dist-node/scripts/service/environment.js +59 -0
- package/dist-node/scripts/service/flags.js +45 -0
- package/dist-node/scripts/service/installation.js +91 -0
- package/dist-node/scripts/service/launch-agent.js +134 -0
- package/dist-node/scripts/service/network.js +244 -0
- package/dist-node/scripts/service/presentation.js +101 -0
- package/dist-node/scripts/service/voice.js +113 -0
- package/dist-node/scripts/service.js +5 -980
- package/dist-node/src/{agent-config.js → agents/agent-config.js} +1 -1
- package/dist-node/src/{model-cache.js → agents/model-cache.js} +1 -1
- package/dist-node/src/{roles.js → agents/roles.js} +3 -3
- package/dist-node/src/db.js +1 -1
- package/dist-node/src/{parked.js → delivery/parked.js} +1 -1
- package/dist-node/src/{dev-server.js → dev-server/controller.js} +8 -291
- package/dist-node/src/dev-server/ports.js +63 -0
- package/dist-node/src/dev-server/processes.js +73 -0
- package/dist-node/src/dev-server/proxy.js +158 -0
- package/dist-node/src/{run-activity.js → dev-server/run-activity.js} +2 -2
- package/dist-node/src/dev-server/types.js +1 -0
- package/dist-node/src/{file-preview.js → files/file-preview.js} +1 -1
- package/dist-node/src/{change-stats.js → git/change-stats.js} +1 -1
- package/dist-node/src/{git.js → git/diff.js} +1 -1
- package/dist-node/src/{autoupdate.js → host/autoupdate.js} +1 -1
- package/dist-node/src/{funnel-watchdog.js → host/funnel-watchdog.js} +4 -4
- package/dist-node/src/{logbuf.js → host/logbuf.js} +1 -1
- package/dist-node/src/{nosleep-helper.js → host/nosleep-helper.js} +1 -1
- package/dist-node/src/{tailscale.js → host/tailscale.js} +1 -1
- package/dist-node/src/http/router-types.js +2 -0
- package/dist-node/src/http/router.js +78 -0
- package/dist-node/src/http/routes/create-workspace.js +90 -0
- package/dist-node/src/http/routes/files.js +53 -0
- package/dist-node/src/http/routes/prompts.js +363 -0
- package/dist-node/src/http/routes/sessions.js +279 -0
- package/dist-node/src/http/routes/state.js +186 -0
- package/dist-node/src/http/routes/system.js +220 -0
- package/dist-node/src/http/routes/voice.js +117 -0
- package/dist-node/src/http/routes/workflows.js +248 -0
- package/dist-node/src/http/routes/workspaces.js +267 -0
- package/dist-node/src/http/services/base.js +252 -0
- package/dist-node/src/http/services/delegations.js +301 -0
- package/dist-node/src/http/services/delivery.js +433 -0
- package/dist-node/src/http/services/files.js +199 -0
- package/dist-node/src/http/services/mcp.js +94 -0
- package/dist-node/src/http/services/responses.js +120 -0
- package/dist-node/src/http/services/voice.js +147 -0
- package/dist-node/src/http/services/workflow-probes.js +215 -0
- package/dist-node/src/http/services/workflow-state.js +195 -0
- package/dist-node/src/http/services/workflow.js +366 -0
- package/dist-node/src/http/services.js +35 -0
- package/dist-node/src/mcp/arguments.js +14 -0
- package/dist-node/src/mcp/dispatcher.js +56 -0
- package/dist-node/src/mcp/formatters.js +110 -0
- package/dist-node/src/mcp/protocol.js +7 -0
- package/dist-node/src/mcp/registry.js +66 -0
- package/dist-node/src/mcp/tools/agent-options.js +122 -0
- package/dist-node/src/mcp/tools/chat-actions.js +194 -0
- package/dist-node/src/mcp/tools/chats.js +194 -0
- package/dist-node/src/mcp/tools/delegation.js +135 -0
- package/dist-node/src/mcp/tools/system.js +135 -0
- package/dist-node/src/mcp/tools/voice.js +136 -0
- package/dist-node/src/mcp/tools/workspaces.js +259 -0
- package/dist-node/src/mcp/types.js +1 -0
- package/dist-node/src/mcp.js +6 -4
- package/dist-node/src/{notify.js → notifications/notify.js} +5 -5
- package/dist-node/src/{webpush.js → notifications/webpush.js} +1 -1
- package/dist-node/src/orchestration/delegation/codec.js +201 -0
- package/dist-node/src/{delegation-intake.js → orchestration/delegation/intake.js} +2 -2
- package/dist-node/src/{delegation-prompt.js → orchestration/delegation/prompt.js} +1 -1
- package/dist-node/src/orchestration/delegation/queue.js +255 -0
- package/dist-node/src/orchestration/delegation/store.js +111 -0
- package/dist-node/src/orchestration/delegation/types.js +2 -0
- package/dist-node/src/orchestration/persistence/capabilities.js +109 -0
- package/dist-node/src/orchestration/persistence/codecs.js +148 -0
- package/dist-node/src/orchestration/persistence/connection.js +205 -0
- package/dist-node/src/orchestration/persistence/db.js +101 -0
- package/dist-node/src/orchestration/persistence/effect-recovery.js +227 -0
- package/dist-node/src/orchestration/persistence/effect-transitions.js +157 -0
- package/dist-node/src/orchestration/persistence/effects.js +109 -0
- package/dist-node/src/orchestration/persistence/errors.js +39 -0
- package/dist-node/src/orchestration/persistence/events.js +29 -0
- package/dist-node/src/orchestration/persistence/idempotency.js +82 -0
- package/dist-node/src/orchestration/persistence/job-attempts.js +114 -0
- package/dist-node/src/orchestration/persistence/jobs.js +159 -0
- package/dist-node/src/orchestration/persistence/projections.js +190 -0
- package/dist-node/src/orchestration/persistence/quarantine.js +46 -0
- package/dist-node/src/orchestration/persistence/records.js +115 -0
- package/dist-node/src/orchestration/persistence/relays.js +78 -0
- package/dist-node/src/orchestration/persistence/runs.js +156 -0
- package/dist-node/src/{orchestration-schema.js → orchestration/persistence/schema.js} +1 -1
- package/dist-node/src/orchestration/persistence/types.js +1 -0
- package/dist-node/src/orchestration/persistence/ui-lease.js +197 -0
- package/dist-node/src/orchestration/persistence/values.js +114 -0
- package/dist-node/src/orchestration/workflow/commands.js +309 -0
- package/dist-node/src/orchestration/workflow/coordinator.js +28 -0
- package/dist-node/src/orchestration/workflow/effect-recovery.js +125 -0
- package/dist-node/src/orchestration/workflow/effects.js +307 -0
- package/dist-node/src/orchestration/workflow/errors.js +13 -0
- package/dist-node/src/orchestration/workflow/helpers.js +154 -0
- package/dist-node/src/orchestration/workflow/job-dispatch.js +312 -0
- package/dist-node/src/orchestration/workflow/job-results.js +280 -0
- package/dist-node/src/{workflow.js → orchestration/workflow/prompts.js} +2 -2
- package/dist-node/src/orchestration/workflow/root.js +293 -0
- package/dist-node/src/orchestration/workflow/start.js +99 -0
- package/dist-node/src/orchestration/workflow/state.js +105 -0
- package/dist-node/src/orchestration/workflow/types.js +1 -0
- package/dist-node/src/orchestration/workflow/wake.js +164 -0
- package/dist-node/src/prefs.js +1 -1
- package/dist-node/src/{background-tasks.js → reads/background-tasks.js} +1 -1
- package/dist-node/src/reads/messages.js +362 -0
- package/dist-node/src/reads/repository.js +87 -0
- package/dist-node/src/reads/sessions.js +165 -0
- package/dist-node/src/reads/types.js +1 -0
- package/dist-node/src/reads/workspaces.js +230 -0
- package/dist-node/src/reads/worktrees.js +45 -0
- package/dist-node/src/routes.js +5 -5
- package/dist-node/src/{search.js → search/coordinator.js} +4 -4
- package/dist-node/src/{search-worker.js → search/worker.js} +3 -3
- package/dist-node/src/server.js +14 -3930
- package/dist-node/src/settings.js +1 -1
- package/dist-node/src/shared.js +10 -10
- package/dist-node/src/{transcript.js → transcript/parser.js} +4 -4
- package/dist-node/src/{tool-usage-worker.js → usage/tool-usage-worker.js} +1 -1
- package/dist-node/src/{tool-usage.js → usage/tool-usage.js} +1 -1
- package/dist-node/src/voice/brief.js +22 -50
- package/dist-node/src/voice/context.js +1 -1
- package/dist-node/src/voice/dates.js +38 -0
- package/dist-node/src/voice/history.js +14 -5
- package/dist-node/src/voice/prompt.js +10 -9
- package/dist-node/src/voice/recall.js +115 -0
- package/dist-node/src/{speech.js → voice/speech.js} +1 -1
- package/dist-node/src/voice/tools.js +92 -3
- package/dist-node/src/voice/webrtc.js +1 -1
- package/dist-node/src/wire.js +2 -2
- package/dist-node/src/writes/actuator.js +124 -0
- package/dist-node/src/writes/agent-options.js +151 -0
- package/dist-node/src/writes/applescript/agent-options.applescript +334 -0
- package/dist-node/src/writes/applescript/chat-targeting.applescript +149 -0
- package/dist-node/src/writes/applescript/composer.applescript +195 -0
- package/dist-node/src/writes/applescript/hotspot.applescript +155 -0
- package/dist-node/src/writes/applescript/lifecycle.applescript +266 -0
- package/dist-node/src/writes/applescript/run-tasks.applescript +208 -0
- package/dist-node/src/writes/applescript/source.js +52 -0
- package/dist-node/src/writes/applescript/window.applescript +436 -0
- package/dist-node/src/writes/applescript/workspace-status.applescript +177 -0
- package/dist-node/src/writes/applescript/workspace-targeting.applescript +359 -0
- package/dist-node/src/writes/chats.js +160 -0
- package/dist-node/src/writes/guards.js +77 -0
- package/dist-node/src/writes/runner.js +67 -0
- package/dist-node/src/{sidecar.js → writes/sidecar.js} +1 -1
- package/dist-node/src/writes/system.js +81 -0
- package/dist-node/src/writes/targeting.js +121 -0
- package/dist-node/src/writes/types.js +1 -0
- package/dist-node/src/writes/ui-lock.js +229 -0
- package/dist-node/src/writes/workspaces.js +233 -0
- package/docs/voice-setup.md +34 -12
- package/package.json +2 -2
- package/dist/assets/index-ByTwVInL.css +0 -1
- package/dist/assets/index-LAm9X7wv.js +0 -65
- package/dist-node/src/conductor.applescript +0 -2279
- package/dist-node/src/delegations.js +0 -557
- package/dist-node/src/mcp-tools.js +0 -1319
- package/dist-node/src/orchestration-db.js +0 -2186
- package/dist-node/src/reads.js +0 -787
- package/dist-node/src/workflow-coordinator.js +0 -2119
- package/dist-node/src/writes.js +0 -1223
- /package/dist-node/src/{conductor-settings.js → agents/conductor-settings.js} +0 -0
- /package/dist-node/src/{firstprompt.js → delivery/firstprompt.js} +0 -0
- /package/dist-node/src/{sendonce.js → delivery/sendonce.js} +0 -0
- /package/dist-node/src/{preview-urls.js → dev-server/preview-urls.js} +0 -0
- /package/dist-node/src/{run-configs.js → dev-server/run-configs.js} +0 -0
- /package/dist-node/src/{attachments.js → files/attachments.js} +0 -0
- /package/dist-node/src/{icons.js → files/icons.js} +0 -0
- /package/dist-node/src/{staged-attachments.js → files/staged-attachments.js} +0 -0
- /package/dist-node/src/{fork-workspace.js → git/fork-workspace.js} +0 -0
- /package/dist-node/src/{merge.js → git/merge.js} +0 -0
- /package/dist-node/src/{pr.js → git/pr.js} +0 -0
- /package/dist-node/src/{nosleep.js → host/nosleep.js} +0 -0
- /package/dist-node/src/{relay-processes.js → host/relay-processes.js} +0 -0
- /package/dist-node/src/{ui-lease-watchdog.js → host/ui-lease-watchdog.js} +0 -0
- /package/dist-node/src/{wifi.js → host/wifi.js} +0 -0
- /package/dist-node/src/{orchestration-schema.generated.js → orchestration/persistence/schema.generated.js} +0 -0
- /package/dist-node/src/{workflow-effect-runner.js → orchestration/workflow/effect-runner.js} +0 -0
- /package/dist-node/src/{workflow-http.js → orchestration/workflow/http.js} +0 -0
- /package/dist-node/src/{workflow-machine.js → orchestration/workflow/machine.js} +0 -0
- /package/dist-node/src/{session-poller.js → reads/session-poller.js} +0 -0
- /package/dist-node/src/{context-breakdown.js → transcript/context-breakdown.js} +0 -0
- /package/dist-node/src/{chat-cursor.js → transcript/cursor.js} +0 -0
- /package/dist-node/src/{plan-usage.js → usage/plan-usage.js} +0 -0
- /package/dist-node/src/{tool-usage-service.js → usage/tool-usage-service.js} +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { agentTypeCanExposeEffort, agentTypeCanExposeFastMode, modelAgentType, modelLabel, modelPickerLabel, shortModel } from "
|
|
1
|
+
import { agentTypeCanExposeEffort, agentTypeCanExposeFastMode, modelAgentType, modelLabel, modelPickerLabel, shortModel } from "../shared.js";
|
|
2
2
|
function modelMatches(state, requested, allowPrefix = false) {
|
|
3
3
|
const label = modelPickerLabel(requested).trim();
|
|
4
4
|
const expectedAgentType = modelAgentType(label);
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import fs from 'node:fs';
|
|
10
10
|
import path from 'node:path';
|
|
11
|
-
import { stateDir } from "
|
|
12
|
-
import { currentModelCatalog, modelAgentType, modelPickerLabel } from "
|
|
13
|
-
export { newestModelSnapshot } from "
|
|
11
|
+
import { stateDir } from "../config.js";
|
|
12
|
+
import { currentModelCatalog, modelAgentType, modelPickerLabel } from "../shared.js";
|
|
13
|
+
export { newestModelSnapshot } from "../shared.js";
|
|
14
14
|
const EFFORTS = new Set(['none', 'low', 'medium', 'high', 'xhigh', 'max', 'ultracode']);
|
|
15
15
|
const ROLE_NAME = /^[a-z][a-z0-9_-]{0,63}$/;
|
|
16
16
|
const MAX_ROLES = 32;
|
package/dist-node/src/db.js
CHANGED
|
@@ -6,7 +6,7 @@ const SLOW_QUERY_LOG_INTERVAL_MS = 60_000;
|
|
|
6
6
|
*
|
|
7
7
|
* The desktop app holds the same file open in WAL mode; a second read-only
|
|
8
8
|
* connection sees every committed write without blocking the app. We never
|
|
9
|
-
* write through this handle — writes go through the actuator (see writes.ts).
|
|
9
|
+
* write through this handle — writes go through the actuator (see src/writes/actuator.ts).
|
|
10
10
|
*/
|
|
11
11
|
export class ConductorDb {
|
|
12
12
|
dbPath;
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* the lock hands its prompt to this queue and returns at once: the phone shows it
|
|
9
9
|
* as a queued bubble in the chat (`workspace.parked_prompts` on `/api/state`),
|
|
10
10
|
* the relay polls the lock and delivers when it lifts, and a push notification
|
|
11
|
-
* says whether it landed. Same ownership story as `firstprompt.ts`: the relay is
|
|
11
|
+
* says whether it landed. Same ownership story as `src/delivery/firstprompt.ts`: the relay is
|
|
12
12
|
* the only deliverer, the phone only watches and can dismiss.
|
|
13
13
|
*
|
|
14
14
|
* The same three properties hold it up:
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* the tailnet-only HTTPS port.
|
|
4
4
|
*
|
|
5
5
|
* The relay never spawns a repository command itself. Starting and stopping go
|
|
6
|
-
* through Conductor's real Run button (`writes.ts`), which preserves its task
|
|
6
|
+
* through Conductor's real Run button (`src/writes/workspaces.ts`), which preserves its task
|
|
7
7
|
* selection, environment, run-mode rules and process-group cleanup. This module
|
|
8
8
|
* discovers that workspace's allocated `CONDUCTOR_PORT`, accepts complete URLs
|
|
9
9
|
* from Conductor configuration or a running application, and gives each local
|
|
@@ -20,309 +20,26 @@
|
|
|
20
20
|
import { execFile } from 'node:child_process';
|
|
21
21
|
import crypto from 'node:crypto';
|
|
22
22
|
import fs from 'node:fs';
|
|
23
|
-
import http, {} from 'node:http';
|
|
24
|
-
import net from 'node:net';
|
|
25
23
|
import path from 'node:path';
|
|
26
24
|
import { promisify } from 'node:util';
|
|
27
|
-
import { stateDir } from "
|
|
25
|
+
import { stateDir } from "../config.js";
|
|
26
|
+
import { magicDnsName, tailscaleBin } from "../host/tailscale.js";
|
|
27
|
+
import { setRunTask } from "../writes/workspaces.js";
|
|
28
|
+
import { chooseServePort, forwardUrl, localPreviewUrl, previewPath, serveProxyAt, tcpOpen, validPort, waitForPort } from "./ports.js";
|
|
28
29
|
import { previewUrlSettings, resolvePreviewTargets } from "./preview-urls.js";
|
|
30
|
+
import { PORT_SNAPSHOT_TTL_MS, processAlive, workspacePort } from "./processes.js";
|
|
31
|
+
import { bridgeMatches, createDevProxy } from "./proxy.js";
|
|
29
32
|
import { runConfigsFor } from "./run-configs.js";
|
|
30
|
-
|
|
31
|
-
import { setRunTask } from "./writes.js";
|
|
32
|
-
const exec = promisify(execFile);
|
|
33
|
+
export const exec = promisify(execFile);
|
|
33
34
|
// Keep the receipt additive and version-1-readable: an installed relay and a
|
|
34
35
|
// source `yarn dev` relay can briefly share this file during an update.
|
|
35
36
|
const STORE_VERSION = 1;
|
|
36
37
|
const PREVIEW_ADVERTISEMENT_VERSION = 1;
|
|
37
38
|
const PORT_WAIT_MS = 15_000;
|
|
38
|
-
const PORT_SNAPSHOT_TTL_MS = 5000;
|
|
39
39
|
const MAX_PREVIEW_ADVERTISEMENT_BYTES = 64 * 1024;
|
|
40
40
|
function recordKey(workspaceId, targetPort) {
|
|
41
41
|
return `${workspaceId}:${targetPort}`;
|
|
42
42
|
}
|
|
43
|
-
function validPort(value) {
|
|
44
|
-
return Number.isInteger(value) && value > 0 && value <= 65535;
|
|
45
|
-
}
|
|
46
|
-
function processAlive(pid) {
|
|
47
|
-
if (!pid || !Number.isInteger(pid) || pid <= 0)
|
|
48
|
-
return false;
|
|
49
|
-
try {
|
|
50
|
-
process.kill(pid, 0);
|
|
51
|
-
return true;
|
|
52
|
-
}
|
|
53
|
-
catch {
|
|
54
|
-
return false;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
function bridgeMatches(record) {
|
|
58
|
-
if (!record.bridgeToken) {
|
|
59
|
-
// Compatibility for the first local prototype receipts. PID plus an open
|
|
60
|
-
// bridge is enough to avoid stealing one during this upgrade; every newly
|
|
61
|
-
// written receipt carries the unambiguous challenge below.
|
|
62
|
-
return processAlive(record.ownerPid) ? tcpOpen(record.bridgePort) : Promise.resolve(false);
|
|
63
|
-
}
|
|
64
|
-
return new Promise(resolve => {
|
|
65
|
-
let settled = false;
|
|
66
|
-
const finish = (matches) => {
|
|
67
|
-
if (settled)
|
|
68
|
-
return;
|
|
69
|
-
settled = true;
|
|
70
|
-
request.destroy();
|
|
71
|
-
resolve(matches);
|
|
72
|
-
};
|
|
73
|
-
const request = http.request({
|
|
74
|
-
host: '127.0.0.1',
|
|
75
|
-
port: record.bridgePort,
|
|
76
|
-
method: 'GET',
|
|
77
|
-
path: '/',
|
|
78
|
-
headers: { 'x-conductor-remote-bridge': record.bridgeToken }
|
|
79
|
-
}, response => {
|
|
80
|
-
response.resume();
|
|
81
|
-
finish(response.statusCode === 204);
|
|
82
|
-
});
|
|
83
|
-
request.setTimeout(500, () => finish(false));
|
|
84
|
-
request.once('error', () => finish(false));
|
|
85
|
-
request.end();
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* Parse a macOS `ps eww -axo command=` snapshot without ever logging it: process
|
|
90
|
-
* environments can contain credentials. Multiple processes are expected, but
|
|
91
|
-
* conflicting ports for one workspace are not safe to guess through.
|
|
92
|
-
*/
|
|
93
|
-
export function parseWorkspacePort(snapshot, workspaceId) {
|
|
94
|
-
const ports = new Set();
|
|
95
|
-
for (const line of snapshot.split('\n')) {
|
|
96
|
-
const id = line.match(/(?:^|\s)CONDUCTOR_WORKSPACE_ID=([^\s]+)/)?.[1];
|
|
97
|
-
if (id !== workspaceId)
|
|
98
|
-
continue;
|
|
99
|
-
const port = Number(line.match(/(?:^|\s)CONDUCTOR_PORT=(\d+)(?:\s|$)/)?.[1]);
|
|
100
|
-
if (validPort(port))
|
|
101
|
-
ports.add(port);
|
|
102
|
-
}
|
|
103
|
-
return ports.size === 1 ? [...ports][0] : null;
|
|
104
|
-
}
|
|
105
|
-
let portSnapshot = null;
|
|
106
|
-
let portSnapshotInFlight = null;
|
|
107
|
-
async function readProcesses() {
|
|
108
|
-
const { stdout } = await exec('ps', ['eww', '-axo', 'command='], {
|
|
109
|
-
encoding: 'utf8',
|
|
110
|
-
maxBuffer: 64 * 1024 * 1024,
|
|
111
|
-
timeout: 5000
|
|
112
|
-
});
|
|
113
|
-
portSnapshot = { at: Date.now(), text: stdout };
|
|
114
|
-
return stdout;
|
|
115
|
-
}
|
|
116
|
-
/**
|
|
117
|
-
* One `ps` snapshot shared by every workspace. It carries every process
|
|
118
|
-
* environment on the Mac (~650 kB here), and the phone polls this state every
|
|
119
|
-
* 2.5s per open chat, so a read reuses a recent one and simultaneous readers
|
|
120
|
-
* await the same child process. A workspace whose Run task is stopped has no
|
|
121
|
-
* port to remember, which is why the miss path needs this and not just the hit
|
|
122
|
-
* path. A start asks for `maxAgeMs` 0, because the task it just pressed is
|
|
123
|
-
* younger than any cached snapshot.
|
|
124
|
-
*/
|
|
125
|
-
function processSnapshot(maxAgeMs) {
|
|
126
|
-
if (maxAgeMs <= 0)
|
|
127
|
-
return readProcesses();
|
|
128
|
-
if (portSnapshot && Date.now() - portSnapshot.at <= maxAgeMs)
|
|
129
|
-
return Promise.resolve(portSnapshot.text);
|
|
130
|
-
portSnapshotInFlight ??= readProcesses().finally(() => {
|
|
131
|
-
portSnapshotInFlight = null;
|
|
132
|
-
});
|
|
133
|
-
return portSnapshotInFlight;
|
|
134
|
-
}
|
|
135
|
-
async function workspacePort(workspaceId, maxAgeMs = 0) {
|
|
136
|
-
try {
|
|
137
|
-
return parseWorkspacePort(await processSnapshot(maxAgeMs), workspaceId);
|
|
138
|
-
}
|
|
139
|
-
catch {
|
|
140
|
-
// Never reflect the error: `execFile` includes stdout, which is the process
|
|
141
|
-
// environment snapshot and may contain secrets.
|
|
142
|
-
return null;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
function tcpOpen(port, timeoutMs = 300) {
|
|
146
|
-
return new Promise(resolve => {
|
|
147
|
-
const socket = net.connect({ host: '127.0.0.1', port });
|
|
148
|
-
const finish = (open) => {
|
|
149
|
-
socket.destroy();
|
|
150
|
-
resolve(open);
|
|
151
|
-
};
|
|
152
|
-
socket.setTimeout(timeoutMs);
|
|
153
|
-
socket.once('connect', () => finish(true));
|
|
154
|
-
socket.once('timeout', () => finish(false));
|
|
155
|
-
socket.once('error', () => finish(false));
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
async function waitForPort(port, open, timeoutMs) {
|
|
159
|
-
const deadline = Date.now() + timeoutMs;
|
|
160
|
-
do {
|
|
161
|
-
if ((await tcpOpen(port)) === open)
|
|
162
|
-
return true;
|
|
163
|
-
await new Promise(resolve => setTimeout(resolve, 250));
|
|
164
|
-
} while (Date.now() < deadline);
|
|
165
|
-
return false;
|
|
166
|
-
}
|
|
167
|
-
function externalOrigin(req) {
|
|
168
|
-
const forwardedProto = String(req.headers['x-forwarded-proto'] ?? 'https')
|
|
169
|
-
.split(',')[0]
|
|
170
|
-
.trim();
|
|
171
|
-
const forwardedHost = String(req.headers['x-forwarded-host'] ?? req.headers.host ?? '')
|
|
172
|
-
.split(',')[0]
|
|
173
|
-
.trim();
|
|
174
|
-
return `${forwardedProto || 'https'}://${forwardedHost}`;
|
|
175
|
-
}
|
|
176
|
-
function localOrigin(targetPort) {
|
|
177
|
-
return `http://127.0.0.1:${targetPort}`;
|
|
178
|
-
}
|
|
179
|
-
function proxyHeaders(req, targetPort) {
|
|
180
|
-
const headers = { ...req.headers };
|
|
181
|
-
const publicHost = String(req.headers['x-forwarded-host'] ?? req.headers.host ?? '');
|
|
182
|
-
headers.host = `127.0.0.1:${targetPort}`;
|
|
183
|
-
if (publicHost)
|
|
184
|
-
headers['x-forwarded-host'] = publicHost;
|
|
185
|
-
headers['x-forwarded-proto'] = String(req.headers['x-forwarded-proto'] ?? 'https');
|
|
186
|
-
// Framework dev servers commonly apply the same allowlist to WebSocket Origin
|
|
187
|
-
// that they apply to Host. Present the local origin they were configured for.
|
|
188
|
-
if (headers.origin)
|
|
189
|
-
headers.origin = localOrigin(targetPort);
|
|
190
|
-
if (headers.referer) {
|
|
191
|
-
try {
|
|
192
|
-
const ref = new URL(headers.referer);
|
|
193
|
-
headers.referer = `${localOrigin(targetPort)}${ref.pathname}${ref.search}`;
|
|
194
|
-
}
|
|
195
|
-
catch {
|
|
196
|
-
// Preserve an unusual Referer rather than inventing one.
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
return headers;
|
|
200
|
-
}
|
|
201
|
-
function rewriteLocation(location, req, targetPort) {
|
|
202
|
-
if (!location)
|
|
203
|
-
return undefined;
|
|
204
|
-
const local = new RegExp(`^https?://(?:127\\.0\\.0\\.1|localhost)(?::${targetPort})?`, 'i');
|
|
205
|
-
return location.replace(local, externalOrigin(req));
|
|
206
|
-
}
|
|
207
|
-
function writeUpgradeRequest(upstream, req, head, targetPort) {
|
|
208
|
-
const headers = proxyHeaders(req, targetPort);
|
|
209
|
-
const lines = [`${req.method ?? 'GET'} ${req.url ?? '/'} HTTP/${req.httpVersion}`];
|
|
210
|
-
for (const [name, value] of Object.entries(headers)) {
|
|
211
|
-
if (value === undefined)
|
|
212
|
-
continue;
|
|
213
|
-
if (Array.isArray(value)) {
|
|
214
|
-
for (const item of value)
|
|
215
|
-
lines.push(`${name}: ${item}`);
|
|
216
|
-
}
|
|
217
|
-
else {
|
|
218
|
-
lines.push(`${name}: ${value}`);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
upstream.write(`${lines.join('\r\n')}\r\n\r\n`);
|
|
222
|
-
if (head.length)
|
|
223
|
-
upstream.write(head);
|
|
224
|
-
}
|
|
225
|
-
/** Loopback reverse proxy with raw WebSocket tunnelling for Vite-style HMR. */
|
|
226
|
-
export function createDevProxy(targetPort) {
|
|
227
|
-
return new Promise((resolve, reject) => {
|
|
228
|
-
const token = crypto.randomBytes(16).toString('hex');
|
|
229
|
-
const sockets = new Set();
|
|
230
|
-
const server = http.createServer((req, res) => {
|
|
231
|
-
if (req.headers['x-conductor-remote-bridge'] === token) {
|
|
232
|
-
res.writeHead(204);
|
|
233
|
-
return res.end();
|
|
234
|
-
}
|
|
235
|
-
const upstream = http.request({
|
|
236
|
-
host: '127.0.0.1',
|
|
237
|
-
port: targetPort,
|
|
238
|
-
method: req.method,
|
|
239
|
-
path: req.url,
|
|
240
|
-
headers: proxyHeaders(req, targetPort)
|
|
241
|
-
}, upstreamRes => {
|
|
242
|
-
const headers = { ...upstreamRes.headers };
|
|
243
|
-
const location = rewriteLocation(upstreamRes.headers.location, req, targetPort);
|
|
244
|
-
if (location)
|
|
245
|
-
headers.location = location;
|
|
246
|
-
res.writeHead(upstreamRes.statusCode ?? 502, headers);
|
|
247
|
-
upstreamRes.pipe(res);
|
|
248
|
-
});
|
|
249
|
-
upstream.once('error', () => {
|
|
250
|
-
if (!res.headersSent)
|
|
251
|
-
res.writeHead(502, { 'content-type': 'text/plain; charset=utf-8' });
|
|
252
|
-
res.end('Dev server is not reachable');
|
|
253
|
-
});
|
|
254
|
-
req.pipe(upstream);
|
|
255
|
-
});
|
|
256
|
-
server.on('connection', socket => {
|
|
257
|
-
sockets.add(socket);
|
|
258
|
-
socket.once('close', () => sockets.delete(socket));
|
|
259
|
-
});
|
|
260
|
-
server.on('upgrade', (req, socket, head) => {
|
|
261
|
-
const upstream = net.connect({ host: '127.0.0.1', port: targetPort }, () => {
|
|
262
|
-
writeUpgradeRequest(upstream, req, head, targetPort);
|
|
263
|
-
socket.pipe(upstream).pipe(socket);
|
|
264
|
-
});
|
|
265
|
-
sockets.add(upstream);
|
|
266
|
-
upstream.once('close', () => sockets.delete(upstream));
|
|
267
|
-
upstream.once('error', () => socket.destroy());
|
|
268
|
-
socket.once('error', () => upstream.destroy());
|
|
269
|
-
});
|
|
270
|
-
server.once('error', reject);
|
|
271
|
-
server.listen(0, '127.0.0.1', () => {
|
|
272
|
-
const address = server.address();
|
|
273
|
-
if (!address || typeof address === 'string') {
|
|
274
|
-
server.close();
|
|
275
|
-
return reject(new Error('could not allocate the dev-server bridge port'));
|
|
276
|
-
}
|
|
277
|
-
resolve({
|
|
278
|
-
port: address.port,
|
|
279
|
-
token,
|
|
280
|
-
close: () => new Promise(done => {
|
|
281
|
-
for (const socket of sockets)
|
|
282
|
-
socket.destroy();
|
|
283
|
-
server.close(() => done());
|
|
284
|
-
})
|
|
285
|
-
});
|
|
286
|
-
});
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
export function serveProxyAt(status, port) {
|
|
290
|
-
for (const [origin, web] of Object.entries(status.Web ?? {})) {
|
|
291
|
-
if (!origin.endsWith(`:${port}`))
|
|
292
|
-
continue;
|
|
293
|
-
return web.Handlers?.['/']?.Proxy ?? null;
|
|
294
|
-
}
|
|
295
|
-
return null;
|
|
296
|
-
}
|
|
297
|
-
function servePorts(status) {
|
|
298
|
-
return new Set(Object.keys(status.TCP ?? {})
|
|
299
|
-
.map(Number)
|
|
300
|
-
.filter(validPort));
|
|
301
|
-
}
|
|
302
|
-
function chooseServePort(status, targetPort, reserved = new Set()) {
|
|
303
|
-
const used = servePorts(status);
|
|
304
|
-
for (let offset = 0; offset < 10; offset++) {
|
|
305
|
-
const candidate = targetPort + offset;
|
|
306
|
-
if (validPort(candidate) && !used.has(candidate) && !reserved.has(candidate))
|
|
307
|
-
return candidate;
|
|
308
|
-
}
|
|
309
|
-
for (let candidate = 49152; candidate <= 65535; candidate++) {
|
|
310
|
-
if (!used.has(candidate) && !reserved.has(candidate))
|
|
311
|
-
return candidate;
|
|
312
|
-
}
|
|
313
|
-
return null;
|
|
314
|
-
}
|
|
315
|
-
function previewPath(previewUrl) {
|
|
316
|
-
const url = new URL(previewUrl);
|
|
317
|
-
return `${url.pathname || '/'}${url.search}${url.hash}`;
|
|
318
|
-
}
|
|
319
|
-
function localPreviewUrl(port, suffix = '/') {
|
|
320
|
-
return `http://localhost:${port}${suffix}`;
|
|
321
|
-
}
|
|
322
|
-
function forwardUrl(record, previewUrl) {
|
|
323
|
-
const suffix = previewUrl ? previewPath(previewUrl) : record.path || '/';
|
|
324
|
-
return `https://${record.host}${record.servePort === 443 ? '' : `:${record.servePort}`}${suffix}`;
|
|
325
|
-
}
|
|
326
43
|
export class DevServerController {
|
|
327
44
|
storeFile;
|
|
328
45
|
advertisementDir;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import net from 'node:net';
|
|
2
|
+
export function validPort(value) {
|
|
3
|
+
return Number.isInteger(value) && value > 0 && value <= 65535;
|
|
4
|
+
}
|
|
5
|
+
export function tcpOpen(port, timeoutMs = 300) {
|
|
6
|
+
return new Promise(resolve => {
|
|
7
|
+
const socket = net.connect({ host: '127.0.0.1', port });
|
|
8
|
+
const finish = (open) => {
|
|
9
|
+
socket.destroy();
|
|
10
|
+
resolve(open);
|
|
11
|
+
};
|
|
12
|
+
socket.setTimeout(timeoutMs);
|
|
13
|
+
socket.once('connect', () => finish(true));
|
|
14
|
+
socket.once('timeout', () => finish(false));
|
|
15
|
+
socket.once('error', () => finish(false));
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
export async function waitForPort(port, open, timeoutMs) {
|
|
19
|
+
const deadline = Date.now() + timeoutMs;
|
|
20
|
+
do {
|
|
21
|
+
if ((await tcpOpen(port)) === open)
|
|
22
|
+
return true;
|
|
23
|
+
await new Promise(resolve => setTimeout(resolve, 250));
|
|
24
|
+
} while (Date.now() < deadline);
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
export function serveProxyAt(status, port) {
|
|
28
|
+
for (const [origin, web] of Object.entries(status.Web ?? {})) {
|
|
29
|
+
if (!origin.endsWith(`:${port}`))
|
|
30
|
+
continue;
|
|
31
|
+
return web.Handlers?.['/']?.Proxy ?? null;
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
function servePorts(status) {
|
|
36
|
+
return new Set(Object.keys(status.TCP ?? {})
|
|
37
|
+
.map(Number)
|
|
38
|
+
.filter(validPort));
|
|
39
|
+
}
|
|
40
|
+
export function chooseServePort(status, targetPort, reserved = new Set()) {
|
|
41
|
+
const used = servePorts(status);
|
|
42
|
+
for (let offset = 0; offset < 10; offset++) {
|
|
43
|
+
const candidate = targetPort + offset;
|
|
44
|
+
if (validPort(candidate) && !used.has(candidate) && !reserved.has(candidate))
|
|
45
|
+
return candidate;
|
|
46
|
+
}
|
|
47
|
+
for (let candidate = 49152; candidate <= 65535; candidate++) {
|
|
48
|
+
if (!used.has(candidate) && !reserved.has(candidate))
|
|
49
|
+
return candidate;
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
export function previewPath(previewUrl) {
|
|
54
|
+
const url = new URL(previewUrl);
|
|
55
|
+
return `${url.pathname || '/'}${url.search}${url.hash}`;
|
|
56
|
+
}
|
|
57
|
+
export function localPreviewUrl(port, suffix = '/') {
|
|
58
|
+
return `http://localhost:${port}${suffix}`;
|
|
59
|
+
}
|
|
60
|
+
export function forwardUrl(record, previewUrl) {
|
|
61
|
+
const suffix = previewUrl ? previewPath(previewUrl) : record.path || '/';
|
|
62
|
+
return `https://${record.host}${record.servePort === 443 ? '' : `:${record.servePort}`}${suffix}`;
|
|
63
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
import { validPort } from "./ports.js";
|
|
4
|
+
const exec = promisify(execFile);
|
|
5
|
+
export const PORT_SNAPSHOT_TTL_MS = 5000;
|
|
6
|
+
export function processAlive(pid) {
|
|
7
|
+
if (!pid || !Number.isInteger(pid) || pid <= 0)
|
|
8
|
+
return false;
|
|
9
|
+
try {
|
|
10
|
+
process.kill(pid, 0);
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Parse a macOS `ps eww -axo command=` snapshot without ever logging it: process
|
|
19
|
+
* environments can contain credentials. Multiple processes are expected, but
|
|
20
|
+
* conflicting ports for one workspace are not safe to guess through.
|
|
21
|
+
*/
|
|
22
|
+
export function parseWorkspacePort(snapshot, workspaceId) {
|
|
23
|
+
const ports = new Set();
|
|
24
|
+
for (const line of snapshot.split('\n')) {
|
|
25
|
+
const id = line.match(/(?:^|\s)CONDUCTOR_WORKSPACE_ID=([^\s]+)/)?.[1];
|
|
26
|
+
if (id !== workspaceId)
|
|
27
|
+
continue;
|
|
28
|
+
const port = Number(line.match(/(?:^|\s)CONDUCTOR_PORT=(\d+)(?:\s|$)/)?.[1]);
|
|
29
|
+
if (validPort(port))
|
|
30
|
+
ports.add(port);
|
|
31
|
+
}
|
|
32
|
+
return ports.size === 1 ? [...ports][0] : null;
|
|
33
|
+
}
|
|
34
|
+
let portSnapshot = null;
|
|
35
|
+
let portSnapshotInFlight = null;
|
|
36
|
+
async function readProcesses() {
|
|
37
|
+
const { stdout } = await exec('ps', ['eww', '-axo', 'command='], {
|
|
38
|
+
encoding: 'utf8',
|
|
39
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
40
|
+
timeout: 5000
|
|
41
|
+
});
|
|
42
|
+
portSnapshot = { at: Date.now(), text: stdout };
|
|
43
|
+
return stdout;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* One `ps` snapshot shared by every workspace. It carries every process
|
|
47
|
+
* environment on the Mac (~650 kB here), and the phone polls this state every
|
|
48
|
+
* 2.5s per open chat, so a read reuses a recent one and simultaneous readers
|
|
49
|
+
* await the same child process. A workspace whose Run task is stopped has no
|
|
50
|
+
* port to remember, which is why the miss path needs this and not just the hit
|
|
51
|
+
* path. A start asks for `maxAgeMs` 0, because the task it just pressed is
|
|
52
|
+
* younger than any cached snapshot.
|
|
53
|
+
*/
|
|
54
|
+
function processSnapshot(maxAgeMs) {
|
|
55
|
+
if (maxAgeMs <= 0)
|
|
56
|
+
return readProcesses();
|
|
57
|
+
if (portSnapshot && Date.now() - portSnapshot.at <= maxAgeMs)
|
|
58
|
+
return Promise.resolve(portSnapshot.text);
|
|
59
|
+
portSnapshotInFlight ??= readProcesses().finally(() => {
|
|
60
|
+
portSnapshotInFlight = null;
|
|
61
|
+
});
|
|
62
|
+
return portSnapshotInFlight;
|
|
63
|
+
}
|
|
64
|
+
export async function workspacePort(workspaceId, maxAgeMs = 0) {
|
|
65
|
+
try {
|
|
66
|
+
return parseWorkspacePort(await processSnapshot(maxAgeMs), workspaceId);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Never reflect the error: `execFile` includes stdout, which is the process
|
|
70
|
+
// environment snapshot and may contain secrets.
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import http, {} from 'node:http';
|
|
3
|
+
import net from 'node:net';
|
|
4
|
+
import { tcpOpen } from "./ports.js";
|
|
5
|
+
import { processAlive } from "./processes.js";
|
|
6
|
+
export function bridgeMatches(record) {
|
|
7
|
+
if (!record.bridgeToken) {
|
|
8
|
+
// Compatibility for the first local prototype receipts. PID plus an open
|
|
9
|
+
// bridge is enough to avoid stealing one during this upgrade; every newly
|
|
10
|
+
// written receipt carries the unambiguous challenge below.
|
|
11
|
+
return processAlive(record.ownerPid) ? tcpOpen(record.bridgePort) : Promise.resolve(false);
|
|
12
|
+
}
|
|
13
|
+
return new Promise(resolve => {
|
|
14
|
+
let settled = false;
|
|
15
|
+
const finish = (matches) => {
|
|
16
|
+
if (settled)
|
|
17
|
+
return;
|
|
18
|
+
settled = true;
|
|
19
|
+
request.destroy();
|
|
20
|
+
resolve(matches);
|
|
21
|
+
};
|
|
22
|
+
const request = http.request({
|
|
23
|
+
host: '127.0.0.1',
|
|
24
|
+
port: record.bridgePort,
|
|
25
|
+
method: 'GET',
|
|
26
|
+
path: '/',
|
|
27
|
+
headers: { 'x-conductor-remote-bridge': record.bridgeToken }
|
|
28
|
+
}, response => {
|
|
29
|
+
response.resume();
|
|
30
|
+
finish(response.statusCode === 204);
|
|
31
|
+
});
|
|
32
|
+
request.setTimeout(500, () => finish(false));
|
|
33
|
+
request.once('error', () => finish(false));
|
|
34
|
+
request.end();
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function externalOrigin(req) {
|
|
38
|
+
const forwardedProto = String(req.headers['x-forwarded-proto'] ?? 'https')
|
|
39
|
+
.split(',')[0]
|
|
40
|
+
.trim();
|
|
41
|
+
const forwardedHost = String(req.headers['x-forwarded-host'] ?? req.headers.host ?? '')
|
|
42
|
+
.split(',')[0]
|
|
43
|
+
.trim();
|
|
44
|
+
return `${forwardedProto || 'https'}://${forwardedHost}`;
|
|
45
|
+
}
|
|
46
|
+
function localOrigin(targetPort) {
|
|
47
|
+
return `http://127.0.0.1:${targetPort}`;
|
|
48
|
+
}
|
|
49
|
+
function proxyHeaders(req, targetPort) {
|
|
50
|
+
const headers = { ...req.headers };
|
|
51
|
+
const publicHost = String(req.headers['x-forwarded-host'] ?? req.headers.host ?? '');
|
|
52
|
+
headers.host = `127.0.0.1:${targetPort}`;
|
|
53
|
+
if (publicHost)
|
|
54
|
+
headers['x-forwarded-host'] = publicHost;
|
|
55
|
+
headers['x-forwarded-proto'] = String(req.headers['x-forwarded-proto'] ?? 'https');
|
|
56
|
+
// Framework dev servers commonly apply the same allowlist to WebSocket Origin
|
|
57
|
+
// that they apply to Host. Present the local origin they were configured for.
|
|
58
|
+
if (headers.origin)
|
|
59
|
+
headers.origin = localOrigin(targetPort);
|
|
60
|
+
if (headers.referer) {
|
|
61
|
+
try {
|
|
62
|
+
const ref = new URL(headers.referer);
|
|
63
|
+
headers.referer = `${localOrigin(targetPort)}${ref.pathname}${ref.search}`;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// Preserve an unusual Referer rather than inventing one.
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return headers;
|
|
70
|
+
}
|
|
71
|
+
function rewriteLocation(location, req, targetPort) {
|
|
72
|
+
if (!location)
|
|
73
|
+
return undefined;
|
|
74
|
+
const local = new RegExp(`^https?://(?:127\\.0\\.0\\.1|localhost)(?::${targetPort})?`, 'i');
|
|
75
|
+
return location.replace(local, externalOrigin(req));
|
|
76
|
+
}
|
|
77
|
+
function writeUpgradeRequest(upstream, req, head, targetPort) {
|
|
78
|
+
const headers = proxyHeaders(req, targetPort);
|
|
79
|
+
const lines = [`${req.method ?? 'GET'} ${req.url ?? '/'} HTTP/${req.httpVersion}`];
|
|
80
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
81
|
+
if (value === undefined)
|
|
82
|
+
continue;
|
|
83
|
+
if (Array.isArray(value)) {
|
|
84
|
+
for (const item of value)
|
|
85
|
+
lines.push(`${name}: ${item}`);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
lines.push(`${name}: ${value}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
upstream.write(`${lines.join('\r\n')}\r\n\r\n`);
|
|
92
|
+
if (head.length)
|
|
93
|
+
upstream.write(head);
|
|
94
|
+
}
|
|
95
|
+
/** Loopback reverse proxy with raw WebSocket tunnelling for Vite-style HMR. */
|
|
96
|
+
export function createDevProxy(targetPort) {
|
|
97
|
+
return new Promise((resolve, reject) => {
|
|
98
|
+
const token = crypto.randomBytes(16).toString('hex');
|
|
99
|
+
const sockets = new Set();
|
|
100
|
+
const server = http.createServer((req, res) => {
|
|
101
|
+
if (req.headers['x-conductor-remote-bridge'] === token) {
|
|
102
|
+
res.writeHead(204);
|
|
103
|
+
return res.end();
|
|
104
|
+
}
|
|
105
|
+
const upstream = http.request({
|
|
106
|
+
host: '127.0.0.1',
|
|
107
|
+
port: targetPort,
|
|
108
|
+
method: req.method,
|
|
109
|
+
path: req.url,
|
|
110
|
+
headers: proxyHeaders(req, targetPort)
|
|
111
|
+
}, upstreamRes => {
|
|
112
|
+
const headers = { ...upstreamRes.headers };
|
|
113
|
+
const location = rewriteLocation(upstreamRes.headers.location, req, targetPort);
|
|
114
|
+
if (location)
|
|
115
|
+
headers.location = location;
|
|
116
|
+
res.writeHead(upstreamRes.statusCode ?? 502, headers);
|
|
117
|
+
upstreamRes.pipe(res);
|
|
118
|
+
});
|
|
119
|
+
upstream.once('error', () => {
|
|
120
|
+
if (!res.headersSent)
|
|
121
|
+
res.writeHead(502, { 'content-type': 'text/plain; charset=utf-8' });
|
|
122
|
+
res.end('Dev server is not reachable');
|
|
123
|
+
});
|
|
124
|
+
req.pipe(upstream);
|
|
125
|
+
});
|
|
126
|
+
server.on('connection', socket => {
|
|
127
|
+
sockets.add(socket);
|
|
128
|
+
socket.once('close', () => sockets.delete(socket));
|
|
129
|
+
});
|
|
130
|
+
server.on('upgrade', (req, socket, head) => {
|
|
131
|
+
const upstream = net.connect({ host: '127.0.0.1', port: targetPort }, () => {
|
|
132
|
+
writeUpgradeRequest(upstream, req, head, targetPort);
|
|
133
|
+
socket.pipe(upstream).pipe(socket);
|
|
134
|
+
});
|
|
135
|
+
sockets.add(upstream);
|
|
136
|
+
upstream.once('close', () => sockets.delete(upstream));
|
|
137
|
+
upstream.once('error', () => socket.destroy());
|
|
138
|
+
socket.once('error', () => upstream.destroy());
|
|
139
|
+
});
|
|
140
|
+
server.once('error', reject);
|
|
141
|
+
server.listen(0, '127.0.0.1', () => {
|
|
142
|
+
const address = server.address();
|
|
143
|
+
if (!address || typeof address === 'string') {
|
|
144
|
+
server.close();
|
|
145
|
+
return reject(new Error('could not allocate the dev-server bridge port'));
|
|
146
|
+
}
|
|
147
|
+
resolve({
|
|
148
|
+
port: address.port,
|
|
149
|
+
token,
|
|
150
|
+
close: () => new Promise(done => {
|
|
151
|
+
for (const socket of sockets)
|
|
152
|
+
socket.destroy();
|
|
153
|
+
server.close(() => done());
|
|
154
|
+
})
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
}
|