@openchamber/web 1.4.4 → 1.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{ToolOutputDialog-BXPi0SDL.js → ToolOutputDialog-Bk6Uzhck.js} +2 -2
- package/dist/assets/index-Cxzt1pIT.css +1 -0
- package/dist/assets/{index-CqQbtUxU.js → index-_QJSNcFo.js} +2 -2
- package/dist/assets/main-COr_R8wu.js +128 -0
- package/dist/assets/{vendor-.bun-BEzqubWg.js → vendor-.bun-C07YQe9X.js} +367 -367
- package/dist/index.html +3 -3
- package/package.json +2 -2
- package/server/index.js +90 -358
- package/dist/assets/index-DR2OFuzB.css +0 -1
- package/dist/assets/main-j7ViaNnX.js +0 -127
package/dist/index.html
CHANGED
|
@@ -160,10 +160,10 @@
|
|
|
160
160
|
pointer-events: none;
|
|
161
161
|
}
|
|
162
162
|
</style>
|
|
163
|
-
<script type="module" crossorigin src="/assets/index-
|
|
164
|
-
<link rel="modulepreload" crossorigin href="/assets/vendor-.bun-
|
|
163
|
+
<script type="module" crossorigin src="/assets/index-_QJSNcFo.js"></script>
|
|
164
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-.bun-C07YQe9X.js">
|
|
165
165
|
<link rel="stylesheet" crossorigin href="/assets/vendor--Jn2c0Clh.css">
|
|
166
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
166
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Cxzt1pIT.css">
|
|
167
167
|
</head>
|
|
168
168
|
<body class="h-full bg-background text-foreground">
|
|
169
169
|
<div id="root" class="h-full">
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openchamber/web",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.6",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./server/index.js",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"@fontsource/ibm-plex-mono": "^5.2.7",
|
|
27
27
|
"@fontsource/ibm-plex-sans": "^5.1.1",
|
|
28
28
|
"@ibm/plex": "^6.4.1",
|
|
29
|
-
"@opencode-ai/sdk": "^1.1.
|
|
29
|
+
"@opencode-ai/sdk": "^1.1.8",
|
|
30
30
|
"@radix-ui/react-collapsible": "^1.1.12",
|
|
31
31
|
"@radix-ui/react-dialog": "^1.1.15",
|
|
32
32
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
package/server/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
2
|
import { createProxyMiddleware } from 'http-proxy-middleware';
|
|
3
3
|
import path from 'path';
|
|
4
|
-
import { spawn
|
|
4
|
+
import { spawn } from 'child_process';
|
|
5
5
|
import fs from 'fs';
|
|
6
6
|
import http from 'http';
|
|
7
7
|
import { fileURLToPath } from 'url';
|
|
@@ -9,12 +9,12 @@ import os from 'os';
|
|
|
9
9
|
import crypto from 'crypto';
|
|
10
10
|
import { createUiAuth } from './lib/ui-auth.js';
|
|
11
11
|
import { startCloudflareTunnel, printTunnelWarning, checkCloudflaredAvailable } from './lib/cloudflare-tunnel.js';
|
|
12
|
+
import { createOpencodeServer } from '@opencode-ai/sdk/server';
|
|
12
13
|
|
|
13
14
|
const __filename = fileURLToPath(import.meta.url);
|
|
14
15
|
const __dirname = path.dirname(__filename);
|
|
15
16
|
|
|
16
17
|
const DEFAULT_PORT = 3000;
|
|
17
|
-
const DEFAULT_OPENCODE_PORT = 0;
|
|
18
18
|
const HEALTH_CHECK_INTERVAL = 30000;
|
|
19
19
|
const SHUTDOWN_TIMEOUT = 10000;
|
|
20
20
|
const MODELS_DEV_API_URL = 'https://models.dev/api.json';
|
|
@@ -590,11 +590,17 @@ const sanitizeSettingsUpdate = (payload) => {
|
|
|
590
590
|
result.typographySizes = typography;
|
|
591
591
|
}
|
|
592
592
|
|
|
593
|
-
if (typeof candidate.defaultModel === 'string'
|
|
594
|
-
|
|
593
|
+
if (typeof candidate.defaultModel === 'string') {
|
|
594
|
+
const trimmed = candidate.defaultModel.trim();
|
|
595
|
+
result.defaultModel = trimmed.length > 0 ? trimmed : undefined;
|
|
595
596
|
}
|
|
596
|
-
if (typeof candidate.
|
|
597
|
-
|
|
597
|
+
if (typeof candidate.defaultVariant === 'string') {
|
|
598
|
+
const trimmed = candidate.defaultVariant.trim();
|
|
599
|
+
result.defaultVariant = trimmed.length > 0 ? trimmed : undefined;
|
|
600
|
+
}
|
|
601
|
+
if (typeof candidate.defaultAgent === 'string') {
|
|
602
|
+
const trimmed = candidate.defaultAgent.trim();
|
|
603
|
+
result.defaultAgent = trimmed.length > 0 ? trimmed : undefined;
|
|
598
604
|
}
|
|
599
605
|
if (typeof candidate.queueModeEnabled === 'boolean') {
|
|
600
606
|
result.queueModeEnabled = candidate.queueModeEnabled;
|
|
@@ -842,7 +848,6 @@ let openCodeApiDetectionTimer = null;
|
|
|
842
848
|
let isDetectingApiPrefix = false;
|
|
843
849
|
let openCodeApiDetectionPromise = null;
|
|
844
850
|
let lastOpenCodeError = null;
|
|
845
|
-
let openCodePortWaiters = [];
|
|
846
851
|
let isOpenCodeReady = false;
|
|
847
852
|
let openCodeNotReadySince = 0;
|
|
848
853
|
let exitOnShutdown = true;
|
|
@@ -884,12 +889,7 @@ async function isOpenCodeProcessHealthy() {
|
|
|
884
889
|
return false;
|
|
885
890
|
}
|
|
886
891
|
|
|
887
|
-
//
|
|
888
|
-
if (openCodeProcess.exitCode !== null || openCodeProcess.signalCode !== null) {
|
|
889
|
-
return false;
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
// Health check via HTTP
|
|
892
|
+
// Health check via HTTP since SDK object doesn't expose exitCode
|
|
893
893
|
try {
|
|
894
894
|
const response = await fetch(`http://127.0.0.1:${openCodePort}/session`, {
|
|
895
895
|
method: 'GET',
|
|
@@ -901,102 +901,6 @@ async function isOpenCodeProcessHealthy() {
|
|
|
901
901
|
}
|
|
902
902
|
}
|
|
903
903
|
|
|
904
|
-
const OPENCODE_BINARY_ENV =
|
|
905
|
-
process.env.OPENCODE_BINARY ||
|
|
906
|
-
process.env.OPENCHAMBER_BINARY ||
|
|
907
|
-
process.env.OPENCODE_PATH ||
|
|
908
|
-
process.env.OPENCHAMBER_OPENCODE_PATH ||
|
|
909
|
-
null;
|
|
910
|
-
|
|
911
|
-
function buildAugmentedPath() {
|
|
912
|
-
const augmented = new Set();
|
|
913
|
-
|
|
914
|
-
const loginShellPath = getLoginShellPath();
|
|
915
|
-
if (loginShellPath) {
|
|
916
|
-
for (const segment of loginShellPath.split(path.delimiter)) {
|
|
917
|
-
if (segment) {
|
|
918
|
-
augmented.add(segment);
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
const current = (process.env.PATH || '').split(path.delimiter).filter(Boolean);
|
|
924
|
-
for (const segment of current) {
|
|
925
|
-
augmented.add(segment);
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
return Array.from(augmented).join(path.delimiter);
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
function getLoginShellPath() {
|
|
932
|
-
if (process.platform === 'win32') {
|
|
933
|
-
return null;
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
const shell = process.env.SHELL || '/bin/zsh';
|
|
937
|
-
const shellName = path.basename(shell);
|
|
938
|
-
|
|
939
|
-
// Nushell requires different flag syntax and PATH access
|
|
940
|
-
const isNushell = shellName === 'nu' || shellName === 'nushell';
|
|
941
|
-
const args = isNushell
|
|
942
|
-
? ['-l', '-i', '-c', '$env.PATH | str join (char esep)']
|
|
943
|
-
: ['-lic', 'echo -n "$PATH"'];
|
|
944
|
-
|
|
945
|
-
try {
|
|
946
|
-
const result = spawnSync(shell, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
947
|
-
if (result.status === 0 && typeof result.stdout === 'string') {
|
|
948
|
-
const value = result.stdout.trim();
|
|
949
|
-
if (value) {
|
|
950
|
-
return value;
|
|
951
|
-
}
|
|
952
|
-
} else if (result.stderr) {
|
|
953
|
-
console.warn(`Failed to read PATH from login shell (${shell}): ${result.stderr}`);
|
|
954
|
-
}
|
|
955
|
-
} catch (error) {
|
|
956
|
-
console.warn(`Error executing login shell (${shell}) for PATH detection: ${error.message}`);
|
|
957
|
-
}
|
|
958
|
-
return null;
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
function resolveBinaryFromPath(binaryName, searchPath) {
|
|
962
|
-
if (!binaryName) {
|
|
963
|
-
return null;
|
|
964
|
-
}
|
|
965
|
-
if (path.isAbsolute(binaryName)) {
|
|
966
|
-
return fs.existsSync(binaryName) ? binaryName : null;
|
|
967
|
-
}
|
|
968
|
-
const directories = searchPath.split(path.delimiter).filter(Boolean);
|
|
969
|
-
for (const directory of directories) {
|
|
970
|
-
try {
|
|
971
|
-
const candidate = path.join(directory, binaryName);
|
|
972
|
-
if (fs.existsSync(candidate)) {
|
|
973
|
-
const stats = fs.statSync(candidate);
|
|
974
|
-
if (stats.isFile()) {
|
|
975
|
-
return candidate;
|
|
976
|
-
}
|
|
977
|
-
}
|
|
978
|
-
} catch {
|
|
979
|
-
// Ignore resolution errors, continue searching
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
return null;
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
function getOpencodeSpawnConfig() {
|
|
986
|
-
if (OPENCODE_BINARY_ENV) {
|
|
987
|
-
const explicit = resolveBinaryFromPath(OPENCODE_BINARY_ENV, process.env.PATH);
|
|
988
|
-
if (explicit) {
|
|
989
|
-
console.log(`Using OpenCode binary from OPENCODE_BINARY: ${explicit}`);
|
|
990
|
-
return { command: explicit, env: undefined };
|
|
991
|
-
}
|
|
992
|
-
console.warn(
|
|
993
|
-
`OPENCODE_BINARY path "${OPENCODE_BINARY_ENV}" not found. Falling back to search.`
|
|
994
|
-
);
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
return { command: 'opencode', env: undefined };
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
904
|
const ENV_CONFIGURED_OPENCODE_PORT = (() => {
|
|
1001
905
|
const raw =
|
|
1002
906
|
process.env.OPENCODE_PORT ||
|
|
@@ -1039,42 +943,31 @@ function setOpenCodePort(port) {
|
|
|
1039
943
|
}
|
|
1040
944
|
|
|
1041
945
|
lastOpenCodeError = null;
|
|
1042
|
-
|
|
1043
|
-
if (openCodePortWaiters.length > 0) {
|
|
1044
|
-
const waiters = openCodePortWaiters;
|
|
1045
|
-
openCodePortWaiters = [];
|
|
1046
|
-
for (const notify of waiters) {
|
|
1047
|
-
try {
|
|
1048
|
-
notify(numericPort);
|
|
1049
|
-
} catch (error) {
|
|
1050
|
-
console.warn('Failed to notify OpenCode port waiter:', error);
|
|
1051
|
-
}
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
946
|
}
|
|
1055
947
|
|
|
1056
|
-
|
|
1057
|
-
if (openCodePort !== null) {
|
|
1058
|
-
return openCodePort;
|
|
1059
|
-
}
|
|
948
|
+
const API_PREFIX_CANDIDATES = ['', '/api']; // Simplified - only check root and /api
|
|
1060
949
|
|
|
1061
|
-
|
|
1062
|
-
|
|
950
|
+
async function waitForReady(url, timeoutMs = 10000) {
|
|
951
|
+
const start = Date.now();
|
|
952
|
+
while (Date.now() - start < timeoutMs) {
|
|
953
|
+
try {
|
|
954
|
+
const controller = new AbortController();
|
|
955
|
+
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
956
|
+
const res = await fetch(`${url.replace(/\/+$/, '')}/config`, {
|
|
957
|
+
method: 'GET',
|
|
958
|
+
headers: { Accept: 'application/json' },
|
|
959
|
+
signal: controller.signal
|
|
960
|
+
});
|
|
1063
961
|
clearTimeout(timeout);
|
|
1064
|
-
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
openCodePortWaiters.push(onPortDetected);
|
|
1073
|
-
});
|
|
962
|
+
if (res.ok) return true;
|
|
963
|
+
} catch {
|
|
964
|
+
// ignore
|
|
965
|
+
}
|
|
966
|
+
await new Promise(r => setTimeout(r, 100));
|
|
967
|
+
}
|
|
968
|
+
return false;
|
|
1074
969
|
}
|
|
1075
970
|
|
|
1076
|
-
const API_PREFIX_CANDIDATES = ['', '/api']; // Simplified - only check root and /api
|
|
1077
|
-
|
|
1078
971
|
function normalizeApiPrefix(prefix) {
|
|
1079
972
|
if (!prefix) {
|
|
1080
973
|
return '';
|
|
@@ -1110,51 +1003,6 @@ function setDetectedOpenCodeApiPrefix(prefix) {
|
|
|
1110
1003
|
}
|
|
1111
1004
|
}
|
|
1112
1005
|
|
|
1113
|
-
function detectPortFromLogMessage(message) {
|
|
1114
|
-
if (openCodePort && ENV_CONFIGURED_OPENCODE_PORT) {
|
|
1115
|
-
return;
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
const regex = /https?:\/\/[^:\s]+:(\d+)/gi;
|
|
1119
|
-
let match;
|
|
1120
|
-
while ((match = regex.exec(message)) !== null) {
|
|
1121
|
-
const port = parseInt(match[1], 10);
|
|
1122
|
-
if (Number.isFinite(port) && port > 0) {
|
|
1123
|
-
setOpenCodePort(port);
|
|
1124
|
-
return;
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
const fallbackMatch = /(?:^|\s)(?:127\.0\.0\.1|localhost):(\d+)/i.exec(message);
|
|
1129
|
-
if (fallbackMatch) {
|
|
1130
|
-
const port = parseInt(fallbackMatch[1], 10);
|
|
1131
|
-
if (Number.isFinite(port) && port > 0) {
|
|
1132
|
-
setOpenCodePort(port);
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
function detectPrefixFromLogMessage(message) {
|
|
1138
|
-
if (!openCodePort) {
|
|
1139
|
-
return;
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
const urlRegex = /https?:\/\/[^:\s]+:(\d+)(\/[^\s"']*)?/gi;
|
|
1143
|
-
let match;
|
|
1144
|
-
|
|
1145
|
-
while ((match = urlRegex.exec(message)) !== null) {
|
|
1146
|
-
const portMatch = parseInt(match[1], 10);
|
|
1147
|
-
if (portMatch !== openCodePort) {
|
|
1148
|
-
continue;
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
const path = match[2] || '';
|
|
1152
|
-
const normalized = normalizeApiPrefix(path);
|
|
1153
|
-
setDetectedOpenCodeApiPrefix(normalized);
|
|
1154
|
-
return;
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
1006
|
function getCandidateApiPrefixes() {
|
|
1159
1007
|
if (openCodeApiPrefixDetected) {
|
|
1160
1008
|
return [openCodeApiPrefix];
|
|
@@ -1486,136 +1334,57 @@ function parseArgs(argv = process.argv.slice(2)) {
|
|
|
1486
1334
|
}
|
|
1487
1335
|
|
|
1488
1336
|
async function startOpenCode() {
|
|
1489
|
-
const desiredPort = ENV_CONFIGURED_OPENCODE_PORT ??
|
|
1337
|
+
const desiredPort = ENV_CONFIGURED_OPENCODE_PORT ?? 0;
|
|
1490
1338
|
console.log(
|
|
1491
|
-
desiredPort
|
|
1339
|
+
desiredPort > 0
|
|
1492
1340
|
? `Starting OpenCode on requested port ${desiredPort}...`
|
|
1493
1341
|
: 'Starting OpenCode with dynamic port assignment...'
|
|
1494
1342
|
);
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
const { command, env } = getOpencodeSpawnConfig();
|
|
1498
|
-
const args = ['serve', '--port', desiredPort.toString()];
|
|
1499
|
-
console.log(`Launching OpenCode via "${command}" with args ${args.join(' ')}`);
|
|
1343
|
+
// Note: SDK starts in current process CWD. openCodeWorkingDirectory is tracked but not used for spawn in SDK.
|
|
1500
1344
|
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1345
|
+
try {
|
|
1346
|
+
const serverInstance = await createOpencodeServer({
|
|
1347
|
+
hostname: '127.0.0.1',
|
|
1348
|
+
port: desiredPort,
|
|
1349
|
+
timeout: 30000,
|
|
1350
|
+
env: {
|
|
1351
|
+
...process.env,
|
|
1352
|
+
// Pass minimal config to avoid pollution, but inherit PATH etc
|
|
1353
|
+
}
|
|
1354
|
+
});
|
|
1508
1355
|
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
firstSignalResolver = resolve;
|
|
1512
|
-
});
|
|
1513
|
-
let firstSignalSettled = false;
|
|
1514
|
-
const settleFirstSignal = () => {
|
|
1515
|
-
if (firstSignalSettled) {
|
|
1516
|
-
return;
|
|
1517
|
-
}
|
|
1518
|
-
firstSignalSettled = true;
|
|
1519
|
-
clearTimeout(firstSignalTimer);
|
|
1520
|
-
child.stdout.off('data', settleFirstSignal);
|
|
1521
|
-
child.stderr.off('data', settleFirstSignal);
|
|
1522
|
-
child.off('exit', settleFirstSignal);
|
|
1523
|
-
if (firstSignalResolver) {
|
|
1524
|
-
firstSignalResolver();
|
|
1356
|
+
if (!serverInstance || !serverInstance.url) {
|
|
1357
|
+
throw new Error('OpenCode server started but URL is missing');
|
|
1525
1358
|
}
|
|
1526
|
-
};
|
|
1527
|
-
const firstSignalTimer = setTimeout(settleFirstSignal, 750);
|
|
1528
|
-
|
|
1529
|
-
child.stdout.once('data', settleFirstSignal);
|
|
1530
|
-
child.stderr.once('data', settleFirstSignal);
|
|
1531
|
-
child.once('exit', settleFirstSignal);
|
|
1532
|
-
|
|
1533
|
-
child.stdout.on('data', (data) => {
|
|
1534
|
-
const text = data.toString();
|
|
1535
|
-
console.log(`OpenCode: ${text.trim()}`);
|
|
1536
|
-
detectPortFromLogMessage(text);
|
|
1537
|
-
detectPrefixFromLogMessage(text);
|
|
1538
|
-
settleFirstSignal();
|
|
1539
|
-
});
|
|
1540
1359
|
|
|
1541
|
-
|
|
1542
|
-
const
|
|
1543
|
-
|
|
1544
|
-
console.error(`OpenCode Error: ${lastOpenCodeError}`);
|
|
1545
|
-
detectPortFromLogMessage(text);
|
|
1546
|
-
detectPrefixFromLogMessage(text);
|
|
1547
|
-
settleFirstSignal();
|
|
1548
|
-
});
|
|
1360
|
+
const url = new URL(serverInstance.url);
|
|
1361
|
+
const port = parseInt(url.port, 10);
|
|
1362
|
+
const prefix = normalizeApiPrefix(url.pathname);
|
|
1549
1363
|
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
child.off('spawn', onSpawn);
|
|
1558
|
-
reject(error);
|
|
1559
|
-
};
|
|
1364
|
+
if (await waitForReady(serverInstance.url, 10000)) {
|
|
1365
|
+
setOpenCodePort(port);
|
|
1366
|
+
setDetectedOpenCodeApiPrefix(prefix); // SDK URL typically includes the prefix if any
|
|
1367
|
+
|
|
1368
|
+
isOpenCodeReady = true;
|
|
1369
|
+
lastOpenCodeError = null;
|
|
1370
|
+
openCodeNotReadySince = 0;
|
|
1560
1371
|
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1372
|
+
return serverInstance;
|
|
1373
|
+
} else {
|
|
1374
|
+
try {
|
|
1375
|
+
serverInstance.close();
|
|
1376
|
+
} catch {
|
|
1377
|
+
// ignore
|
|
1378
|
+
}
|
|
1379
|
+
throw new Error('Server started but health check failed (timeout)');
|
|
1380
|
+
}
|
|
1381
|
+
} catch (error) {
|
|
1564
1382
|
lastOpenCodeError = error.message;
|
|
1565
1383
|
openCodePort = null;
|
|
1566
1384
|
syncToHmrState();
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
});
|
|
1570
|
-
|
|
1571
|
-
if (startupError) {
|
|
1572
|
-
if (startupError.code === 'ENOENT') {
|
|
1573
|
-
const enhanced = new Error(
|
|
1574
|
-
`Failed to start OpenCode – executable "${command}" not found. ` +
|
|
1575
|
-
'Set OPENCODE_BINARY to the full path of the opencode CLI or ensure it is on PATH.'
|
|
1576
|
-
);
|
|
1577
|
-
enhanced.code = startupError.code;
|
|
1578
|
-
startupError = enhanced;
|
|
1579
|
-
}
|
|
1580
|
-
throw startupError;
|
|
1385
|
+
console.error(`Failed to start OpenCode: ${error.message}`);
|
|
1386
|
+
throw error;
|
|
1581
1387
|
}
|
|
1582
|
-
|
|
1583
|
-
child.on('exit', (code, signal) => {
|
|
1584
|
-
lastOpenCodeError = `OpenCode exited with code ${code}, signal ${signal ?? 'null'}`;
|
|
1585
|
-
isOpenCodeReady = false;
|
|
1586
|
-
openCodeNotReadySince = Date.now();
|
|
1587
|
-
|
|
1588
|
-
if (!isShuttingDown && !isRestartingOpenCode) {
|
|
1589
|
-
console.log(`OpenCode process exited with code ${code}, signal ${signal}`);
|
|
1590
|
-
|
|
1591
|
-
setTimeout(() => {
|
|
1592
|
-
restartOpenCode().catch((err) => {
|
|
1593
|
-
console.error('Failed to restart OpenCode after exit:', err);
|
|
1594
|
-
});
|
|
1595
|
-
}, 5000);
|
|
1596
|
-
} else if (isRestartingOpenCode) {
|
|
1597
|
-
console.log('OpenCode exit during controlled restart, not triggering auto-restart');
|
|
1598
|
-
}
|
|
1599
|
-
});
|
|
1600
|
-
|
|
1601
|
-
child.on('error', (error) => {
|
|
1602
|
-
lastOpenCodeError = error.message;
|
|
1603
|
-
isOpenCodeReady = false;
|
|
1604
|
-
openCodeNotReadySince = Date.now();
|
|
1605
|
-
console.error(`OpenCode process error: ${error.message}`);
|
|
1606
|
-
if (!isShuttingDown) {
|
|
1607
|
-
|
|
1608
|
-
setTimeout(() => {
|
|
1609
|
-
restartOpenCode().catch((err) => {
|
|
1610
|
-
console.error('Failed to restart OpenCode after error:', err);
|
|
1611
|
-
});
|
|
1612
|
-
}, 5000);
|
|
1613
|
-
}
|
|
1614
|
-
});
|
|
1615
|
-
|
|
1616
|
-
await firstSignalPromise;
|
|
1617
|
-
|
|
1618
|
-
return child;
|
|
1619
1388
|
}
|
|
1620
1389
|
|
|
1621
1390
|
async function restartOpenCode() {
|
|
@@ -1632,60 +1401,16 @@ async function restartOpenCode() {
|
|
|
1632
1401
|
console.log('Restarting OpenCode process...');
|
|
1633
1402
|
|
|
1634
1403
|
if (openCodeProcess) {
|
|
1635
|
-
console.log('
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
processToTerminate.kill('SIGTERM');
|
|
1641
|
-
|
|
1642
|
-
await new Promise((resolve) => {
|
|
1643
|
-
let resolved = false;
|
|
1644
|
-
|
|
1645
|
-
const cleanup = () => {
|
|
1646
|
-
processToTerminate.off('exit', onExit);
|
|
1647
|
-
clearTimeout(forceKillTimer);
|
|
1648
|
-
clearTimeout(hardStopTimer);
|
|
1649
|
-
if (!resolved) {
|
|
1650
|
-
resolved = true;
|
|
1651
|
-
resolve();
|
|
1652
|
-
}
|
|
1653
|
-
};
|
|
1654
|
-
|
|
1655
|
-
const onExit = () => {
|
|
1656
|
-
cleanup();
|
|
1657
|
-
};
|
|
1658
|
-
|
|
1659
|
-
const forceKillTimer = setTimeout(() => {
|
|
1660
|
-
if (resolved) {
|
|
1661
|
-
return;
|
|
1662
|
-
}
|
|
1663
|
-
forcedTermination = true;
|
|
1664
|
-
console.warn('OpenCode process did not exit after SIGTERM, sending SIGKILL');
|
|
1665
|
-
processToTerminate.kill('SIGKILL');
|
|
1666
|
-
}, 3000);
|
|
1667
|
-
|
|
1668
|
-
const hardStopTimer = setTimeout(() => {
|
|
1669
|
-
if (resolved) {
|
|
1670
|
-
return;
|
|
1671
|
-
}
|
|
1672
|
-
console.warn('OpenCode process unresponsive after SIGKILL, continuing restart');
|
|
1673
|
-
cleanup();
|
|
1674
|
-
}, 5000);
|
|
1675
|
-
|
|
1676
|
-
processToTerminate.once('exit', onExit);
|
|
1677
|
-
});
|
|
1678
|
-
|
|
1679
|
-
if (forcedTermination) {
|
|
1680
|
-
console.log('OpenCode process terminated forcefully during restart');
|
|
1681
|
-
}
|
|
1682
|
-
} else {
|
|
1683
|
-
console.log('OpenCode process already stopped before restart command');
|
|
1404
|
+
console.log('Stopping existing OpenCode process...');
|
|
1405
|
+
try {
|
|
1406
|
+
openCodeProcess.close();
|
|
1407
|
+
} catch (error) {
|
|
1408
|
+
console.warn('Error closing OpenCode process:', error);
|
|
1684
1409
|
}
|
|
1685
|
-
|
|
1686
1410
|
openCodeProcess = null;
|
|
1687
1411
|
syncToHmrState();
|
|
1688
|
-
|
|
1412
|
+
|
|
1413
|
+
// Brief delay to allow port release
|
|
1689
1414
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
1690
1415
|
}
|
|
1691
1416
|
|
|
@@ -1696,6 +1421,8 @@ async function restartOpenCode() {
|
|
|
1696
1421
|
openCodePort = null;
|
|
1697
1422
|
syncToHmrState();
|
|
1698
1423
|
}
|
|
1424
|
+
|
|
1425
|
+
// Reset detection state
|
|
1699
1426
|
openCodeApiPrefixDetected = false;
|
|
1700
1427
|
if (openCodeApiDetectionTimer) {
|
|
1701
1428
|
clearTimeout(openCodeApiDetectionTimer);
|
|
@@ -1707,13 +1434,10 @@ async function restartOpenCode() {
|
|
|
1707
1434
|
openCodeProcess = await startOpenCode();
|
|
1708
1435
|
syncToHmrState();
|
|
1709
1436
|
|
|
1710
|
-
if (!ENV_CONFIGURED_OPENCODE_PORT) {
|
|
1711
|
-
await waitForOpenCodePort();
|
|
1712
|
-
}
|
|
1713
|
-
|
|
1714
1437
|
if (expressApp) {
|
|
1715
1438
|
setupProxy(expressApp);
|
|
1716
|
-
|
|
1439
|
+
// Ensure prefix is set correctly (SDK usually handles this, but just in case)
|
|
1440
|
+
ensureOpenCodeApiPrefix();
|
|
1717
1441
|
}
|
|
1718
1442
|
})();
|
|
1719
1443
|
|
|
@@ -4121,7 +3845,12 @@ async function main(options = {}) {
|
|
|
4121
3845
|
// NOTE: This route supports background execution to avoid tying up browser connections.
|
|
4122
3846
|
const execJobs = new Map();
|
|
4123
3847
|
const EXEC_JOB_TTL_MS = 30 * 60 * 1000;
|
|
4124
|
-
const COMMAND_TIMEOUT_MS =
|
|
3848
|
+
const COMMAND_TIMEOUT_MS = (() => {
|
|
3849
|
+
const raw = Number(process.env.OPENCHAMBER_FS_EXEC_TIMEOUT_MS);
|
|
3850
|
+
if (Number.isFinite(raw) && raw > 0) return raw;
|
|
3851
|
+
// `bun install` (common worktree setup cmd) often takes >60s.
|
|
3852
|
+
return 5 * 60 * 1000;
|
|
3853
|
+
})();
|
|
4125
3854
|
|
|
4126
3855
|
const pruneExecJobs = () => {
|
|
4127
3856
|
const now = Date.now();
|
|
@@ -4143,9 +3872,12 @@ async function main(options = {}) {
|
|
|
4143
3872
|
let stderr = '';
|
|
4144
3873
|
let timedOut = false;
|
|
4145
3874
|
|
|
3875
|
+
const envPath = buildAugmentedPath();
|
|
3876
|
+
const execEnv = { ...process.env, PATH: envPath };
|
|
3877
|
+
|
|
4146
3878
|
const child = spawn(shell, [shellFlag, command], {
|
|
4147
3879
|
cwd: resolvedCwd,
|
|
4148
|
-
env:
|
|
3880
|
+
env: execEnv,
|
|
4149
3881
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
4150
3882
|
});
|
|
4151
3883
|
|