visual-remote 0.2.0 → 0.3.1
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 +49 -20
- package/apps/cli/dist/direct-exec-mcp.js +869 -0
- package/apps/cli/dist/index.js +760 -72
- package/apps/cli/dist/next-client.js +11 -0
- package/apps/cli/dist/next.js +5536 -0
- package/apps/cli/dist/vite.js +2738 -2255
- package/apps/cli/next-client.d.ts +1 -0
- package/apps/cli/next.d.ts +22 -0
- package/package.json +19 -4
- package/packages/overlay/dist/client.js +115 -7
- package/packages/overlay/dist/viewer.js +4 -4
package/apps/cli/dist/index.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { realpathSync } from "node:fs";
|
|
4
|
+
import { realpathSync as realpathSync2 } from "node:fs";
|
|
5
5
|
import { resolve as resolve8 } from "node:path";
|
|
6
6
|
import { pathToFileURL } from "node:url";
|
|
7
7
|
import { Command, InvalidArgumentError } from "commander";
|
|
8
8
|
|
|
9
|
+
// src/bridge.ts
|
|
10
|
+
import { createConnection } from "node:net";
|
|
11
|
+
|
|
9
12
|
// ../../packages/bridge-core/src/bridge/control-service.ts
|
|
10
13
|
var ControlServiceError = class extends Error {
|
|
11
14
|
statusCode;
|
|
@@ -161,6 +164,11 @@ function pairingTokensMatch(expected, candidate) {
|
|
|
161
164
|
const candidateBytes = Buffer.from(candidate);
|
|
162
165
|
return expectedBytes.length === candidateBytes.length && timingSafeEqual(expectedBytes, candidateBytes);
|
|
163
166
|
}
|
|
167
|
+
function createPairingUrl(baseUrl, token) {
|
|
168
|
+
const url = new URL(baseUrl);
|
|
169
|
+
url.hash = `visual-pair=${encodeURIComponent(token)}`;
|
|
170
|
+
return url.toString();
|
|
171
|
+
}
|
|
164
172
|
|
|
165
173
|
// ../../packages/bridge-core/src/runtime/ports.ts
|
|
166
174
|
import { createServer } from "node:net";
|
|
@@ -284,7 +292,7 @@ function originAllowed(request, allowedOrigins) {
|
|
|
284
292
|
}
|
|
285
293
|
}
|
|
286
294
|
function tokenAccess(token, controlToken, viewerSessions, now) {
|
|
287
|
-
if (token === void 0 || token.length === 0) return
|
|
295
|
+
if (token === void 0 || token.length === 0) return void 0;
|
|
288
296
|
if (pairingTokensMatch(controlToken, token)) return "control";
|
|
289
297
|
const expiresAt = viewerSessions.get(token);
|
|
290
298
|
if (expiresAt !== void 0) {
|
|
@@ -689,7 +697,12 @@ function createGatewayServer(options) {
|
|
|
689
697
|
const access3 = requestAccess(request, options.pairingToken, viewerSessions);
|
|
690
698
|
if (access3 === void 0) {
|
|
691
699
|
response.setHeader("www-authenticate", "Bearer");
|
|
692
|
-
writeApiError(
|
|
700
|
+
writeApiError(
|
|
701
|
+
response,
|
|
702
|
+
401,
|
|
703
|
+
"unauthorized",
|
|
704
|
+
"A valid control or viewer token is required"
|
|
705
|
+
);
|
|
693
706
|
return;
|
|
694
707
|
}
|
|
695
708
|
try {
|
|
@@ -982,7 +995,65 @@ function itemFiles(item) {
|
|
|
982
995
|
if (direct) files.push(direct);
|
|
983
996
|
return [...new Set(files)];
|
|
984
997
|
}
|
|
985
|
-
function
|
|
998
|
+
function stringArray(value) {
|
|
999
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === "string") ? value : void 0;
|
|
1000
|
+
}
|
|
1001
|
+
function formatArgv(argv) {
|
|
1002
|
+
return argv.map((argument) => /^[A-Za-z0-9_./:=@%+,-]+$/u.test(argument) ? argument : JSON.stringify(argument)).join(" ");
|
|
1003
|
+
}
|
|
1004
|
+
function isDirectExecItem(item) {
|
|
1005
|
+
return item.type === "mcp_tool_call" && item.server === "visual_remote_exec" && item.tool === "run_readonly";
|
|
1006
|
+
}
|
|
1007
|
+
function directExecSummary(item) {
|
|
1008
|
+
const arguments_ = asRecord(item.arguments);
|
|
1009
|
+
const commands = Array.isArray(arguments_?.commands) ? arguments_.commands : [];
|
|
1010
|
+
const summaries = commands.flatMap((candidate) => {
|
|
1011
|
+
const command = asRecord(candidate);
|
|
1012
|
+
const argv = stringArray(command?.argv);
|
|
1013
|
+
return argv === void 0 ? [] : [formatArgv(argv)];
|
|
1014
|
+
});
|
|
1015
|
+
return summaries.length === 0 ? void 0 : summaries.join(" \xB7 ");
|
|
1016
|
+
}
|
|
1017
|
+
function directExecResults(item, defaultCwd) {
|
|
1018
|
+
const result = asRecord(item.result);
|
|
1019
|
+
const structured = asRecord(result?.structured_content ?? result?.structuredContent);
|
|
1020
|
+
const results = Array.isArray(structured?.results) ? structured.results : [];
|
|
1021
|
+
return results.flatMap((candidate) => {
|
|
1022
|
+
const command = asRecord(candidate);
|
|
1023
|
+
const argv = stringArray(command?.argv);
|
|
1024
|
+
if (argv === void 0) return [];
|
|
1025
|
+
const exitCode = typeof command?.exitCode === "number" ? command.exitCode : void 0;
|
|
1026
|
+
const durationMs = typeof command?.durationMs === "number" ? command.durationMs : void 0;
|
|
1027
|
+
return [{
|
|
1028
|
+
command: formatArgv(argv),
|
|
1029
|
+
cwd: asText(command?.cwd) ?? defaultCwd,
|
|
1030
|
+
ok: exitCode === 0,
|
|
1031
|
+
...exitCode === void 0 ? {} : { exitCode },
|
|
1032
|
+
...durationMs === void 0 ? {} : { durationMs },
|
|
1033
|
+
...typeof command?.usedRtk === "boolean" ? { usedRtk: command.usedRtk } : {},
|
|
1034
|
+
...typeof command?.timedOut === "boolean" ? { timedOut: command.timedOut } : {},
|
|
1035
|
+
...typeof command?.truncated === "boolean" ? { truncated: command.truncated } : {}
|
|
1036
|
+
}];
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
function normalizedUsage(record) {
|
|
1040
|
+
const result = asRecord(record.result);
|
|
1041
|
+
const usage = asRecord(record.usage) ?? (result ? asRecord(result.usage) : void 0);
|
|
1042
|
+
if (usage === void 0) return void 0;
|
|
1043
|
+
const inputTokens = usage.input_tokens ?? usage.inputTokens;
|
|
1044
|
+
const outputTokens = usage.output_tokens ?? usage.outputTokens;
|
|
1045
|
+
const cachedInputTokens = usage.cached_input_tokens ?? usage.cachedInputTokens;
|
|
1046
|
+
if (typeof inputTokens !== "number" || typeof outputTokens !== "number") {
|
|
1047
|
+
return void 0;
|
|
1048
|
+
}
|
|
1049
|
+
return {
|
|
1050
|
+
type: "usage",
|
|
1051
|
+
inputTokens,
|
|
1052
|
+
outputTokens,
|
|
1053
|
+
...typeof cachedInputTokens === "number" ? { cachedInputTokens } : {}
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
function parseCodexJsonLine(line, defaultCwd = "") {
|
|
986
1057
|
const trimmed = line.trim();
|
|
987
1058
|
if (!trimmed) return [];
|
|
988
1059
|
let value;
|
|
@@ -1002,7 +1073,12 @@ function parseCodexJsonLine(line) {
|
|
|
1002
1073
|
if (type === "turn.completed") {
|
|
1003
1074
|
const result = asRecord(record.result);
|
|
1004
1075
|
const summary = asText(record.summary) ?? (result ? asText(result.summary) : void 0);
|
|
1005
|
-
|
|
1076
|
+
const usage = normalizedUsage(record);
|
|
1077
|
+
return [
|
|
1078
|
+
...events,
|
|
1079
|
+
...usage === void 0 ? [] : [usage],
|
|
1080
|
+
summary ? { type: "complete", summary } : { type: "complete" }
|
|
1081
|
+
];
|
|
1006
1082
|
}
|
|
1007
1083
|
if (type === "turn.failed" || type === "error") {
|
|
1008
1084
|
const error = asRecord(record.error);
|
|
@@ -1012,8 +1088,9 @@ function parseCodexJsonLine(line) {
|
|
|
1012
1088
|
const item = asRecord(record.item);
|
|
1013
1089
|
if (type === "item.started" && item) {
|
|
1014
1090
|
const itemType = asText(item.type) ?? "item";
|
|
1015
|
-
const
|
|
1016
|
-
const
|
|
1091
|
+
const directExec = isDirectExecItem(item);
|
|
1092
|
+
const summary = directExec ? directExecSummary(item) : asText(item.command) ?? asText(item.text);
|
|
1093
|
+
const start = summary ? { type: "tool_start", name: directExec ? "direct_exec" : itemType, summary } : { type: "tool_start", name: directExec ? "direct_exec" : itemType };
|
|
1017
1094
|
return [...events, start];
|
|
1018
1095
|
}
|
|
1019
1096
|
if (type === "item.completed" && item) {
|
|
@@ -1028,13 +1105,30 @@ function parseCodexJsonLine(line) {
|
|
|
1028
1105
|
events.push({
|
|
1029
1106
|
type: "command",
|
|
1030
1107
|
command,
|
|
1031
|
-
cwd: asText(item.cwd) ??
|
|
1108
|
+
cwd: asText(item.cwd) ?? defaultCwd
|
|
1032
1109
|
});
|
|
1033
1110
|
}
|
|
1034
1111
|
}
|
|
1112
|
+
const directResults = isDirectExecItem(item) ? directExecResults(item, defaultCwd) : [];
|
|
1113
|
+
for (const result of directResults) {
|
|
1114
|
+
events.push({
|
|
1115
|
+
type: "command",
|
|
1116
|
+
command: result.command,
|
|
1117
|
+
cwd: result.cwd,
|
|
1118
|
+
...result.exitCode === void 0 ? {} : { exitCode: result.exitCode },
|
|
1119
|
+
...result.durationMs === void 0 ? {} : { durationMs: result.durationMs },
|
|
1120
|
+
...result.usedRtk === void 0 ? {} : { usedRtk: result.usedRtk },
|
|
1121
|
+
...result.timedOut === void 0 ? {} : { timedOut: result.timedOut },
|
|
1122
|
+
...result.truncated === void 0 ? {} : { truncated: result.truncated }
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1035
1125
|
for (const path of itemFiles(item)) events.push({ type: "file_hint", path });
|
|
1036
1126
|
const exitCode = typeof item.exit_code === "number" ? item.exit_code : void 0;
|
|
1037
|
-
events.push({
|
|
1127
|
+
events.push({
|
|
1128
|
+
type: "tool_end",
|
|
1129
|
+
name: isDirectExecItem(item) ? "direct_exec" : itemType,
|
|
1130
|
+
ok: directResults.length > 0 ? directResults.every((result) => result.ok) : exitCode === void 0 || exitCode === 0
|
|
1131
|
+
});
|
|
1038
1132
|
return events;
|
|
1039
1133
|
}
|
|
1040
1134
|
const message = asText(record.message);
|
|
@@ -1043,7 +1137,10 @@ function parseCodexJsonLine(line) {
|
|
|
1043
1137
|
}
|
|
1044
1138
|
|
|
1045
1139
|
// ../../packages/bridge-core/src/agents/codex-adapter.ts
|
|
1046
|
-
import { spawn as spawn2 } from "node:child_process";
|
|
1140
|
+
import { execFile, spawn as spawn2 } from "node:child_process";
|
|
1141
|
+
import { existsSync } from "node:fs";
|
|
1142
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1143
|
+
import { promisify } from "node:util";
|
|
1047
1144
|
|
|
1048
1145
|
// ../../packages/bridge-core/src/agents/async-queue.ts
|
|
1049
1146
|
var AsyncQueue = class {
|
|
@@ -1242,6 +1339,7 @@ async function startManagedProcess(options) {
|
|
|
1242
1339
|
}
|
|
1243
1340
|
|
|
1244
1341
|
// ../../packages/bridge-core/src/agents/codex-adapter.ts
|
|
1342
|
+
var execFileAsync = promisify(execFile);
|
|
1245
1343
|
var INHERITED_ENVIRONMENT = [
|
|
1246
1344
|
"PATH",
|
|
1247
1345
|
"HOME",
|
|
@@ -1282,13 +1380,72 @@ function splitLines(chunk, previous, onLine) {
|
|
|
1282
1380
|
for (const line of lines) onLine(line);
|
|
1283
1381
|
return remainder;
|
|
1284
1382
|
}
|
|
1383
|
+
function defaultDirectExecMcpScript() {
|
|
1384
|
+
const candidates = [
|
|
1385
|
+
fileURLToPath2(new URL("./direct-exec-mcp.js", import.meta.url)),
|
|
1386
|
+
fileURLToPath2(
|
|
1387
|
+
new URL("../../../../apps/cli/dist/direct-exec-mcp.js", import.meta.url)
|
|
1388
|
+
)
|
|
1389
|
+
];
|
|
1390
|
+
return candidates.find((candidate) => existsSync(candidate));
|
|
1391
|
+
}
|
|
1285
1392
|
var CodexAdapter = class {
|
|
1286
1393
|
id = "codex";
|
|
1287
1394
|
#executable;
|
|
1288
1395
|
#killGraceMs;
|
|
1396
|
+
#rtkExecutable;
|
|
1397
|
+
#directExecMcpScript;
|
|
1398
|
+
#rtkVersion;
|
|
1289
1399
|
constructor(options = {}) {
|
|
1290
1400
|
this.#executable = options.executable ?? "codex";
|
|
1291
1401
|
this.#killGraceMs = options.killGraceMs ?? 2e3;
|
|
1402
|
+
this.#rtkExecutable = options.rtkExecutable ?? "rtk";
|
|
1403
|
+
this.#directExecMcpScript = options.directExecMcpScript === false ? void 0 : options.directExecMcpScript ?? defaultDirectExecMcpScript();
|
|
1404
|
+
}
|
|
1405
|
+
#probeRtk(environment) {
|
|
1406
|
+
if (this.#rtkExecutable === false) return Promise.resolve(void 0);
|
|
1407
|
+
this.#rtkVersion ??= execFileAsync(this.#rtkExecutable, ["--version"], {
|
|
1408
|
+
encoding: "utf8",
|
|
1409
|
+
env: environment,
|
|
1410
|
+
timeout: 1e3,
|
|
1411
|
+
windowsHide: true,
|
|
1412
|
+
maxBuffer: 16 * 1024
|
|
1413
|
+
}).then(({ stdout }) => stdout.trim().split(/\r?\n/, 1)[0] || void 0).catch(() => void 0);
|
|
1414
|
+
return this.#rtkVersion;
|
|
1415
|
+
}
|
|
1416
|
+
async #runtimePrompt(input, environment) {
|
|
1417
|
+
const commandGuidance = this.#rtkExecutable === false ? "" : await this.#probeRtk(environment).then((version) => version ? `RTK command proxy:
|
|
1418
|
+
- ${version} is installed and available in this runtime.
|
|
1419
|
+
- Prefix shell commands with RTK by default (for example: rtk git status, rtk rg <pattern>, rtk read <file>, rtk npm test).
|
|
1420
|
+
- Use the native command only when RTK has no suitable proxy or RTK execution fails. Do not spend time rediscovering or reinstalling RTK.` : `RTK command proxy:
|
|
1421
|
+
- RTK was not detected in this runtime. Use native repository commands directly and do not spend time searching for RTK.`);
|
|
1422
|
+
const directExecGuidance = this.#directExecMcpScript === void 0 ? "" : `Direct read-only command runner:
|
|
1423
|
+
- Use the visual_remote_exec run_readonly MCP tool (mcp__visual_remote_exec__run_readonly) for repository inspection by default: pwd, version checks, file listing/reading/search, and read-only Git status/diff/log/show.
|
|
1424
|
+
- Send argv arrays, batch independent reads in one tool call, and keep cwd at the registered workspace unless a known subdirectory is required.
|
|
1425
|
+
- The tool executes without a shell and applies RTK automatically when supported.
|
|
1426
|
+
- Use command_execution only for edits, tests/builds, or commands that genuinely require shell syntax. Do not retry a policy-rejected command through another shell unless the requested work requires that non-read-only operation.`;
|
|
1427
|
+
const guidance = [directExecGuidance, commandGuidance].filter(Boolean).join("\n\n");
|
|
1428
|
+
return guidance.length === 0 ? input.prompt : `${input.prompt.trimEnd()}
|
|
1429
|
+
|
|
1430
|
+
${guidance}
|
|
1431
|
+
`;
|
|
1432
|
+
}
|
|
1433
|
+
#directExecConfig(input) {
|
|
1434
|
+
if (this.#directExecMcpScript === void 0) return [];
|
|
1435
|
+
const serverArgs = [
|
|
1436
|
+
this.#directExecMcpScript,
|
|
1437
|
+
"--repo-root",
|
|
1438
|
+
input.repoRoot,
|
|
1439
|
+
"--workspace-root",
|
|
1440
|
+
input.workspaceRoot,
|
|
1441
|
+
...this.#rtkExecutable === false ? ["--no-rtk"] : ["--rtk", this.#rtkExecutable]
|
|
1442
|
+
];
|
|
1443
|
+
return [
|
|
1444
|
+
"-c",
|
|
1445
|
+
`mcp_servers.visual_remote_exec.command=${JSON.stringify(process.execPath)}`,
|
|
1446
|
+
"-c",
|
|
1447
|
+
`mcp_servers.visual_remote_exec.args=${JSON.stringify(serverArgs)}`
|
|
1448
|
+
];
|
|
1292
1449
|
}
|
|
1293
1450
|
async probe() {
|
|
1294
1451
|
return await new Promise((resolve9) => {
|
|
@@ -1324,7 +1481,8 @@ var CodexAdapter = class {
|
|
|
1324
1481
|
"-s",
|
|
1325
1482
|
"workspace-write",
|
|
1326
1483
|
"-C",
|
|
1327
|
-
input.
|
|
1484
|
+
input.workspaceRoot,
|
|
1485
|
+
...this.#directExecConfig(input),
|
|
1328
1486
|
"-"
|
|
1329
1487
|
];
|
|
1330
1488
|
yield* this.#execute(input, signal, args);
|
|
@@ -1341,7 +1499,8 @@ var CodexAdapter = class {
|
|
|
1341
1499
|
"-s",
|
|
1342
1500
|
"workspace-write",
|
|
1343
1501
|
"-C",
|
|
1344
|
-
input.
|
|
1502
|
+
input.workspaceRoot,
|
|
1503
|
+
...this.#directExecConfig(input),
|
|
1345
1504
|
"resume",
|
|
1346
1505
|
input.sessionId,
|
|
1347
1506
|
"-"
|
|
@@ -1350,9 +1509,11 @@ var CodexAdapter = class {
|
|
|
1350
1509
|
}
|
|
1351
1510
|
async *#execute(input, signal, args) {
|
|
1352
1511
|
const queue = new AsyncQueue();
|
|
1512
|
+
const environment = processEnv(input.environment);
|
|
1513
|
+
const prompt = await this.#runtimePrompt(input, environment);
|
|
1353
1514
|
const child = spawn2(this.#executable, args, {
|
|
1354
|
-
cwd: input.
|
|
1355
|
-
env:
|
|
1515
|
+
cwd: input.workspaceRoot,
|
|
1516
|
+
env: environment,
|
|
1356
1517
|
detached: process.platform !== "win32",
|
|
1357
1518
|
shell: false,
|
|
1358
1519
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -1383,7 +1544,7 @@ var CodexAdapter = class {
|
|
|
1383
1544
|
if (signal.aborted) abort();
|
|
1384
1545
|
child.stdout.on("data", (chunk) => {
|
|
1385
1546
|
stdoutRemainder = splitLines(chunk, stdoutRemainder, (line) => {
|
|
1386
|
-
for (const event of parseCodexJsonLine(line)) queue.push(event);
|
|
1547
|
+
for (const event of parseCodexJsonLine(line, input.workspaceRoot)) queue.push(event);
|
|
1387
1548
|
});
|
|
1388
1549
|
});
|
|
1389
1550
|
child.stderr.on("data", (chunk) => {
|
|
@@ -1398,7 +1559,9 @@ var CodexAdapter = class {
|
|
|
1398
1559
|
void (async () => {
|
|
1399
1560
|
await requestTermination();
|
|
1400
1561
|
if (stdoutRemainder.trim()) {
|
|
1401
|
-
for (const event of parseCodexJsonLine(stdoutRemainder))
|
|
1562
|
+
for (const event of parseCodexJsonLine(stdoutRemainder, input.workspaceRoot)) {
|
|
1563
|
+
queue.push(event);
|
|
1564
|
+
}
|
|
1402
1565
|
}
|
|
1403
1566
|
if (stderrRemainder.trim()) queue.push({ type: "warning", text: stderrRemainder });
|
|
1404
1567
|
if (timedOut) queue.end(new AgentTimeoutError());
|
|
@@ -1419,7 +1582,7 @@ var CodexAdapter = class {
|
|
|
1419
1582
|
child.stdin.on("error", (error) => {
|
|
1420
1583
|
if (error.code !== "EPIPE") queue.end(error);
|
|
1421
1584
|
});
|
|
1422
|
-
child.stdin.end(
|
|
1585
|
+
child.stdin.end(prompt);
|
|
1423
1586
|
try {
|
|
1424
1587
|
for await (const event of queue) yield event;
|
|
1425
1588
|
} finally {
|
|
@@ -1438,6 +1601,9 @@ var CodexAdapter = class {
|
|
|
1438
1601
|
}
|
|
1439
1602
|
};
|
|
1440
1603
|
|
|
1604
|
+
// ../../packages/bridge-core/src/agents/direct-exec.ts
|
|
1605
|
+
var DEFAULT_OUTPUT_BYTES = 64 * 1024;
|
|
1606
|
+
|
|
1441
1607
|
// ../../packages/bridge-core/src/config/loader.ts
|
|
1442
1608
|
import { readFile, realpath, stat as stat2 } from "node:fs/promises";
|
|
1443
1609
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
@@ -1867,6 +2033,20 @@ function isWithin(root, candidate) {
|
|
|
1867
2033
|
const path = relative2(root, candidate);
|
|
1868
2034
|
return path === "" || !path.startsWith(`..${sep2}`) && path !== ".." && !isAbsolute2(path);
|
|
1869
2035
|
}
|
|
2036
|
+
function rebaseWorkspacePatterns(repoRoot, workspaceRoot, patterns) {
|
|
2037
|
+
const repository = resolve2(repoRoot);
|
|
2038
|
+
const workspace = resolve2(workspaceRoot);
|
|
2039
|
+
if (!isWithin(repository, workspace)) {
|
|
2040
|
+
throw new PathSafetyError(
|
|
2041
|
+
"PATH_OUTSIDE_REPOSITORY",
|
|
2042
|
+
workspaceRoot,
|
|
2043
|
+
`Workspace is outside the repository: ${workspaceRoot}`
|
|
2044
|
+
);
|
|
2045
|
+
}
|
|
2046
|
+
const prefix = repositoryRelative(repository, workspace);
|
|
2047
|
+
if (!prefix) return [...patterns];
|
|
2048
|
+
return patterns.map((pattern) => normalizeSlashes(`${prefix}/${pattern}`));
|
|
2049
|
+
}
|
|
1870
2050
|
var PathPolicy = class {
|
|
1871
2051
|
repoRoot;
|
|
1872
2052
|
allowedPatterns;
|
|
@@ -2710,6 +2890,9 @@ var TERMINAL = /* @__PURE__ */ new Set(["accepted", "reverted", "failed", "cance
|
|
|
2710
2890
|
function isActiveTaskStatus(status) {
|
|
2711
2891
|
return status !== "queued" && status !== "review" && !TERMINAL.has(status);
|
|
2712
2892
|
}
|
|
2893
|
+
function isWorkingTaskStatus(status) {
|
|
2894
|
+
return status === "queued" || isActiveTaskStatus(status);
|
|
2895
|
+
}
|
|
2713
2896
|
function isTerminalTaskStatus(status) {
|
|
2714
2897
|
return TERMINAL.has(status);
|
|
2715
2898
|
}
|
|
@@ -2764,14 +2947,18 @@ Follow-up context:
|
|
|
2764
2947
|
- Previous request: ${options.parent.requestText}
|
|
2765
2948
|
- Previous diff summary: ${options.parent.diffSummary || "No file changes"}
|
|
2766
2949
|
` : "";
|
|
2767
|
-
return `
|
|
2768
|
-
|
|
2950
|
+
return `Target service context (authoritative):
|
|
2951
|
+
- Repository worktree: ${options.repoRoot}
|
|
2952
|
+
- Workspace: ${options.workspaceRoot}
|
|
2953
|
+
- Browser URL: ${options.context.page.url}
|
|
2954
|
+
${options.upstreamUrl ? `- Local upstream URL: ${options.upstreamUrl}
|
|
2955
|
+
` : ""}
|
|
2956
|
+
Treat the workspace above as the already-resolved service directory and the repository worktree as its safety boundary. Run project commands from the workspace; do not search parent directories or run directory-discovery commands to locate the project again.
|
|
2769
2957
|
|
|
2770
2958
|
User request:
|
|
2771
2959
|
${options.context.request.text}
|
|
2772
2960
|
|
|
2773
2961
|
Selected UI context:
|
|
2774
|
-
- URL: ${options.context.page.url}
|
|
2775
2962
|
- Route: ${options.context.page.pathname}
|
|
2776
2963
|
- Selection mode: ${options.context.selection.mode}
|
|
2777
2964
|
${targets || "- No concrete target; use the page context and repository search."}
|
|
@@ -3246,6 +3433,7 @@ function repositoryGuardMessage(result) {
|
|
|
3246
3433
|
var TaskService = class {
|
|
3247
3434
|
#projectId;
|
|
3248
3435
|
#workspaceRoot;
|
|
3436
|
+
#upstreamUrl;
|
|
3249
3437
|
#adapter;
|
|
3250
3438
|
#store;
|
|
3251
3439
|
#git;
|
|
@@ -3269,6 +3457,7 @@ var TaskService = class {
|
|
|
3269
3457
|
constructor(options) {
|
|
3270
3458
|
this.#projectId = options.projectId;
|
|
3271
3459
|
this.#workspaceRoot = resolve6(options.workspaceRoot ?? options.git.repoRoot);
|
|
3460
|
+
this.#upstreamUrl = options.upstreamUrl;
|
|
3272
3461
|
if (!isWithin3(options.git.repoRoot, this.#workspaceRoot)) {
|
|
3273
3462
|
throw new TaskServiceError(
|
|
3274
3463
|
"WORKSPACE_OUTSIDE_REPOSITORY",
|
|
@@ -3655,6 +3844,7 @@ var TaskService = class {
|
|
|
3655
3844
|
const prompt = buildAgentPrompt({
|
|
3656
3845
|
repoRoot: this.#git.repoRoot,
|
|
3657
3846
|
workspaceRoot: this.#workspaceRoot,
|
|
3847
|
+
...this.#upstreamUrl === void 0 ? {} : { upstreamUrl: this.#upstreamUrl },
|
|
3658
3848
|
contextBundlePath: contextPath,
|
|
3659
3849
|
context,
|
|
3660
3850
|
allowedPatterns: this.#git.pathPolicy.allowedPatterns,
|
|
@@ -3887,6 +4077,18 @@ var TaskService = class {
|
|
|
3887
4077
|
};
|
|
3888
4078
|
|
|
3889
4079
|
// ../../packages/bridge-core/src/verification/browser-sessions.ts
|
|
4080
|
+
function errorSignature(event) {
|
|
4081
|
+
return `${event.level}\0${event.message}`;
|
|
4082
|
+
}
|
|
4083
|
+
function samePage(left, right) {
|
|
4084
|
+
try {
|
|
4085
|
+
const leftUrl = new URL(left);
|
|
4086
|
+
const rightUrl = new URL(right);
|
|
4087
|
+
return leftUrl.origin === rightUrl.origin && leftUrl.pathname === rightUrl.pathname && leftUrl.search === rightUrl.search;
|
|
4088
|
+
} catch {
|
|
4089
|
+
return left === right;
|
|
4090
|
+
}
|
|
4091
|
+
}
|
|
3890
4092
|
var BrowserSessionManager = class {
|
|
3891
4093
|
#sessions = /* @__PURE__ */ new Map();
|
|
3892
4094
|
#maxConsoleEvents;
|
|
@@ -3962,7 +4164,13 @@ var BrowserSessionManager = class {
|
|
|
3962
4164
|
return {
|
|
3963
4165
|
browserSessionId: id,
|
|
3964
4166
|
renderRevision: session.renderRevision,
|
|
3965
|
-
startedAt: now.toISOString()
|
|
4167
|
+
startedAt: now.toISOString(),
|
|
4168
|
+
url: session.url,
|
|
4169
|
+
knownErrorSignatures: [
|
|
4170
|
+
...new Set(
|
|
4171
|
+
session.consoleEvents.filter((event) => event.level === "error" || event.level === "unhandled").map(errorSignature)
|
|
4172
|
+
)
|
|
4173
|
+
]
|
|
3966
4174
|
};
|
|
3967
4175
|
}
|
|
3968
4176
|
verify(baseline, options = {}) {
|
|
@@ -3975,10 +4183,16 @@ var BrowserSessionManager = class {
|
|
|
3975
4183
|
summary: "Origin browser session is disconnected."
|
|
3976
4184
|
};
|
|
3977
4185
|
}
|
|
3978
|
-
const
|
|
3979
|
-
|
|
3980
|
-
|
|
4186
|
+
const knownErrors = new Set(baseline.knownErrorSignatures);
|
|
4187
|
+
const newErrors = [
|
|
4188
|
+
...new Map(
|
|
4189
|
+
session.consoleEvents.filter(
|
|
4190
|
+
(event) => event.createdAt > baseline.startedAt && (event.level === "error" || event.level === "unhandled") && !knownErrors.has(errorSignature(event))
|
|
4191
|
+
).map((event) => [errorSignature(event), event])
|
|
4192
|
+
).values()
|
|
4193
|
+
];
|
|
3981
4194
|
const renderChanged = session.renderRevision > baseline.renderRevision;
|
|
4195
|
+
const pageUnchanged = samePage(baseline.url, session.url);
|
|
3982
4196
|
const targetResult = options.taskId === void 0 ? void 0 : session.targetResults.filter(
|
|
3983
4197
|
(result) => result.taskId === options.taskId && result.createdAt >= baseline.startedAt
|
|
3984
4198
|
).at(-1);
|
|
@@ -3991,6 +4205,15 @@ var BrowserSessionManager = class {
|
|
|
3991
4205
|
summary: `${newErrors.length} new browser error${newErrors.length === 1 ? "" : "s"} detected.`
|
|
3992
4206
|
};
|
|
3993
4207
|
}
|
|
4208
|
+
if (!pageUnchanged) {
|
|
4209
|
+
return {
|
|
4210
|
+
status: "partial",
|
|
4211
|
+
renderChanged,
|
|
4212
|
+
newErrors: [],
|
|
4213
|
+
...targetResult ? { targetResult } : {},
|
|
4214
|
+
summary: "Origin browser navigated to a different page during verification."
|
|
4215
|
+
};
|
|
4216
|
+
}
|
|
3994
4217
|
if (options.targetEvidenceRequired) {
|
|
3995
4218
|
const targetChanged = targetResult?.state === "found-and-changed" && targetResult.targetCount > 0 && targetResult.foundCount === targetResult.targetCount && targetResult.changedCount > 0 && targetResult.renderRevision > baseline.renderRevision;
|
|
3996
4219
|
if (targetChanged) {
|
|
@@ -4579,7 +4802,7 @@ ${output2}` : ""}`
|
|
|
4579
4802
|
status: "ok",
|
|
4580
4803
|
bridge: "online",
|
|
4581
4804
|
projectId: options.project.id,
|
|
4582
|
-
activeTask: taskService.list().find((task) =>
|
|
4805
|
+
activeTask: taskService.list().find((task) => isWorkingTaskStatus(task.status))?.id ?? null
|
|
4583
4806
|
}),
|
|
4584
4807
|
project: () => ({
|
|
4585
4808
|
id: options.project.id,
|
|
@@ -4630,14 +4853,23 @@ async function createDefaultControlService(context, environment = process.env) {
|
|
|
4630
4853
|
);
|
|
4631
4854
|
}
|
|
4632
4855
|
const git = await GitTransactionManager.open(context.repoRoot, {
|
|
4633
|
-
allowed:
|
|
4634
|
-
|
|
4856
|
+
allowed: rebaseWorkspacePatterns(
|
|
4857
|
+
context.repoRoot,
|
|
4858
|
+
context.workspaceRoot,
|
|
4859
|
+
loaded.config.paths.allowed
|
|
4860
|
+
),
|
|
4861
|
+
denied: rebaseWorkspacePatterns(
|
|
4862
|
+
context.repoRoot,
|
|
4863
|
+
context.workspaceRoot,
|
|
4864
|
+
loaded.config.paths.denied
|
|
4865
|
+
)
|
|
4635
4866
|
});
|
|
4636
4867
|
const storagePaths = await resolveStoragePaths(context.repoRoot, environment);
|
|
4637
4868
|
const store = new SqliteTaskStore(storagePaths.databasePath);
|
|
4638
4869
|
const taskService = new TaskService({
|
|
4639
4870
|
projectId: context.projectId,
|
|
4640
4871
|
workspaceRoot: context.workspaceRoot,
|
|
4872
|
+
upstreamUrl: context.upstreamUrl,
|
|
4641
4873
|
adapter: new CodexAdapter(),
|
|
4642
4874
|
store,
|
|
4643
4875
|
git,
|
|
@@ -4646,7 +4878,7 @@ async function createDefaultControlService(context, environment = process.env) {
|
|
|
4646
4878
|
resumeMode: loaded.config.agent.resumeMode,
|
|
4647
4879
|
environment: {}
|
|
4648
4880
|
});
|
|
4649
|
-
|
|
4881
|
+
const controlService = createTaskControlService({
|
|
4650
4882
|
taskService,
|
|
4651
4883
|
hmrWaitMs: loaded.config.verification.hmrWaitMs,
|
|
4652
4884
|
verificationCommands: loaded.config.verification.commands,
|
|
@@ -4658,10 +4890,32 @@ async function createDefaultControlService(context, environment = process.env) {
|
|
|
4658
4890
|
upstreamUrl: context.upstreamUrl
|
|
4659
4891
|
}
|
|
4660
4892
|
});
|
|
4893
|
+
const reportRuntimeState = () => {
|
|
4894
|
+
const activeTask = taskService.list().find((task) => isWorkingTaskStatus(task.status));
|
|
4895
|
+
context.onRuntimeState?.({
|
|
4896
|
+
status: activeTask === void 0 ? "idle" : "working",
|
|
4897
|
+
...activeTask === void 0 ? {} : { activeTaskId: activeTask.id }
|
|
4898
|
+
});
|
|
4899
|
+
};
|
|
4900
|
+
const unsubscribeRuntime = taskService.subscribe(reportRuntimeState);
|
|
4901
|
+
reportRuntimeState();
|
|
4902
|
+
return {
|
|
4903
|
+
...controlService,
|
|
4904
|
+
close: async () => {
|
|
4905
|
+
unsubscribeRuntime();
|
|
4906
|
+
await controlService.close?.();
|
|
4907
|
+
}
|
|
4908
|
+
};
|
|
4661
4909
|
}
|
|
4662
4910
|
|
|
4663
4911
|
// ../../packages/bridge-core/src/runtime/registry.ts
|
|
4664
4912
|
import { createHash as createHash3, randomUUID as randomUUID2 } from "node:crypto";
|
|
4913
|
+
import {
|
|
4914
|
+
readFileSync,
|
|
4915
|
+
realpathSync,
|
|
4916
|
+
rmSync,
|
|
4917
|
+
unlinkSync
|
|
4918
|
+
} from "node:fs";
|
|
4665
4919
|
import {
|
|
4666
4920
|
mkdir as mkdir2,
|
|
4667
4921
|
link,
|
|
@@ -4702,6 +4956,11 @@ async function repositoryKey(repositoryRoot) {
|
|
|
4702
4956
|
const canonicalRoot = await realpath8(repositoryRoot);
|
|
4703
4957
|
return createHash3("sha256").update(canonicalRoot).digest("hex");
|
|
4704
4958
|
}
|
|
4959
|
+
function runtimeDirectoryForSync(repositoryRoot, options) {
|
|
4960
|
+
const canonicalRoot = realpathSync(repositoryRoot);
|
|
4961
|
+
const repoKey = createHash3("sha256").update(canonicalRoot).digest("hex");
|
|
4962
|
+
return join3(runtimeRoot(options), repoKey);
|
|
4963
|
+
}
|
|
4705
4964
|
async function runtimeDirectoryFor(repositoryRoot, options = {}) {
|
|
4706
4965
|
return join3(runtimeRoot(options), await repositoryKey(repositoryRoot));
|
|
4707
4966
|
}
|
|
@@ -4740,6 +4999,13 @@ async function readJson(path) {
|
|
|
4740
4999
|
return void 0;
|
|
4741
5000
|
}
|
|
4742
5001
|
}
|
|
5002
|
+
function readJsonSync(path) {
|
|
5003
|
+
try {
|
|
5004
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
5005
|
+
} catch {
|
|
5006
|
+
return void 0;
|
|
5007
|
+
}
|
|
5008
|
+
}
|
|
4743
5009
|
async function readInstance(repositoryRoot, options = {}) {
|
|
4744
5010
|
const directory = await runtimeDirectoryFor(repositoryRoot, options);
|
|
4745
5011
|
const value = await readJson(join3(directory, "instance.json"));
|
|
@@ -4757,6 +5023,21 @@ async function writeInstance(repositoryRoot, instance, options = {}) {
|
|
|
4757
5023
|
});
|
|
4758
5024
|
await rename(temporaryPath, path);
|
|
4759
5025
|
}
|
|
5026
|
+
async function updateInstance(repositoryRoot, expectedPid, update, options = {}) {
|
|
5027
|
+
const current = await readInstance(repositoryRoot, options);
|
|
5028
|
+
if (current === void 0 || current.pid !== expectedPid) return void 0;
|
|
5029
|
+
const next = {
|
|
5030
|
+
...current,
|
|
5031
|
+
...update.status === void 0 ? {} : { status: update.status }
|
|
5032
|
+
};
|
|
5033
|
+
if (update.activeTaskId === null) {
|
|
5034
|
+
delete next.activeTaskId;
|
|
5035
|
+
} else if (update.activeTaskId !== void 0) {
|
|
5036
|
+
next.activeTaskId = update.activeTaskId;
|
|
5037
|
+
}
|
|
5038
|
+
await writeInstance(repositoryRoot, next, options);
|
|
5039
|
+
return next;
|
|
5040
|
+
}
|
|
4760
5041
|
async function removeInstance(repositoryRoot, expectedPid, options = {}) {
|
|
4761
5042
|
const directory = await runtimeDirectoryFor(repositoryRoot, options);
|
|
4762
5043
|
const path = join3(directory, "instance.json");
|
|
@@ -4768,6 +5049,17 @@ async function removeInstance(repositoryRoot, expectedPid, options = {}) {
|
|
|
4768
5049
|
}
|
|
4769
5050
|
await rm3(path, { force: true });
|
|
4770
5051
|
}
|
|
5052
|
+
function removeInstanceSync(repositoryRoot, expectedPid, options = {}) {
|
|
5053
|
+
const directory = runtimeDirectoryForSync(repositoryRoot, options);
|
|
5054
|
+
const path = join3(directory, "instance.json");
|
|
5055
|
+
if (expectedPid !== void 0) {
|
|
5056
|
+
const current = readJsonSync(path);
|
|
5057
|
+
if (!isBridgeInstanceRecord(current) || current.pid !== expectedPid) {
|
|
5058
|
+
return;
|
|
5059
|
+
}
|
|
5060
|
+
}
|
|
5061
|
+
rmSync(path, { force: true });
|
|
5062
|
+
}
|
|
4771
5063
|
async function acquireWorktreeLock(repositoryRoot, options = {}) {
|
|
4772
5064
|
const repoRoot = await realpath8(repositoryRoot);
|
|
4773
5065
|
const repoKey = await repositoryKey(repoRoot);
|
|
@@ -4799,23 +5091,43 @@ async function acquireWorktreeLock(repositoryRoot, options = {}) {
|
|
|
4799
5091
|
await rm3(candidatePath, { force: true });
|
|
4800
5092
|
}
|
|
4801
5093
|
let released = false;
|
|
5094
|
+
let releasePromise;
|
|
4802
5095
|
return {
|
|
4803
5096
|
repoKey,
|
|
4804
5097
|
runtimeDirectory,
|
|
4805
5098
|
lockPath,
|
|
4806
|
-
|
|
5099
|
+
release() {
|
|
4807
5100
|
if (released) {
|
|
5101
|
+
return Promise.resolve();
|
|
5102
|
+
}
|
|
5103
|
+
releasePromise ??= (async () => {
|
|
5104
|
+
const current = await readJson(lockPath);
|
|
5105
|
+
if (isLockRecord(current) && current.ownerId === ownerId) {
|
|
5106
|
+
await unlink(lockPath).catch((error) => {
|
|
5107
|
+
if (error.code !== "ENOENT") {
|
|
5108
|
+
throw error;
|
|
5109
|
+
}
|
|
5110
|
+
});
|
|
5111
|
+
}
|
|
5112
|
+
released = true;
|
|
5113
|
+
})();
|
|
5114
|
+
return releasePromise;
|
|
5115
|
+
},
|
|
5116
|
+
releaseSync() {
|
|
5117
|
+
if (released || releasePromise !== void 0) {
|
|
4808
5118
|
return;
|
|
4809
5119
|
}
|
|
4810
|
-
|
|
4811
|
-
const current = await readJson(lockPath);
|
|
5120
|
+
const current = readJsonSync(lockPath);
|
|
4812
5121
|
if (isLockRecord(current) && current.ownerId === ownerId) {
|
|
4813
|
-
|
|
4814
|
-
|
|
5122
|
+
try {
|
|
5123
|
+
unlinkSync(lockPath);
|
|
5124
|
+
} catch (error) {
|
|
5125
|
+
if (typeof error !== "object" || error === null || !("code" in error) || error.code !== "ENOENT") {
|
|
4815
5126
|
throw error;
|
|
4816
5127
|
}
|
|
4817
|
-
}
|
|
5128
|
+
}
|
|
4818
5129
|
}
|
|
5130
|
+
released = true;
|
|
4819
5131
|
}
|
|
4820
5132
|
};
|
|
4821
5133
|
} catch (error) {
|
|
@@ -4835,10 +5147,10 @@ async function acquireWorktreeLock(repositoryRoot, options = {}) {
|
|
|
4835
5147
|
}
|
|
4836
5148
|
|
|
4837
5149
|
// ../../packages/bridge-core/src/runtime/repository.ts
|
|
4838
|
-
import { execFile } from "node:child_process";
|
|
5150
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
4839
5151
|
import { realpath as realpath9 } from "node:fs/promises";
|
|
4840
|
-
import { promisify } from "node:util";
|
|
4841
|
-
var
|
|
5152
|
+
import { promisify as promisify2 } from "node:util";
|
|
5153
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
4842
5154
|
var GitWorktreeNotFoundError = class extends Error {
|
|
4843
5155
|
constructor(cwd, options = {}) {
|
|
4844
5156
|
super(
|
|
@@ -4850,7 +5162,7 @@ var GitWorktreeNotFoundError = class extends Error {
|
|
|
4850
5162
|
};
|
|
4851
5163
|
async function discoverGitWorktreeRoot(cwd = process.cwd()) {
|
|
4852
5164
|
try {
|
|
4853
|
-
const { stdout } = await
|
|
5165
|
+
const { stdout } = await execFileAsync2(
|
|
4854
5166
|
"git",
|
|
4855
5167
|
["-C", cwd, "rev-parse", "--show-toplevel"],
|
|
4856
5168
|
{
|
|
@@ -4869,6 +5181,91 @@ async function discoverGitWorktreeRoot(cwd = process.cwd()) {
|
|
|
4869
5181
|
}
|
|
4870
5182
|
|
|
4871
5183
|
// src/bridge.ts
|
|
5184
|
+
var DEFAULT_UPSTREAM_MONITOR_INTERVAL_MS = 1e3;
|
|
5185
|
+
var DEFAULT_UPSTREAM_CONNECT_TIMEOUT_MS = 500;
|
|
5186
|
+
var DEFAULT_UPSTREAM_FAILURE_GRACE_MS = 5e3;
|
|
5187
|
+
function positiveMilliseconds(value, fallback) {
|
|
5188
|
+
return value === void 0 || !Number.isFinite(value) || value <= 0 ? fallback : Math.max(1, Math.floor(value));
|
|
5189
|
+
}
|
|
5190
|
+
async function probeUpstream(upstreamUrl, timeoutMs) {
|
|
5191
|
+
const upstream = new URL(upstreamUrl);
|
|
5192
|
+
const hostname = upstream.hostname.startsWith("[") ? upstream.hostname.slice(1, -1) : upstream.hostname;
|
|
5193
|
+
const port = Number(
|
|
5194
|
+
upstream.port || (upstream.protocol === "https:" ? 443 : 80)
|
|
5195
|
+
);
|
|
5196
|
+
return await new Promise((resolve9) => {
|
|
5197
|
+
let settled = false;
|
|
5198
|
+
let timeout;
|
|
5199
|
+
const socket = createConnection({ host: hostname, port });
|
|
5200
|
+
socket.unref();
|
|
5201
|
+
const finish = (reachable) => {
|
|
5202
|
+
if (settled) return;
|
|
5203
|
+
settled = true;
|
|
5204
|
+
if (timeout !== void 0) clearTimeout(timeout);
|
|
5205
|
+
socket.destroy();
|
|
5206
|
+
resolve9(reachable);
|
|
5207
|
+
};
|
|
5208
|
+
socket.once("connect", () => finish(true));
|
|
5209
|
+
socket.once("error", () => finish(false));
|
|
5210
|
+
timeout = setTimeout(() => finish(false), timeoutMs);
|
|
5211
|
+
timeout.unref();
|
|
5212
|
+
});
|
|
5213
|
+
}
|
|
5214
|
+
function monitorUpstream(bridge, options) {
|
|
5215
|
+
const intervalMs = positiveMilliseconds(
|
|
5216
|
+
options.intervalMs,
|
|
5217
|
+
DEFAULT_UPSTREAM_MONITOR_INTERVAL_MS
|
|
5218
|
+
);
|
|
5219
|
+
const connectTimeoutMs = positiveMilliseconds(
|
|
5220
|
+
options.connectTimeoutMs,
|
|
5221
|
+
DEFAULT_UPSTREAM_CONNECT_TIMEOUT_MS
|
|
5222
|
+
);
|
|
5223
|
+
const initialTimeoutMs = positiveMilliseconds(
|
|
5224
|
+
options.initialTimeoutMs,
|
|
5225
|
+
6e4
|
|
5226
|
+
);
|
|
5227
|
+
const failureGraceMs = positiveMilliseconds(
|
|
5228
|
+
options.failureGraceMs,
|
|
5229
|
+
DEFAULT_UPSTREAM_FAILURE_GRACE_MS
|
|
5230
|
+
);
|
|
5231
|
+
let connected = false;
|
|
5232
|
+
let unavailableSince;
|
|
5233
|
+
let stopped = false;
|
|
5234
|
+
let timer;
|
|
5235
|
+
const stop = () => {
|
|
5236
|
+
stopped = true;
|
|
5237
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
5238
|
+
};
|
|
5239
|
+
const schedule = () => {
|
|
5240
|
+
if (stopped) return;
|
|
5241
|
+
timer = setTimeout(() => {
|
|
5242
|
+
void check().catch(() => void 0);
|
|
5243
|
+
}, intervalMs);
|
|
5244
|
+
timer.unref();
|
|
5245
|
+
};
|
|
5246
|
+
const check = async () => {
|
|
5247
|
+
if (stopped) return;
|
|
5248
|
+
const reachable = await probeUpstream(bridge.upstreamUrl, connectTimeoutMs);
|
|
5249
|
+
if (stopped) return;
|
|
5250
|
+
const now = Date.now();
|
|
5251
|
+
if (reachable) {
|
|
5252
|
+
connected = true;
|
|
5253
|
+
unavailableSince = void 0;
|
|
5254
|
+
schedule();
|
|
5255
|
+
return;
|
|
5256
|
+
}
|
|
5257
|
+
unavailableSince ??= now;
|
|
5258
|
+
const timeoutMs = connected ? failureGraceMs : initialTimeoutMs;
|
|
5259
|
+
if (now - unavailableSince >= timeoutMs) {
|
|
5260
|
+
stop();
|
|
5261
|
+
await bridge.close();
|
|
5262
|
+
return;
|
|
5263
|
+
}
|
|
5264
|
+
schedule();
|
|
5265
|
+
};
|
|
5266
|
+
void bridge.closed.then(stop);
|
|
5267
|
+
void check().catch(() => void 0);
|
|
5268
|
+
}
|
|
4872
5269
|
function normalizeUpstream(value) {
|
|
4873
5270
|
const url = new URL(value);
|
|
4874
5271
|
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
@@ -4905,6 +5302,26 @@ async function startBridgeCore(options, dependencies) {
|
|
|
4905
5302
|
let gateway;
|
|
4906
5303
|
let controlService;
|
|
4907
5304
|
let registryWritten = false;
|
|
5305
|
+
let registryUpdate = Promise.resolve();
|
|
5306
|
+
let latestRuntimeState = { status: "idle" };
|
|
5307
|
+
const updateRuntimeState = (status, activeTaskId) => {
|
|
5308
|
+
latestRuntimeState = {
|
|
5309
|
+
status,
|
|
5310
|
+
...activeTaskId === void 0 ? {} : { activeTaskId }
|
|
5311
|
+
};
|
|
5312
|
+
if (!registryWritten) return;
|
|
5313
|
+
registryUpdate = registryUpdate.then(async () => {
|
|
5314
|
+
await updateInstance(
|
|
5315
|
+
loadedConfig.repoRoot,
|
|
5316
|
+
process.pid,
|
|
5317
|
+
{
|
|
5318
|
+
status,
|
|
5319
|
+
activeTaskId: activeTaskId ?? null
|
|
5320
|
+
},
|
|
5321
|
+
{ environment }
|
|
5322
|
+
);
|
|
5323
|
+
}).catch(() => void 0);
|
|
5324
|
+
};
|
|
4908
5325
|
try {
|
|
4909
5326
|
lock = options.lock ?? await acquireWorktreeLock(loadedConfig.repoRoot, { environment });
|
|
4910
5327
|
const host = options.host ?? loadedConfig.config.gateway.host;
|
|
@@ -4918,7 +5335,10 @@ async function startBridgeCore(options, dependencies) {
|
|
|
4918
5335
|
repoRoot: loadedConfig.repoRoot,
|
|
4919
5336
|
configRoot: loadedConfig.configRoot,
|
|
4920
5337
|
workspaceRoot: loadedConfig.workspaceRoot,
|
|
4921
|
-
upstreamUrl: options.upstreamUrl
|
|
5338
|
+
upstreamUrl: options.upstreamUrl,
|
|
5339
|
+
onRuntimeState: (state) => {
|
|
5340
|
+
updateRuntimeState(state.status, state.activeTaskId);
|
|
5341
|
+
}
|
|
4922
5342
|
};
|
|
4923
5343
|
controlService = await resolveControlService(dependencies, controlContext);
|
|
4924
5344
|
const allowedOrigins = new Set(loadedConfig.config.security.allowedOrigins);
|
|
@@ -4933,20 +5353,33 @@ async function startBridgeCore(options, dependencies) {
|
|
|
4933
5353
|
allowedOrigins: [...allowedOrigins]
|
|
4934
5354
|
});
|
|
4935
5355
|
const address = await gateway.start();
|
|
4936
|
-
const openUrl = publicUrl ?? address.url;
|
|
5356
|
+
const openUrl = createPairingUrl(publicUrl ?? address.url, token);
|
|
4937
5357
|
const instance = {
|
|
4938
5358
|
projectId: loadedConfig.config.project.id,
|
|
4939
5359
|
repoRoot: loadedConfig.repoRoot,
|
|
4940
5360
|
pid: process.pid,
|
|
4941
5361
|
gatewayUrl: address.url,
|
|
4942
5362
|
upstreamUrl: options.upstreamUrl,
|
|
4943
|
-
status:
|
|
5363
|
+
status: latestRuntimeState.status,
|
|
4944
5364
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5365
|
+
...latestRuntimeState.activeTaskId === void 0 ? {} : { activeTaskId: latestRuntimeState.activeTaskId },
|
|
4945
5366
|
...publicUrl === void 0 ? {} : { publicUrl }
|
|
4946
5367
|
};
|
|
4947
5368
|
await writeInstance(loadedConfig.repoRoot, instance, { environment });
|
|
4948
5369
|
registryWritten = true;
|
|
4949
|
-
|
|
5370
|
+
const emergencyExitCleanup = () => {
|
|
5371
|
+
try {
|
|
5372
|
+
removeInstanceSync(loadedConfig.repoRoot, process.pid, { environment });
|
|
5373
|
+
lock?.releaseSync();
|
|
5374
|
+
} catch {
|
|
5375
|
+
}
|
|
5376
|
+
};
|
|
5377
|
+
process.once("exit", emergencyExitCleanup);
|
|
5378
|
+
let resolveClosed = () => void 0;
|
|
5379
|
+
const closed = new Promise((resolve9) => {
|
|
5380
|
+
resolveClosed = resolve9;
|
|
5381
|
+
});
|
|
5382
|
+
let closePromise;
|
|
4950
5383
|
return {
|
|
4951
5384
|
mode: options.mode,
|
|
4952
5385
|
projectId: loadedConfig.config.project.id,
|
|
@@ -4958,18 +5391,33 @@ async function startBridgeCore(options, dependencies) {
|
|
|
4958
5391
|
openUrl,
|
|
4959
5392
|
gateway,
|
|
4960
5393
|
...options.managedProcess === void 0 ? {} : { managedProcess: options.managedProcess },
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
5394
|
+
closed,
|
|
5395
|
+
close() {
|
|
5396
|
+
closePromise ??= (async () => {
|
|
5397
|
+
process.off("exit", emergencyExitCleanup);
|
|
5398
|
+
try {
|
|
5399
|
+
await registryUpdate;
|
|
5400
|
+
await updateInstance(
|
|
5401
|
+
loadedConfig.repoRoot,
|
|
5402
|
+
process.pid,
|
|
5403
|
+
{ status: "stopping", activeTaskId: null },
|
|
5404
|
+
{ environment }
|
|
5405
|
+
);
|
|
5406
|
+
await Promise.allSettled([
|
|
5407
|
+
gateway?.close(),
|
|
5408
|
+
options.managedProcess?.stop(),
|
|
5409
|
+
controlService?.close?.()
|
|
5410
|
+
]);
|
|
5411
|
+
await removeInstance(loadedConfig.repoRoot, process.pid, { environment });
|
|
5412
|
+
} finally {
|
|
5413
|
+
try {
|
|
5414
|
+
await lock?.release();
|
|
5415
|
+
} finally {
|
|
5416
|
+
resolveClosed();
|
|
5417
|
+
}
|
|
5418
|
+
}
|
|
5419
|
+
})();
|
|
5420
|
+
return closePromise;
|
|
4973
5421
|
}
|
|
4974
5422
|
};
|
|
4975
5423
|
} catch (error) {
|
|
@@ -4990,7 +5438,7 @@ async function startAttachBridge(options, dependencies = {}) {
|
|
|
4990
5438
|
const repoRoot = await discoverGitWorktreeRoot(cwd);
|
|
4991
5439
|
const configRoot = await discoverVisualDevConfigRoot(cwd, repoRoot);
|
|
4992
5440
|
const loadedConfig = await loadVisualDevConfig(repoRoot, { configRoot });
|
|
4993
|
-
|
|
5441
|
+
const bridge = await startBridgeCore(
|
|
4994
5442
|
{
|
|
4995
5443
|
mode: "attach",
|
|
4996
5444
|
loadedConfig,
|
|
@@ -5001,6 +5449,13 @@ async function startAttachBridge(options, dependencies = {}) {
|
|
|
5001
5449
|
},
|
|
5002
5450
|
dependencies
|
|
5003
5451
|
);
|
|
5452
|
+
if (dependencies.upstreamMonitor !== false) {
|
|
5453
|
+
monitorUpstream(bridge, {
|
|
5454
|
+
initialTimeoutMs: loadedConfig.config.upstream.ready.timeoutMs,
|
|
5455
|
+
...dependencies.upstreamMonitor
|
|
5456
|
+
});
|
|
5457
|
+
}
|
|
5458
|
+
return bridge;
|
|
5004
5459
|
}
|
|
5005
5460
|
async function startManagedBridge(options = {}, dependencies = {}) {
|
|
5006
5461
|
const cwd = dependencies.cwd ?? process.cwd();
|
|
@@ -5102,6 +5557,7 @@ async function runBridgeUntilSignal(bridge, processLike = process, gracefulTimeo
|
|
|
5102
5557
|
};
|
|
5103
5558
|
for (const signal of signals) processLike.once(signal, shutdown);
|
|
5104
5559
|
void bridge.managedProcess?.exit.then(shutdown);
|
|
5560
|
+
void bridge.closed?.then(shutdown);
|
|
5105
5561
|
});
|
|
5106
5562
|
}
|
|
5107
5563
|
function formatBridgeSummary(bridge) {
|
|
@@ -5120,9 +5576,9 @@ function formatBridgeSummary(bridge) {
|
|
|
5120
5576
|
import { constants } from "node:fs";
|
|
5121
5577
|
import { access as access2, stat as stat3 } from "node:fs/promises";
|
|
5122
5578
|
import { delimiter, isAbsolute as isAbsolute5, join as join4, relative as relative6, resolve as resolve7 } from "node:path";
|
|
5123
|
-
import { execFile as
|
|
5124
|
-
import { promisify as
|
|
5125
|
-
var
|
|
5579
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
5580
|
+
import { promisify as promisify3 } from "node:util";
|
|
5581
|
+
var execFileAsync3 = promisify3(execFile3);
|
|
5126
5582
|
async function fileExists(path) {
|
|
5127
5583
|
try {
|
|
5128
5584
|
await stat3(path);
|
|
@@ -5136,7 +5592,7 @@ async function fileExists(path) {
|
|
|
5136
5592
|
}
|
|
5137
5593
|
async function isIgnored(repoRoot, path) {
|
|
5138
5594
|
try {
|
|
5139
|
-
await
|
|
5595
|
+
await execFileAsync3("git", ["-C", repoRoot, "check-ignore", "--quiet", "--", path], {
|
|
5140
5596
|
windowsHide: true
|
|
5141
5597
|
});
|
|
5142
5598
|
return true;
|
|
@@ -5178,6 +5634,7 @@ async function runDoctor(dependencies = {}) {
|
|
|
5178
5634
|
repoRoot
|
|
5179
5635
|
);
|
|
5180
5636
|
const loaded = await loadVisualDevConfig(repoRoot, { configRoot });
|
|
5637
|
+
const environment = dependencies.environment ?? process.env;
|
|
5181
5638
|
checks.push({
|
|
5182
5639
|
name: "config",
|
|
5183
5640
|
status: loaded.loadedFiles.length === 0 ? "warning" : "pass",
|
|
@@ -5198,7 +5655,7 @@ async function runDoctor(dependencies = {}) {
|
|
|
5198
5655
|
const available = executable !== void 0 && await executableAvailable(
|
|
5199
5656
|
executable,
|
|
5200
5657
|
loaded.workspaceRoot,
|
|
5201
|
-
|
|
5658
|
+
environment
|
|
5202
5659
|
);
|
|
5203
5660
|
checks.push({
|
|
5204
5661
|
name: "dev-command",
|
|
@@ -5206,6 +5663,65 @@ async function runDoctor(dependencies = {}) {
|
|
|
5206
5663
|
message: available ? `${executable} is executable.` : `${executable ?? "<empty>"} was not found or is not executable.`
|
|
5207
5664
|
});
|
|
5208
5665
|
}
|
|
5666
|
+
const adapter = loaded.config.agent.adapter;
|
|
5667
|
+
const adapterSupported = adapter === "codex";
|
|
5668
|
+
const agentAvailable = await executableAvailable(
|
|
5669
|
+
adapter,
|
|
5670
|
+
loaded.workspaceRoot,
|
|
5671
|
+
environment
|
|
5672
|
+
);
|
|
5673
|
+
checks.push({
|
|
5674
|
+
name: "agent",
|
|
5675
|
+
status: adapterSupported && agentAvailable ? "pass" : "fail",
|
|
5676
|
+
message: !adapterSupported ? `${adapter} is configured but is not implemented in this build.` : agentAvailable ? `${adapter} is executable.` : `${adapter} was not found or is not executable.`
|
|
5677
|
+
});
|
|
5678
|
+
const rtkAvailable = await executableAvailable(
|
|
5679
|
+
"rtk",
|
|
5680
|
+
loaded.workspaceRoot,
|
|
5681
|
+
environment
|
|
5682
|
+
);
|
|
5683
|
+
checks.push({
|
|
5684
|
+
name: "rtk",
|
|
5685
|
+
status: rtkAvailable ? "pass" : "warning",
|
|
5686
|
+
message: rtkAvailable ? "rtk is available for token-efficient command output." : "rtk was not found; agent commands will use their native output."
|
|
5687
|
+
});
|
|
5688
|
+
const verificationCommands = loaded.config.verification.commands;
|
|
5689
|
+
if (verificationCommands.length === 0) {
|
|
5690
|
+
checks.push({
|
|
5691
|
+
name: "verification",
|
|
5692
|
+
status: "warning",
|
|
5693
|
+
message: "No verification commands are configured."
|
|
5694
|
+
});
|
|
5695
|
+
} else {
|
|
5696
|
+
const availability = await Promise.all(
|
|
5697
|
+
verificationCommands.map(async ({ command: command2, name }) => ({
|
|
5698
|
+
name,
|
|
5699
|
+
available: await executableAvailable(
|
|
5700
|
+
command2[0] ?? "",
|
|
5701
|
+
loaded.workspaceRoot,
|
|
5702
|
+
environment
|
|
5703
|
+
)
|
|
5704
|
+
}))
|
|
5705
|
+
);
|
|
5706
|
+
const missing = availability.filter(({ available }) => !available).map(({ name }) => name);
|
|
5707
|
+
checks.push({
|
|
5708
|
+
name: "verification",
|
|
5709
|
+
status: missing.length === 0 ? "pass" : "fail",
|
|
5710
|
+
message: missing.length === 0 ? `${verificationCommands.length} verification command(s) are ready.` : `Missing executable for: ${missing.join(", ")}.`
|
|
5711
|
+
});
|
|
5712
|
+
}
|
|
5713
|
+
const publicUrl = loaded.config.gateway.publicUrl;
|
|
5714
|
+
checks.push({
|
|
5715
|
+
name: "public-url",
|
|
5716
|
+
status: publicUrl === void 0 ? "warning" : "pass",
|
|
5717
|
+
message: publicUrl === void 0 ? "No gateway.publicUrl is configured; pairing links will use the local gateway URL." : `Pairing links will use ${publicUrl}.`
|
|
5718
|
+
});
|
|
5719
|
+
const allowedOrigins = loaded.config.security.allowedOrigins;
|
|
5720
|
+
checks.push({
|
|
5721
|
+
name: "allowed-origins",
|
|
5722
|
+
status: allowedOrigins.length > 0 || publicUrl !== void 0 ? "pass" : "warning",
|
|
5723
|
+
message: allowedOrigins.length > 0 ? `${allowedOrigins.length} browser origin(s) are explicitly allowed.` : publicUrl !== void 0 ? "The configured public URL origin will be allowed automatically." : "No browser origins are configured; add security.allowedOrigins before remote access."
|
|
5724
|
+
});
|
|
5209
5725
|
} catch (error) {
|
|
5210
5726
|
checks.push({
|
|
5211
5727
|
name: "config",
|
|
@@ -5234,6 +5750,20 @@ var VITE_CONFIG_FILES = [
|
|
|
5234
5750
|
"vite.config.mjs"
|
|
5235
5751
|
];
|
|
5236
5752
|
var VITE_IMPORT = 'import { visualRemote } from "visual-remote/vite";';
|
|
5753
|
+
var NEXT_CONFIG_FILES = [
|
|
5754
|
+
"next.config.ts",
|
|
5755
|
+
"next.config.mjs",
|
|
5756
|
+
"next.config.js"
|
|
5757
|
+
];
|
|
5758
|
+
var NEXT_ESM_IMPORT = 'import { withVisualRemote } from "visual-remote/next";';
|
|
5759
|
+
var NEXT_CJS_IMPORT = 'const { withVisualRemote } = require("visual-remote/next");';
|
|
5760
|
+
var NEXT_CLIENT_MODULE = "visual-remote/next/client";
|
|
5761
|
+
var NEXT_CLIENT_BOOTSTRAP = [
|
|
5762
|
+
'if (process.env.NODE_ENV === "development") {',
|
|
5763
|
+
` void import("${NEXT_CLIENT_MODULE}");`,
|
|
5764
|
+
"}",
|
|
5765
|
+
""
|
|
5766
|
+
].join("\n");
|
|
5237
5767
|
function isMissingFile(error) {
|
|
5238
5768
|
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
5239
5769
|
}
|
|
@@ -5284,6 +5814,13 @@ function isViteProject(manifest, devScript) {
|
|
|
5284
5814
|
};
|
|
5285
5815
|
return "vite" in packages || /(^|[\s;&|])vite(?:\s|$)/.test(devScript);
|
|
5286
5816
|
}
|
|
5817
|
+
function isNextProject(manifest, devScript) {
|
|
5818
|
+
const packages = {
|
|
5819
|
+
...packageRecord(manifest.dependencies),
|
|
5820
|
+
...packageRecord(manifest.devDependencies)
|
|
5821
|
+
};
|
|
5822
|
+
return "next" in packages || /(^|[\s;&|])next(?:\s|$)/.test(devScript);
|
|
5823
|
+
}
|
|
5287
5824
|
function hasVisualRemote(manifest) {
|
|
5288
5825
|
return "visual-remote" in packageRecord(manifest.dependencies) || "visual-remote" in packageRecord(manifest.devDependencies);
|
|
5289
5826
|
}
|
|
@@ -5330,6 +5867,13 @@ async function findViteConfig(projectRoot) {
|
|
|
5330
5867
|
"visual init found Vite but could not find vite.config.ts, .mts, .js, or .mjs"
|
|
5331
5868
|
);
|
|
5332
5869
|
}
|
|
5870
|
+
async function findNextConfig(projectRoot) {
|
|
5871
|
+
for (const filename of NEXT_CONFIG_FILES) {
|
|
5872
|
+
const candidate = join5(projectRoot, filename);
|
|
5873
|
+
if (await fileExists2(candidate)) return candidate;
|
|
5874
|
+
}
|
|
5875
|
+
return join5(projectRoot, "next.config.mjs");
|
|
5876
|
+
}
|
|
5333
5877
|
function insertViteImport(source) {
|
|
5334
5878
|
const lines = source.split("\n");
|
|
5335
5879
|
let index = 0;
|
|
@@ -5367,6 +5911,130 @@ async function configureVite(configPath) {
|
|
|
5367
5911
|
await writeFile3(configPath, transformed, "utf8");
|
|
5368
5912
|
return true;
|
|
5369
5913
|
}
|
|
5914
|
+
function expressionEnd(source, expressionStart) {
|
|
5915
|
+
let roundDepth = 0;
|
|
5916
|
+
let squareDepth = 0;
|
|
5917
|
+
let curlyDepth = 0;
|
|
5918
|
+
let quote;
|
|
5919
|
+
let lineComment = false;
|
|
5920
|
+
let blockComment = false;
|
|
5921
|
+
for (let index = expressionStart; index < source.length; index += 1) {
|
|
5922
|
+
const character = source[index];
|
|
5923
|
+
const nextCharacter = source[index + 1];
|
|
5924
|
+
if (lineComment) {
|
|
5925
|
+
if (character === "\n") lineComment = false;
|
|
5926
|
+
continue;
|
|
5927
|
+
}
|
|
5928
|
+
if (blockComment) {
|
|
5929
|
+
if (character === "*" && nextCharacter === "/") {
|
|
5930
|
+
blockComment = false;
|
|
5931
|
+
index += 1;
|
|
5932
|
+
}
|
|
5933
|
+
continue;
|
|
5934
|
+
}
|
|
5935
|
+
if (quote !== void 0) {
|
|
5936
|
+
if (character === "\\") {
|
|
5937
|
+
index += 1;
|
|
5938
|
+
} else if (character === quote) {
|
|
5939
|
+
quote = void 0;
|
|
5940
|
+
}
|
|
5941
|
+
continue;
|
|
5942
|
+
}
|
|
5943
|
+
if (character === "/" && nextCharacter === "/") {
|
|
5944
|
+
lineComment = true;
|
|
5945
|
+
index += 1;
|
|
5946
|
+
continue;
|
|
5947
|
+
}
|
|
5948
|
+
if (character === "/" && nextCharacter === "*") {
|
|
5949
|
+
blockComment = true;
|
|
5950
|
+
index += 1;
|
|
5951
|
+
continue;
|
|
5952
|
+
}
|
|
5953
|
+
if (character === "'" || character === '"' || character === "`") {
|
|
5954
|
+
quote = character;
|
|
5955
|
+
continue;
|
|
5956
|
+
}
|
|
5957
|
+
if (character === "(") roundDepth += 1;
|
|
5958
|
+
if (character === ")") roundDepth -= 1;
|
|
5959
|
+
if (character === "[") squareDepth += 1;
|
|
5960
|
+
if (character === "]") squareDepth -= 1;
|
|
5961
|
+
if (character === "{") curlyDepth += 1;
|
|
5962
|
+
if (character === "}") curlyDepth -= 1;
|
|
5963
|
+
if (character === ";" && roundDepth === 0 && squareDepth === 0 && curlyDepth === 0) {
|
|
5964
|
+
return index;
|
|
5965
|
+
}
|
|
5966
|
+
}
|
|
5967
|
+
return source.length;
|
|
5968
|
+
}
|
|
5969
|
+
function wrapConfigExpression(source, assignmentPattern) {
|
|
5970
|
+
const assignment = assignmentPattern.exec(source);
|
|
5971
|
+
if (assignment?.index === void 0) {
|
|
5972
|
+
throw new Error("visual init could not find the default Next.js config export");
|
|
5973
|
+
}
|
|
5974
|
+
let start = assignment.index + assignment[0].length;
|
|
5975
|
+
while (/\s/.test(source[start] ?? "")) start += 1;
|
|
5976
|
+
const end = expressionEnd(source, start);
|
|
5977
|
+
const rawExpression = source.slice(start, end);
|
|
5978
|
+
const trailingWhitespace = rawExpression.match(/\s*$/)?.[0] ?? "";
|
|
5979
|
+
const expression = rawExpression.slice(0, rawExpression.length - trailingWhitespace.length);
|
|
5980
|
+
if (expression.length === 0) {
|
|
5981
|
+
throw new Error("visual init found an empty Next.js config export");
|
|
5982
|
+
}
|
|
5983
|
+
return `${source.slice(0, start)}withVisualRemote(${expression})${trailingWhitespace}${source.slice(end)}`;
|
|
5984
|
+
}
|
|
5985
|
+
function transformNextConfig(source) {
|
|
5986
|
+
if (source.includes("visual-remote/next") && /\bwithVisualRemote\s*\(/.test(source)) {
|
|
5987
|
+
return source;
|
|
5988
|
+
}
|
|
5989
|
+
if (/\bmodule\.exports\s*=/.test(source)) {
|
|
5990
|
+
const wrapped2 = wrapConfigExpression(source, /\bmodule\.exports\s*=/);
|
|
5991
|
+
return wrapped2.includes(NEXT_CJS_IMPORT) ? wrapped2 : `${NEXT_CJS_IMPORT}
|
|
5992
|
+
${wrapped2}`;
|
|
5993
|
+
}
|
|
5994
|
+
const wrapped = wrapConfigExpression(source, /\bexport\s+default\b/);
|
|
5995
|
+
return wrapped.includes(NEXT_ESM_IMPORT) ? wrapped : `${NEXT_ESM_IMPORT}
|
|
5996
|
+
${wrapped}`;
|
|
5997
|
+
}
|
|
5998
|
+
async function configureNext(configPath) {
|
|
5999
|
+
let source;
|
|
6000
|
+
try {
|
|
6001
|
+
source = await readFile5(configPath, "utf8");
|
|
6002
|
+
} catch (error) {
|
|
6003
|
+
if (!isMissingFile(error)) throw error;
|
|
6004
|
+
source = "export default {};\n";
|
|
6005
|
+
}
|
|
6006
|
+
const transformed = transformNextConfig(source);
|
|
6007
|
+
if (transformed === source) return false;
|
|
6008
|
+
await writeFile3(configPath, transformed, "utf8");
|
|
6009
|
+
return true;
|
|
6010
|
+
}
|
|
6011
|
+
async function findNextClientPath(projectRoot) {
|
|
6012
|
+
const candidates = [
|
|
6013
|
+
join5(projectRoot, "instrumentation-client.ts"),
|
|
6014
|
+
join5(projectRoot, "instrumentation-client.js"),
|
|
6015
|
+
join5(projectRoot, "src", "instrumentation-client.ts"),
|
|
6016
|
+
join5(projectRoot, "src", "instrumentation-client.js")
|
|
6017
|
+
];
|
|
6018
|
+
for (const candidate of candidates) {
|
|
6019
|
+
if (await fileExists2(candidate)) return candidate;
|
|
6020
|
+
}
|
|
6021
|
+
const sourceRoot = await fileExists2(join5(projectRoot, "src")) ? join5(projectRoot, "src") : projectRoot;
|
|
6022
|
+
const extension = await fileExists2(join5(projectRoot, "tsconfig.json")) ? "ts" : "js";
|
|
6023
|
+
return join5(sourceRoot, `instrumentation-client.${extension}`);
|
|
6024
|
+
}
|
|
6025
|
+
async function configureNextClient(clientPath) {
|
|
6026
|
+
let source = "";
|
|
6027
|
+
try {
|
|
6028
|
+
source = await readFile5(clientPath, "utf8");
|
|
6029
|
+
} catch (error) {
|
|
6030
|
+
if (!isMissingFile(error)) throw error;
|
|
6031
|
+
}
|
|
6032
|
+
if (source.includes(NEXT_CLIENT_MODULE)) return false;
|
|
6033
|
+
const separator = source.length === 0 || source.endsWith("\n") ? "" : "\n";
|
|
6034
|
+
await mkdir3(dirname4(clientPath), { recursive: true });
|
|
6035
|
+
await writeFile3(clientPath, `${source}${separator}${NEXT_CLIENT_BOOTSTRAP}`, "utf8");
|
|
6036
|
+
return true;
|
|
6037
|
+
}
|
|
5370
6038
|
function installCommand(request) {
|
|
5371
6039
|
if (request.packageManager === "pnpm") {
|
|
5372
6040
|
return {
|
|
@@ -5416,11 +6084,12 @@ async function initializeVisualDev(dependencies = {}) {
|
|
|
5416
6084
|
const repoRoot = await discoverGitWorktreeRoot(projectRoot);
|
|
5417
6085
|
const manifest = await readManifest(projectRoot);
|
|
5418
6086
|
const devScript = readDevScript(manifest);
|
|
5419
|
-
|
|
5420
|
-
|
|
6087
|
+
const framework = isViteProject(manifest, devScript) ? "vite" : isNextProject(manifest, devScript) ? "next" : void 0;
|
|
6088
|
+
if (framework === void 0) {
|
|
6089
|
+
throw new Error("visual init currently supports Vite and Next.js projects");
|
|
5421
6090
|
}
|
|
5422
6091
|
const packageManager = await detectPackageManager(projectRoot, repoRoot, manifest);
|
|
5423
|
-
const integrationPath = await findViteConfig(projectRoot);
|
|
6092
|
+
const integrationPath = framework === "vite" ? await findViteConfig(projectRoot) : await findNextConfig(projectRoot);
|
|
5424
6093
|
const packageInstalled = !hasVisualRemote(manifest);
|
|
5425
6094
|
if (packageInstalled) {
|
|
5426
6095
|
await (dependencies.installPackage ?? installPackage)({
|
|
@@ -5429,7 +6098,9 @@ async function initializeVisualDev(dependencies = {}) {
|
|
|
5429
6098
|
packageSpec: "visual-remote@latest"
|
|
5430
6099
|
});
|
|
5431
6100
|
}
|
|
5432
|
-
const integrationChanged = await configureVite(integrationPath);
|
|
6101
|
+
const integrationChanged = framework === "vite" ? await configureVite(integrationPath) : await configureNext(integrationPath);
|
|
6102
|
+
const clientPath = framework === "next" ? await findNextClientPath(projectRoot) : void 0;
|
|
6103
|
+
const clientChanged = clientPath === void 0 ? void 0 : await configureNextClient(clientPath);
|
|
5433
6104
|
const configPath = join5(projectRoot, CONFIG_PATH);
|
|
5434
6105
|
const created = !await fileExists2(configPath);
|
|
5435
6106
|
if (created) {
|
|
@@ -5461,24 +6132,35 @@ async function initializeVisualDev(dependencies = {}) {
|
|
|
5461
6132
|
}
|
|
5462
6133
|
}
|
|
5463
6134
|
return {
|
|
6135
|
+
framework,
|
|
5464
6136
|
created,
|
|
5465
6137
|
configPath,
|
|
5466
6138
|
devScript,
|
|
5467
6139
|
packageManager,
|
|
5468
6140
|
integrationPath,
|
|
5469
6141
|
integrationChanged,
|
|
6142
|
+
...clientPath === void 0 || clientChanged === void 0 ? {} : { clientPath, clientChanged },
|
|
5470
6143
|
packageInstalled
|
|
5471
6144
|
};
|
|
5472
6145
|
}
|
|
5473
6146
|
function formatInitResult(result) {
|
|
5474
6147
|
const configLabel = result.created ? "Created" : "Existing";
|
|
5475
6148
|
const integrationLabel = result.integrationChanged ? "Configured" : "Existing";
|
|
5476
|
-
|
|
6149
|
+
const rows = [
|
|
6150
|
+
`Framework: ${result.framework === "next" ? "Next.js" : "Vite"}`,
|
|
5477
6151
|
`${configLabel}: ${relative7(process.cwd(), result.configPath) || CONFIG_PATH}`,
|
|
5478
|
-
`${integrationLabel}: ${relative7(process.cwd(), result.integrationPath)}
|
|
6152
|
+
`${integrationLabel}: ${relative7(process.cwd(), result.integrationPath)}`
|
|
6153
|
+
];
|
|
6154
|
+
if (result.clientPath !== void 0) {
|
|
6155
|
+
rows.push(
|
|
6156
|
+
`${result.clientChanged ? "Configured" : "Existing"}: ${relative7(process.cwd(), result.clientPath)}`
|
|
6157
|
+
);
|
|
6158
|
+
}
|
|
6159
|
+
rows.push(
|
|
5479
6160
|
result.packageInstalled ? "Installed: visual-remote@latest" : "Installed: visual-remote",
|
|
5480
6161
|
"Next: Start the app normally and open its original URL."
|
|
5481
|
-
|
|
6162
|
+
);
|
|
6163
|
+
return rows.join("\n");
|
|
5482
6164
|
}
|
|
5483
6165
|
|
|
5484
6166
|
// src/status.ts
|
|
@@ -5500,13 +6182,18 @@ function formatBridgeStatus(status) {
|
|
|
5500
6182
|
return "No Visual Bridge is running for this worktree.";
|
|
5501
6183
|
}
|
|
5502
6184
|
const { instance } = status;
|
|
5503
|
-
|
|
6185
|
+
const rows = [
|
|
5504
6186
|
`Project: ${instance.projectId}`,
|
|
5505
6187
|
`Status: ${instance.status}`,
|
|
5506
6188
|
`PID: ${instance.pid}`,
|
|
5507
6189
|
`Gateway: ${instance.gatewayUrl}`,
|
|
5508
6190
|
`Upstream: ${instance.upstreamUrl}`
|
|
5509
|
-
]
|
|
6191
|
+
];
|
|
6192
|
+
if (instance.publicUrl !== void 0) rows.push(`Public: ${instance.publicUrl}`);
|
|
6193
|
+
if (instance.activeTaskId !== void 0) {
|
|
6194
|
+
rows.push(`Active: ${instance.activeTaskId}`);
|
|
6195
|
+
}
|
|
6196
|
+
return rows.join("\n");
|
|
5510
6197
|
}
|
|
5511
6198
|
|
|
5512
6199
|
// src/index.ts
|
|
@@ -5531,8 +6218,8 @@ function setExitCode(dependencies, code) {
|
|
|
5531
6218
|
}
|
|
5532
6219
|
}
|
|
5533
6220
|
function createCli(dependencies = {}) {
|
|
5534
|
-
const program = new Command().name("visual").description("Visual Remote Dev Bridge").version("0.
|
|
5535
|
-
program.command("init").description("
|
|
6221
|
+
const program = new Command().name("visual").description("Visual Remote Dev Bridge").version("0.3.1");
|
|
6222
|
+
program.command("init").description("Configure Visual Remote for the current Vite or Next.js project").action(async () => {
|
|
5536
6223
|
const result = await initializeVisualDev(dependencies);
|
|
5537
6224
|
output(dependencies, formatInitResult(result));
|
|
5538
6225
|
});
|
|
@@ -5583,7 +6270,7 @@ async function main(argv = process.argv, dependencies = {}) {
|
|
|
5583
6270
|
}
|
|
5584
6271
|
function isDirectEntry(entryPath2) {
|
|
5585
6272
|
try {
|
|
5586
|
-
return pathToFileURL(
|
|
6273
|
+
return pathToFileURL(realpathSync2(resolve8(entryPath2))).href === import.meta.url;
|
|
5587
6274
|
} catch {
|
|
5588
6275
|
return false;
|
|
5589
6276
|
}
|
|
@@ -5611,5 +6298,6 @@ export {
|
|
|
5611
6298
|
runDoctor,
|
|
5612
6299
|
startAttachBridge,
|
|
5613
6300
|
startManagedBridge,
|
|
6301
|
+
transformNextConfig,
|
|
5614
6302
|
transformViteConfig
|
|
5615
6303
|
};
|