@hua-labs/tap 0.1.1 → 0.2.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 +123 -152
- package/dist/bridges/codex-app-server-auth-gateway.d.mts +8 -0
- package/dist/bridges/codex-app-server-auth-gateway.mjs +183 -0
- package/dist/bridges/codex-app-server-auth-gateway.mjs.map +1 -0
- package/dist/bridges/codex-bridge-runner.d.mts +10 -1
- package/dist/bridges/codex-bridge-runner.mjs +233 -121
- package/dist/bridges/codex-bridge-runner.mjs.map +1 -1
- package/dist/cli.mjs +2728 -991
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +139 -5
- package/dist/index.mjs +528 -69
- package/dist/index.mjs.map +1 -1
- package/dist/mcp-server.d.mts +2 -0
- package/dist/mcp-server.mjs +22174 -0
- package/dist/mcp-server.mjs.map +1 -0
- package/package.json +7 -4
package/dist/index.mjs
CHANGED
|
@@ -1,45 +1,116 @@
|
|
|
1
1
|
// src/state.ts
|
|
2
|
-
import * as
|
|
3
|
-
import * as
|
|
2
|
+
import * as fs3 from "fs";
|
|
3
|
+
import * as path3 from "path";
|
|
4
4
|
import * as crypto from "crypto";
|
|
5
5
|
|
|
6
6
|
// src/config/resolve.ts
|
|
7
|
+
import * as fs2 from "fs";
|
|
8
|
+
import * as path2 from "path";
|
|
9
|
+
|
|
10
|
+
// src/utils.ts
|
|
7
11
|
import * as fs from "fs";
|
|
8
12
|
import * as path from "path";
|
|
9
|
-
var
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
var _noGitWarned = false;
|
|
14
|
+
function _setNoGitWarned() {
|
|
15
|
+
_noGitWarned = true;
|
|
16
|
+
}
|
|
13
17
|
function findRepoRoot(startDir = process.cwd()) {
|
|
14
18
|
let dir = path.resolve(startDir);
|
|
15
19
|
while (true) {
|
|
16
20
|
if (fs.existsSync(path.join(dir, ".git"))) return dir;
|
|
17
|
-
if (fs.existsSync(path.join(dir, "package.json")))
|
|
21
|
+
if (fs.existsSync(path.join(dir, "package.json"))) {
|
|
22
|
+
if (!_noGitWarned) {
|
|
23
|
+
_setNoGitWarned();
|
|
24
|
+
logWarn(
|
|
25
|
+
"No .git directory found. Resolved repo root via package.json \u2014 comms directory may be created in an unexpected location. Use --comms-dir to specify explicitly."
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
return dir;
|
|
29
|
+
}
|
|
18
30
|
const parent = path.dirname(dir);
|
|
19
31
|
if (parent === dir) break;
|
|
20
32
|
dir = parent;
|
|
21
33
|
}
|
|
34
|
+
if (!_noGitWarned) {
|
|
35
|
+
_setNoGitWarned();
|
|
36
|
+
logWarn(
|
|
37
|
+
"No git repository or package.json found. Using current directory as root. Run 'git init' first, or use --comms-dir to specify the comms path."
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
return process.cwd();
|
|
41
|
+
}
|
|
42
|
+
var _jsonMode = false;
|
|
43
|
+
function logWarn(message) {
|
|
44
|
+
if (!_jsonMode) console.log(` ! ${message}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/config/resolve.ts
|
|
48
|
+
var SHARED_CONFIG_FILE = "tap-config.json";
|
|
49
|
+
var LOCAL_CONFIG_FILE = "tap-config.local.json";
|
|
50
|
+
var LEGACY_CONFIG_FILE = ".tap-config";
|
|
51
|
+
var DEFAULT_RUNTIME_COMMAND = "node";
|
|
52
|
+
var DEFAULT_APP_SERVER_URL = "ws://127.0.0.1:4501";
|
|
53
|
+
function findRepoRoot2(startDir = process.cwd()) {
|
|
54
|
+
let dir = path2.resolve(startDir);
|
|
55
|
+
while (true) {
|
|
56
|
+
if (fs2.existsSync(path2.join(dir, ".git"))) return dir;
|
|
57
|
+
if (fs2.existsSync(path2.join(dir, "package.json"))) {
|
|
58
|
+
if (!_noGitWarned) {
|
|
59
|
+
_setNoGitWarned();
|
|
60
|
+
console.error(
|
|
61
|
+
"[tap] warning: No .git directory found. Resolved via package.json. Use --comms-dir to specify explicitly."
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
return dir;
|
|
65
|
+
}
|
|
66
|
+
const parent = path2.dirname(dir);
|
|
67
|
+
if (parent === dir) break;
|
|
68
|
+
dir = parent;
|
|
69
|
+
}
|
|
70
|
+
if (!_noGitWarned) {
|
|
71
|
+
_setNoGitWarned();
|
|
72
|
+
console.error(
|
|
73
|
+
"[tap] warning: No git repository found. Using cwd as root. Run 'git init' or use --comms-dir."
|
|
74
|
+
);
|
|
75
|
+
}
|
|
22
76
|
return process.cwd();
|
|
23
77
|
}
|
|
24
78
|
function loadJsonFile(filePath) {
|
|
25
|
-
if (!
|
|
79
|
+
if (!fs2.existsSync(filePath)) return null;
|
|
26
80
|
try {
|
|
27
|
-
const raw =
|
|
81
|
+
const raw = fs2.readFileSync(filePath, "utf-8");
|
|
28
82
|
return JSON.parse(raw);
|
|
29
83
|
} catch {
|
|
30
84
|
return null;
|
|
31
85
|
}
|
|
32
86
|
}
|
|
33
87
|
function loadSharedConfig(repoRoot) {
|
|
34
|
-
return loadJsonFile(
|
|
88
|
+
return loadJsonFile(path2.join(repoRoot, SHARED_CONFIG_FILE));
|
|
35
89
|
}
|
|
36
90
|
function loadLocalConfig(repoRoot) {
|
|
37
|
-
return loadJsonFile(
|
|
91
|
+
return loadJsonFile(path2.join(repoRoot, LOCAL_CONFIG_FILE));
|
|
92
|
+
}
|
|
93
|
+
function readLegacyShellValue(configText, key) {
|
|
94
|
+
const match = configText.match(new RegExp(`^${key}="?(.+?)"?$`, "m"));
|
|
95
|
+
return match?.[1]?.trim() || null;
|
|
96
|
+
}
|
|
97
|
+
function loadLegacyShellConfig(repoRoot) {
|
|
98
|
+
const filePath = path2.join(repoRoot, LEGACY_CONFIG_FILE);
|
|
99
|
+
if (!fs2.existsSync(filePath)) return null;
|
|
100
|
+
try {
|
|
101
|
+
const raw = fs2.readFileSync(filePath, "utf-8");
|
|
102
|
+
const commsDir = readLegacyShellValue(raw, "TAP_COMMS_DIR");
|
|
103
|
+
if (!commsDir) return null;
|
|
104
|
+
return { commsDir };
|
|
105
|
+
} catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
38
108
|
}
|
|
39
109
|
function resolveConfig(overrides = {}, startDir) {
|
|
40
|
-
const repoRoot =
|
|
110
|
+
const repoRoot = findRepoRoot2(startDir);
|
|
41
111
|
const shared = loadSharedConfig(repoRoot) ?? {};
|
|
42
112
|
const local = loadLocalConfig(repoRoot) ?? {};
|
|
113
|
+
const legacy = loadLegacyShellConfig(repoRoot) ?? {};
|
|
43
114
|
const sources = {
|
|
44
115
|
repoRoot: "auto",
|
|
45
116
|
commsDir: "auto",
|
|
@@ -49,10 +120,10 @@ function resolveConfig(overrides = {}, startDir) {
|
|
|
49
120
|
};
|
|
50
121
|
let commsDir;
|
|
51
122
|
if (overrides.commsDir) {
|
|
52
|
-
commsDir =
|
|
123
|
+
commsDir = resolvePath(repoRoot, overrides.commsDir);
|
|
53
124
|
sources.commsDir = "cli-flag";
|
|
54
125
|
} else if (process.env.TAP_COMMS_DIR) {
|
|
55
|
-
commsDir =
|
|
126
|
+
commsDir = resolvePath(repoRoot, process.env.TAP_COMMS_DIR);
|
|
56
127
|
sources.commsDir = "env";
|
|
57
128
|
} else if (local.commsDir) {
|
|
58
129
|
commsDir = resolvePath(repoRoot, local.commsDir);
|
|
@@ -60,15 +131,18 @@ function resolveConfig(overrides = {}, startDir) {
|
|
|
60
131
|
} else if (shared.commsDir) {
|
|
61
132
|
commsDir = resolvePath(repoRoot, shared.commsDir);
|
|
62
133
|
sources.commsDir = "shared-config";
|
|
134
|
+
} else if (legacy.commsDir) {
|
|
135
|
+
commsDir = resolvePath(repoRoot, legacy.commsDir);
|
|
136
|
+
sources.commsDir = "legacy-shell-config";
|
|
63
137
|
} else {
|
|
64
|
-
commsDir =
|
|
138
|
+
commsDir = path2.join(repoRoot, "tap-comms");
|
|
65
139
|
}
|
|
66
140
|
let stateDir;
|
|
67
141
|
if (overrides.stateDir) {
|
|
68
|
-
stateDir =
|
|
142
|
+
stateDir = resolvePath(repoRoot, overrides.stateDir);
|
|
69
143
|
sources.stateDir = "cli-flag";
|
|
70
144
|
} else if (process.env.TAP_STATE_DIR) {
|
|
71
|
-
stateDir =
|
|
145
|
+
stateDir = resolvePath(repoRoot, process.env.TAP_STATE_DIR);
|
|
72
146
|
sources.stateDir = "env";
|
|
73
147
|
} else if (local.stateDir) {
|
|
74
148
|
stateDir = resolvePath(repoRoot, local.stateDir);
|
|
@@ -77,7 +151,7 @@ function resolveConfig(overrides = {}, startDir) {
|
|
|
77
151
|
stateDir = resolvePath(repoRoot, shared.stateDir);
|
|
78
152
|
sources.stateDir = "shared-config";
|
|
79
153
|
} else {
|
|
80
|
-
stateDir =
|
|
154
|
+
stateDir = path2.join(repoRoot, ".tap-comms");
|
|
81
155
|
}
|
|
82
156
|
let runtimeCommand;
|
|
83
157
|
if (overrides.runtimeCommand) {
|
|
@@ -117,19 +191,33 @@ function resolveConfig(overrides = {}, startDir) {
|
|
|
117
191
|
};
|
|
118
192
|
}
|
|
119
193
|
function saveSharedConfig(repoRoot, config) {
|
|
120
|
-
const filePath =
|
|
194
|
+
const filePath = path2.join(repoRoot, SHARED_CONFIG_FILE);
|
|
121
195
|
const tmp = `${filePath}.tmp.${process.pid}`;
|
|
122
|
-
|
|
123
|
-
|
|
196
|
+
fs2.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
197
|
+
fs2.renameSync(tmp, filePath);
|
|
124
198
|
}
|
|
125
199
|
function saveLocalConfig(repoRoot, config) {
|
|
126
|
-
const filePath =
|
|
200
|
+
const filePath = path2.join(repoRoot, LOCAL_CONFIG_FILE);
|
|
127
201
|
const tmp = `${filePath}.tmp.${process.pid}`;
|
|
128
|
-
|
|
129
|
-
|
|
202
|
+
fs2.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
203
|
+
fs2.renameSync(tmp, filePath);
|
|
130
204
|
}
|
|
131
205
|
function resolvePath(repoRoot, p) {
|
|
132
|
-
|
|
206
|
+
const normalized = normalizeTapPath(p);
|
|
207
|
+
return path2.isAbsolute(normalized) ? normalized : path2.resolve(repoRoot, normalized);
|
|
208
|
+
}
|
|
209
|
+
function normalizeTapPath(input) {
|
|
210
|
+
const trimmed = input.trim().replace(/^["'`]+|["'`]+$/g, "");
|
|
211
|
+
if (/^[A-Za-z]:[\\/]/.test(trimmed)) {
|
|
212
|
+
return trimmed;
|
|
213
|
+
}
|
|
214
|
+
if (process.platform === "win32") {
|
|
215
|
+
const match = trimmed.match(/^\/([A-Za-z])\/(.*)$/);
|
|
216
|
+
if (match) {
|
|
217
|
+
return `${match[1].toUpperCase()}:\\${match[2].replace(/\//g, "\\")}`;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return trimmed;
|
|
133
221
|
}
|
|
134
222
|
|
|
135
223
|
// src/state.ts
|
|
@@ -140,10 +228,10 @@ function getStateDir(repoRoot) {
|
|
|
140
228
|
return config.stateDir;
|
|
141
229
|
}
|
|
142
230
|
function getStatePath(repoRoot) {
|
|
143
|
-
return
|
|
231
|
+
return path3.join(getStateDir(repoRoot), STATE_FILE);
|
|
144
232
|
}
|
|
145
233
|
function stateExists(repoRoot) {
|
|
146
|
-
return
|
|
234
|
+
return fs3.existsSync(getStatePath(repoRoot));
|
|
147
235
|
}
|
|
148
236
|
function migrateStateV1toV2(v1) {
|
|
149
237
|
const instances = {};
|
|
@@ -171,8 +259,8 @@ function migrateStateV1toV2(v1) {
|
|
|
171
259
|
}
|
|
172
260
|
function loadState(repoRoot) {
|
|
173
261
|
const statePath = getStatePath(repoRoot);
|
|
174
|
-
if (!
|
|
175
|
-
const raw =
|
|
262
|
+
if (!fs3.existsSync(statePath)) return null;
|
|
263
|
+
const raw = fs3.readFileSync(statePath, "utf-8");
|
|
176
264
|
const parsed = JSON.parse(raw);
|
|
177
265
|
if (parsed.schemaVersion === 1 || parsed.runtimes) {
|
|
178
266
|
const migrated = migrateStateV1toV2(parsed);
|
|
@@ -183,11 +271,11 @@ function loadState(repoRoot) {
|
|
|
183
271
|
}
|
|
184
272
|
function saveState(repoRoot, state) {
|
|
185
273
|
const stateDir = getStateDir(repoRoot);
|
|
186
|
-
|
|
274
|
+
fs3.mkdirSync(stateDir, { recursive: true });
|
|
187
275
|
const statePath = getStatePath(repoRoot);
|
|
188
276
|
const tmp = `${statePath}.tmp.${process.pid}`;
|
|
189
|
-
|
|
190
|
-
|
|
277
|
+
fs3.writeFileSync(tmp, JSON.stringify(state, null, 2), "utf-8");
|
|
278
|
+
fs3.renameSync(tmp, statePath);
|
|
191
279
|
}
|
|
192
280
|
function createInitialState(commsDir, repoRoot, packageVersion) {
|
|
193
281
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -195,30 +283,49 @@ function createInitialState(commsDir, repoRoot, packageVersion) {
|
|
|
195
283
|
schemaVersion: SCHEMA_VERSION,
|
|
196
284
|
createdAt: now,
|
|
197
285
|
updatedAt: now,
|
|
198
|
-
commsDir:
|
|
199
|
-
repoRoot:
|
|
286
|
+
commsDir: path3.resolve(commsDir),
|
|
287
|
+
repoRoot: path3.resolve(repoRoot),
|
|
200
288
|
packageVersion,
|
|
201
289
|
instances: {}
|
|
202
290
|
};
|
|
203
291
|
}
|
|
204
292
|
|
|
205
293
|
// src/version.ts
|
|
206
|
-
var version = "0.1.0";
|
|
207
|
-
|
|
208
|
-
// src/engine/bridge.ts
|
|
209
294
|
import * as fs4 from "fs";
|
|
210
295
|
import * as path4 from "path";
|
|
211
|
-
import {
|
|
296
|
+
import { fileURLToPath } from "url";
|
|
297
|
+
var FALLBACK_VERSION = "0.0.0";
|
|
298
|
+
function resolvePackageVersion(metaUrl = import.meta.url) {
|
|
299
|
+
const moduleDir = path4.dirname(fileURLToPath(metaUrl));
|
|
300
|
+
const packageJsonPath = path4.join(moduleDir, "..", "package.json");
|
|
301
|
+
try {
|
|
302
|
+
const parsed = JSON.parse(fs4.readFileSync(packageJsonPath, "utf-8"));
|
|
303
|
+
if (typeof parsed.version === "string" && parsed.version.trim()) {
|
|
304
|
+
return parsed.version;
|
|
305
|
+
}
|
|
306
|
+
} catch {
|
|
307
|
+
}
|
|
308
|
+
return FALLBACK_VERSION;
|
|
309
|
+
}
|
|
310
|
+
var version = resolvePackageVersion();
|
|
311
|
+
|
|
312
|
+
// src/engine/bridge.ts
|
|
313
|
+
import * as fs6 from "fs";
|
|
314
|
+
import * as net from "net";
|
|
315
|
+
import * as path6 from "path";
|
|
316
|
+
import { randomBytes } from "crypto";
|
|
317
|
+
import { spawn, spawnSync, execSync as execSync2 } from "child_process";
|
|
318
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
212
319
|
|
|
213
320
|
// src/runtime/resolve-node.ts
|
|
214
|
-
import * as
|
|
215
|
-
import * as
|
|
321
|
+
import * as fs5 from "fs";
|
|
322
|
+
import * as path5 from "path";
|
|
216
323
|
import { execSync } from "child_process";
|
|
217
324
|
function readNodeVersion(repoRoot) {
|
|
218
|
-
const nvFile =
|
|
219
|
-
if (!
|
|
325
|
+
const nvFile = path5.join(repoRoot, ".node-version");
|
|
326
|
+
if (!fs5.existsSync(nvFile)) return null;
|
|
220
327
|
try {
|
|
221
|
-
const raw =
|
|
328
|
+
const raw = fs5.readFileSync(nvFile, "utf-8").trim();
|
|
222
329
|
return raw.length > 0 ? raw.replace(/^v/, "") : null;
|
|
223
330
|
} catch {
|
|
224
331
|
return null;
|
|
@@ -228,16 +335,16 @@ function fnmCandidateDirs() {
|
|
|
228
335
|
if (process.platform === "win32") {
|
|
229
336
|
return [
|
|
230
337
|
process.env.FNM_DIR,
|
|
231
|
-
process.env.APPDATA ?
|
|
232
|
-
process.env.LOCALAPPDATA ?
|
|
233
|
-
process.env.USERPROFILE ?
|
|
338
|
+
process.env.APPDATA ? path5.join(process.env.APPDATA, "fnm") : null,
|
|
339
|
+
process.env.LOCALAPPDATA ? path5.join(process.env.LOCALAPPDATA, "fnm") : null,
|
|
340
|
+
process.env.USERPROFILE ? path5.join(process.env.USERPROFILE, "scoop", "persist", "fnm") : null
|
|
234
341
|
].filter(Boolean);
|
|
235
342
|
}
|
|
236
343
|
return [
|
|
237
344
|
process.env.FNM_DIR,
|
|
238
|
-
process.env.HOME ?
|
|
239
|
-
process.env.HOME ?
|
|
240
|
-
process.env.XDG_DATA_HOME ?
|
|
345
|
+
process.env.HOME ? path5.join(process.env.HOME, ".local", "share", "fnm") : null,
|
|
346
|
+
process.env.HOME ? path5.join(process.env.HOME, ".fnm") : null,
|
|
347
|
+
process.env.XDG_DATA_HOME ? path5.join(process.env.XDG_DATA_HOME, "fnm") : null
|
|
241
348
|
].filter(Boolean);
|
|
242
349
|
}
|
|
243
350
|
function nodeExecutableName() {
|
|
@@ -247,14 +354,14 @@ function probeFnmNode(desiredVersion) {
|
|
|
247
354
|
const dirs = fnmCandidateDirs();
|
|
248
355
|
const exe = nodeExecutableName();
|
|
249
356
|
for (const baseDir of dirs) {
|
|
250
|
-
const candidate =
|
|
357
|
+
const candidate = path5.join(
|
|
251
358
|
baseDir,
|
|
252
359
|
"node-versions",
|
|
253
360
|
`v${desiredVersion}`,
|
|
254
361
|
"installation",
|
|
255
362
|
exe
|
|
256
363
|
);
|
|
257
|
-
if (!
|
|
364
|
+
if (!fs5.existsSync(candidate)) continue;
|
|
258
365
|
try {
|
|
259
366
|
const v = execSync(`"${candidate}" --version`, {
|
|
260
367
|
encoding: "utf-8",
|
|
@@ -295,12 +402,12 @@ function checkStripTypesSupport(command) {
|
|
|
295
402
|
}
|
|
296
403
|
function findTsxFallback(repoRoot) {
|
|
297
404
|
const candidates = [
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
405
|
+
path5.join(repoRoot, "node_modules", ".bin", "tsx.exe"),
|
|
406
|
+
path5.join(repoRoot, "node_modules", ".bin", "tsx.CMD"),
|
|
407
|
+
path5.join(repoRoot, "node_modules", ".bin", "tsx")
|
|
301
408
|
];
|
|
302
409
|
for (const c of candidates) {
|
|
303
|
-
if (
|
|
410
|
+
if (fs5.existsSync(c)) return c;
|
|
304
411
|
}
|
|
305
412
|
return null;
|
|
306
413
|
}
|
|
@@ -309,7 +416,7 @@ function getFnmBinDir(repoRoot) {
|
|
|
309
416
|
if (!desiredVersion) return null;
|
|
310
417
|
const nodePath = probeFnmNode(desiredVersion);
|
|
311
418
|
if (!nodePath) return null;
|
|
312
|
-
return
|
|
419
|
+
return path5.dirname(nodePath);
|
|
313
420
|
}
|
|
314
421
|
function resolveNodeRuntime(configCommand, repoRoot) {
|
|
315
422
|
if (configCommand === "bun" || configCommand.endsWith("bun.exe")) {
|
|
@@ -365,19 +472,53 @@ function buildRuntimeEnv(repoRoot, baseEnv = process.env) {
|
|
|
365
472
|
const currentPath = baseEnv[pathKey] ?? baseEnv.PATH ?? "";
|
|
366
473
|
return {
|
|
367
474
|
...baseEnv,
|
|
368
|
-
[pathKey]: `${fnmBin}${
|
|
475
|
+
[pathKey]: `${fnmBin}${path5.delimiter}${currentPath}`
|
|
369
476
|
};
|
|
370
477
|
}
|
|
371
478
|
|
|
372
479
|
// src/engine/bridge.ts
|
|
480
|
+
var APP_SERVER_AUTH_FILE_MODE = 384;
|
|
481
|
+
function writeProtectedTextFile(filePath, content) {
|
|
482
|
+
fs6.mkdirSync(path6.dirname(filePath), { recursive: true });
|
|
483
|
+
const tmp = `${filePath}.tmp.${process.pid}`;
|
|
484
|
+
fs6.writeFileSync(tmp, content, {
|
|
485
|
+
encoding: "utf-8",
|
|
486
|
+
mode: APP_SERVER_AUTH_FILE_MODE
|
|
487
|
+
});
|
|
488
|
+
fs6.chmodSync(tmp, APP_SERVER_AUTH_FILE_MODE);
|
|
489
|
+
fs6.renameSync(tmp, filePath);
|
|
490
|
+
fs6.chmodSync(filePath, APP_SERVER_AUTH_FILE_MODE);
|
|
491
|
+
}
|
|
373
492
|
function pidFilePath(stateDir, instanceId) {
|
|
374
|
-
return
|
|
493
|
+
return path6.join(stateDir, "pids", `bridge-${instanceId}.json`);
|
|
494
|
+
}
|
|
495
|
+
function runtimeHeartbeatFilePath(runtimeStateDir) {
|
|
496
|
+
return path6.join(runtimeStateDir, "heartbeat.json");
|
|
497
|
+
}
|
|
498
|
+
function loadRuntimeHeartbeatTimestamp(runtimeStateDir) {
|
|
499
|
+
if (!runtimeStateDir) {
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
const heartbeatPath = runtimeHeartbeatFilePath(runtimeStateDir);
|
|
503
|
+
if (!fs6.existsSync(heartbeatPath)) {
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
506
|
+
try {
|
|
507
|
+
const raw = fs6.readFileSync(heartbeatPath, "utf-8");
|
|
508
|
+
const parsed = JSON.parse(raw);
|
|
509
|
+
return typeof parsed.updatedAt === "string" ? parsed.updatedAt : null;
|
|
510
|
+
} catch {
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
function resolveHeartbeatTimestamp(state) {
|
|
515
|
+
return loadRuntimeHeartbeatTimestamp(state?.runtimeStateDir) ?? state?.lastHeartbeat ?? null;
|
|
375
516
|
}
|
|
376
517
|
function loadBridgeState(stateDir, instanceId) {
|
|
377
518
|
const pidPath = pidFilePath(stateDir, instanceId);
|
|
378
|
-
if (!
|
|
519
|
+
if (!fs6.existsSync(pidPath)) return null;
|
|
379
520
|
try {
|
|
380
|
-
const raw =
|
|
521
|
+
const raw = fs6.readFileSync(pidPath, "utf-8");
|
|
381
522
|
return JSON.parse(raw);
|
|
382
523
|
} catch {
|
|
383
524
|
return null;
|
|
@@ -385,18 +526,33 @@ function loadBridgeState(stateDir, instanceId) {
|
|
|
385
526
|
}
|
|
386
527
|
function saveBridgeState(stateDir, instanceId, state) {
|
|
387
528
|
const pidPath = pidFilePath(stateDir, instanceId);
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
529
|
+
const serializable = JSON.parse(JSON.stringify(state));
|
|
530
|
+
if (serializable.appServer?.auth) {
|
|
531
|
+
delete serializable.appServer.auth.token;
|
|
532
|
+
}
|
|
533
|
+
writeProtectedTextFile(pidPath, JSON.stringify(serializable, null, 2));
|
|
534
|
+
}
|
|
535
|
+
function clearBridgeState(stateDir, instanceId) {
|
|
536
|
+
const pidPath = pidFilePath(stateDir, instanceId);
|
|
537
|
+
if (fs6.existsSync(pidPath)) {
|
|
538
|
+
fs6.unlinkSync(pidPath);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
function isProcessAlive(pid) {
|
|
542
|
+
try {
|
|
543
|
+
process.kill(pid, 0);
|
|
544
|
+
return true;
|
|
545
|
+
} catch {
|
|
546
|
+
return false;
|
|
547
|
+
}
|
|
392
548
|
}
|
|
393
549
|
function rotateLog(logPath) {
|
|
394
|
-
if (!
|
|
550
|
+
if (!fs6.existsSync(logPath)) return;
|
|
395
551
|
try {
|
|
396
|
-
const stats =
|
|
552
|
+
const stats = fs6.statSync(logPath);
|
|
397
553
|
if (stats.size === 0) return;
|
|
398
554
|
const prevPath = `${logPath}.prev`;
|
|
399
|
-
|
|
555
|
+
fs6.renameSync(logPath, prevPath);
|
|
400
556
|
} catch {
|
|
401
557
|
}
|
|
402
558
|
}
|
|
@@ -409,16 +565,317 @@ function updateBridgeHeartbeat(stateDir, instanceId) {
|
|
|
409
565
|
}
|
|
410
566
|
function getHeartbeatAge(stateDir, instanceId) {
|
|
411
567
|
const state = loadBridgeState(stateDir, instanceId);
|
|
412
|
-
|
|
413
|
-
|
|
568
|
+
const heartbeat = resolveHeartbeatTimestamp(state);
|
|
569
|
+
if (!heartbeat) return null;
|
|
570
|
+
const heartbeatTime = new Date(heartbeat).getTime();
|
|
414
571
|
if (isNaN(heartbeatTime)) return null;
|
|
415
572
|
return Math.floor((Date.now() - heartbeatTime) / 1e3);
|
|
416
573
|
}
|
|
574
|
+
function getBridgeStatus(stateDir, instanceId) {
|
|
575
|
+
const state = loadBridgeState(stateDir, instanceId);
|
|
576
|
+
if (!state) return "stopped";
|
|
577
|
+
if (!isProcessAlive(state.pid)) {
|
|
578
|
+
clearBridgeState(stateDir, instanceId);
|
|
579
|
+
return "stale";
|
|
580
|
+
}
|
|
581
|
+
return "running";
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// src/engine/dashboard.ts
|
|
585
|
+
import * as fs7 from "fs";
|
|
586
|
+
import * as path7 from "path";
|
|
587
|
+
import { execSync as execSync3 } from "child_process";
|
|
588
|
+
function collectAgents(commsDir) {
|
|
589
|
+
const heartbeatsPath = path7.join(commsDir, "heartbeats.json");
|
|
590
|
+
if (!fs7.existsSync(heartbeatsPath)) return [];
|
|
591
|
+
try {
|
|
592
|
+
const raw = fs7.readFileSync(heartbeatsPath, "utf-8");
|
|
593
|
+
const data = JSON.parse(raw);
|
|
594
|
+
return Object.entries(data).map(([name, info]) => ({
|
|
595
|
+
name: info.agent ?? name,
|
|
596
|
+
status: info.status ?? null,
|
|
597
|
+
lastActivity: info.lastActivity ?? info.timestamp ?? null,
|
|
598
|
+
joinedAt: info.joinedAt ?? null
|
|
599
|
+
}));
|
|
600
|
+
} catch {
|
|
601
|
+
return [];
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
function collectBridges(repoRoot) {
|
|
605
|
+
const state = loadState(repoRoot);
|
|
606
|
+
const { config } = resolveConfig({}, repoRoot);
|
|
607
|
+
const stateDir = config.stateDir;
|
|
608
|
+
const bridges = [];
|
|
609
|
+
if (state) {
|
|
610
|
+
for (const [id, inst] of Object.entries(state.instances)) {
|
|
611
|
+
if (!inst?.installed) continue;
|
|
612
|
+
if (inst.bridgeMode !== "app-server") continue;
|
|
613
|
+
const instanceId = id;
|
|
614
|
+
const status = getBridgeStatus(stateDir, instanceId);
|
|
615
|
+
const bridgeState = loadBridgeState(stateDir, instanceId);
|
|
616
|
+
const age = getHeartbeatAge(stateDir, instanceId);
|
|
617
|
+
bridges.push({
|
|
618
|
+
instanceId: id,
|
|
619
|
+
runtime: inst.runtime,
|
|
620
|
+
status,
|
|
621
|
+
pid: bridgeState?.pid ?? null,
|
|
622
|
+
port: inst.port ?? null,
|
|
623
|
+
heartbeatAge: age,
|
|
624
|
+
headless: inst.headless?.enabled ?? false
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
const tmpDir = path7.join(repoRoot, ".tmp");
|
|
629
|
+
if (fs7.existsSync(tmpDir)) {
|
|
630
|
+
try {
|
|
631
|
+
const dirs = fs7.readdirSync(tmpDir).filter((d) => d.startsWith("codex-app-server-bridge"));
|
|
632
|
+
for (const dir of dirs) {
|
|
633
|
+
const daemonPath = path7.join(tmpDir, dir, "bridge-daemon.json");
|
|
634
|
+
if (!fs7.existsSync(daemonPath)) continue;
|
|
635
|
+
try {
|
|
636
|
+
const raw = fs7.readFileSync(daemonPath, "utf-8");
|
|
637
|
+
const daemon = JSON.parse(raw);
|
|
638
|
+
const alreadyCovered = bridges.some(
|
|
639
|
+
(b) => b.pid === daemon.pid && b.pid !== null
|
|
640
|
+
);
|
|
641
|
+
if (alreadyCovered) continue;
|
|
642
|
+
const agentFile = path7.join(tmpDir, dir, "agent-name.txt");
|
|
643
|
+
const agentName = fs7.existsSync(agentFile) ? fs7.readFileSync(agentFile, "utf-8").trim() : dir;
|
|
644
|
+
const running = daemon.pid ? isProcessAlive(daemon.pid) : false;
|
|
645
|
+
const portMatch = daemon.appServerUrl?.match(/:(\d+)/);
|
|
646
|
+
const port = portMatch ? parseInt(portMatch[1], 10) : null;
|
|
647
|
+
bridges.push({
|
|
648
|
+
instanceId: agentName,
|
|
649
|
+
runtime: "codex",
|
|
650
|
+
status: running ? "running" : "stale",
|
|
651
|
+
pid: daemon.pid ?? null,
|
|
652
|
+
port,
|
|
653
|
+
heartbeatAge: null,
|
|
654
|
+
headless: false
|
|
655
|
+
});
|
|
656
|
+
} catch {
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
} catch {
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return bridges;
|
|
663
|
+
}
|
|
664
|
+
function collectPRs() {
|
|
665
|
+
try {
|
|
666
|
+
const output = execSync3(
|
|
667
|
+
"gh pr list --state all --limit 10 --json number,title,author,state,url",
|
|
668
|
+
{ encoding: "utf-8", timeout: 1e4, stdio: ["pipe", "pipe", "pipe"] }
|
|
669
|
+
);
|
|
670
|
+
const prs = JSON.parse(output);
|
|
671
|
+
return prs.map((pr) => ({
|
|
672
|
+
number: pr.number,
|
|
673
|
+
title: pr.title,
|
|
674
|
+
author: pr.author.login,
|
|
675
|
+
state: pr.state,
|
|
676
|
+
url: pr.url
|
|
677
|
+
}));
|
|
678
|
+
} catch {
|
|
679
|
+
return [];
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
function collectWarnings(bridges, agents) {
|
|
683
|
+
const warnings = [];
|
|
684
|
+
for (const bridge of bridges) {
|
|
685
|
+
if (bridge.status === "stale") {
|
|
686
|
+
warnings.push({
|
|
687
|
+
level: "warn",
|
|
688
|
+
message: `Bridge ${bridge.instanceId} is stale (PID ${bridge.pid} dead)`
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
if (bridge.status === "running" && bridge.heartbeatAge !== null && bridge.heartbeatAge > 60) {
|
|
692
|
+
warnings.push({
|
|
693
|
+
level: "warn",
|
|
694
|
+
message: `Bridge ${bridge.instanceId} heartbeat stale (${bridge.heartbeatAge}s ago)`
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
if (bridges.length === 0) {
|
|
699
|
+
warnings.push({
|
|
700
|
+
level: "warn",
|
|
701
|
+
message: "No bridges configured"
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
if (agents.length === 0) {
|
|
705
|
+
warnings.push({
|
|
706
|
+
level: "warn",
|
|
707
|
+
message: "No agent heartbeats found"
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
return warnings;
|
|
711
|
+
}
|
|
712
|
+
function collectDashboardSnapshot(repoRoot, commsDirOverride) {
|
|
713
|
+
const { config } = resolveConfig(
|
|
714
|
+
commsDirOverride ? { commsDir: commsDirOverride } : {},
|
|
715
|
+
repoRoot
|
|
716
|
+
);
|
|
717
|
+
const resolved = config;
|
|
718
|
+
const agents = collectAgents(resolved.commsDir);
|
|
719
|
+
const bridges = collectBridges(resolved.repoRoot);
|
|
720
|
+
const prs = collectPRs();
|
|
721
|
+
const warnings = collectWarnings(bridges, agents);
|
|
722
|
+
return {
|
|
723
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
724
|
+
repoRoot: resolved.repoRoot,
|
|
725
|
+
commsDir: resolved.commsDir,
|
|
726
|
+
agents,
|
|
727
|
+
bridges,
|
|
728
|
+
prs,
|
|
729
|
+
warnings
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// src/api/state.ts
|
|
734
|
+
function getDashboardSnapshot(options) {
|
|
735
|
+
const repoRoot = options?.repoRoot ?? findRepoRoot();
|
|
736
|
+
return collectDashboardSnapshot(repoRoot, options?.commsDir);
|
|
737
|
+
}
|
|
738
|
+
async function* streamEvents(options) {
|
|
739
|
+
const intervalMs = options?.intervalMs ?? 2e3;
|
|
740
|
+
const repoRoot = options?.repoRoot ?? findRepoRoot();
|
|
741
|
+
while (!options?.signal?.aborted) {
|
|
742
|
+
yield collectDashboardSnapshot(repoRoot, options?.commsDir);
|
|
743
|
+
await new Promise((resolve5) => {
|
|
744
|
+
const onAbort = () => {
|
|
745
|
+
clearTimeout(timer);
|
|
746
|
+
resolve5();
|
|
747
|
+
};
|
|
748
|
+
const timer = setTimeout(() => {
|
|
749
|
+
options?.signal?.removeEventListener("abort", onAbort);
|
|
750
|
+
resolve5();
|
|
751
|
+
}, intervalMs);
|
|
752
|
+
options?.signal?.addEventListener("abort", onAbort, { once: true });
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
function getConfig(options) {
|
|
757
|
+
const repoRoot = options?.repoRoot ?? findRepoRoot();
|
|
758
|
+
const { config } = resolveConfig({}, repoRoot);
|
|
759
|
+
return {
|
|
760
|
+
repoRoot,
|
|
761
|
+
commsDir: options?.commsDir ?? config.commsDir,
|
|
762
|
+
stateDir: config.stateDir,
|
|
763
|
+
appServerUrl: config.appServerUrl
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// src/api/http.ts
|
|
768
|
+
import {
|
|
769
|
+
createServer as createServer2
|
|
770
|
+
} from "http";
|
|
771
|
+
var CORS_HEADERS = {
|
|
772
|
+
"Access-Control-Allow-Origin": "http://localhost:3000",
|
|
773
|
+
"Access-Control-Allow-Methods": "GET, OPTIONS",
|
|
774
|
+
"Access-Control-Allow-Headers": "Content-Type"
|
|
775
|
+
};
|
|
776
|
+
function jsonResponse(res, data, status = 200) {
|
|
777
|
+
res.writeHead(status, {
|
|
778
|
+
"Content-Type": "application/json",
|
|
779
|
+
...CORS_HEADERS
|
|
780
|
+
});
|
|
781
|
+
res.end(JSON.stringify(data));
|
|
782
|
+
}
|
|
783
|
+
function handleSnapshot(res, apiOptions) {
|
|
784
|
+
const snapshot = getDashboardSnapshot(apiOptions);
|
|
785
|
+
jsonResponse(res, snapshot);
|
|
786
|
+
}
|
|
787
|
+
function handleConfig(res, apiOptions) {
|
|
788
|
+
const config = getConfig(apiOptions);
|
|
789
|
+
jsonResponse(res, config);
|
|
790
|
+
}
|
|
791
|
+
async function handleEvents(req, res, apiOptions) {
|
|
792
|
+
res.writeHead(200, {
|
|
793
|
+
"Content-Type": "text/event-stream",
|
|
794
|
+
"Cache-Control": "no-cache",
|
|
795
|
+
Connection: "keep-alive",
|
|
796
|
+
...CORS_HEADERS
|
|
797
|
+
});
|
|
798
|
+
const controller = new AbortController();
|
|
799
|
+
req.on("close", () => controller.abort());
|
|
800
|
+
for await (const snapshot of streamEvents({
|
|
801
|
+
...apiOptions,
|
|
802
|
+
signal: controller.signal
|
|
803
|
+
})) {
|
|
804
|
+
if (controller.signal.aborted) break;
|
|
805
|
+
res.write(`data: ${JSON.stringify(snapshot)}
|
|
806
|
+
|
|
807
|
+
`);
|
|
808
|
+
}
|
|
809
|
+
res.end();
|
|
810
|
+
}
|
|
811
|
+
function handleHealth(res) {
|
|
812
|
+
jsonResponse(res, { ok: true, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
813
|
+
}
|
|
814
|
+
async function startHttpServer(options) {
|
|
815
|
+
const port = options?.port ?? 4580;
|
|
816
|
+
const host = "127.0.0.1";
|
|
817
|
+
const apiOptions = {
|
|
818
|
+
repoRoot: options?.repoRoot,
|
|
819
|
+
commsDir: options?.commsDir
|
|
820
|
+
};
|
|
821
|
+
const server = createServer2(
|
|
822
|
+
async (req, res) => {
|
|
823
|
+
const url = new URL(req.url ?? "/", `http://${host}:${port}`);
|
|
824
|
+
const pathname = url.pathname;
|
|
825
|
+
if (req.method === "OPTIONS") {
|
|
826
|
+
res.writeHead(204, CORS_HEADERS);
|
|
827
|
+
res.end();
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
if (req.method !== "GET") {
|
|
831
|
+
jsonResponse(res, { error: "Method not allowed" }, 405);
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
try {
|
|
835
|
+
switch (pathname) {
|
|
836
|
+
case "/api/snapshot":
|
|
837
|
+
handleSnapshot(res, apiOptions);
|
|
838
|
+
break;
|
|
839
|
+
case "/api/events":
|
|
840
|
+
await handleEvents(req, res, apiOptions);
|
|
841
|
+
break;
|
|
842
|
+
case "/api/config":
|
|
843
|
+
handleConfig(res, apiOptions);
|
|
844
|
+
break;
|
|
845
|
+
case "/health":
|
|
846
|
+
handleHealth(res);
|
|
847
|
+
break;
|
|
848
|
+
default:
|
|
849
|
+
jsonResponse(res, { error: "Not found" }, 404);
|
|
850
|
+
}
|
|
851
|
+
} catch (err) {
|
|
852
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
853
|
+
jsonResponse(res, { error: message }, 500);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
);
|
|
857
|
+
await new Promise((resolve5, reject) => {
|
|
858
|
+
server.once("error", reject);
|
|
859
|
+
server.listen(port, host, () => {
|
|
860
|
+
server.removeListener("error", reject);
|
|
861
|
+
resolve5();
|
|
862
|
+
});
|
|
863
|
+
});
|
|
864
|
+
return {
|
|
865
|
+
port,
|
|
866
|
+
close: () => new Promise((resolve5, reject) => {
|
|
867
|
+
server.close((err) => err ? reject(err) : resolve5());
|
|
868
|
+
})
|
|
869
|
+
};
|
|
870
|
+
}
|
|
417
871
|
export {
|
|
418
872
|
LOCAL_CONFIG_FILE,
|
|
419
873
|
SHARED_CONFIG_FILE,
|
|
420
874
|
buildRuntimeEnv,
|
|
875
|
+
collectDashboardSnapshot,
|
|
421
876
|
createInitialState,
|
|
877
|
+
getConfig,
|
|
878
|
+
getDashboardSnapshot,
|
|
422
879
|
getFnmBinDir,
|
|
423
880
|
getHeartbeatAge,
|
|
424
881
|
loadLocalConfig,
|
|
@@ -432,7 +889,9 @@ export {
|
|
|
432
889
|
saveLocalConfig,
|
|
433
890
|
saveSharedConfig,
|
|
434
891
|
saveState,
|
|
892
|
+
startHttpServer,
|
|
435
893
|
stateExists,
|
|
894
|
+
streamEvents,
|
|
436
895
|
updateBridgeHeartbeat,
|
|
437
896
|
version
|
|
438
897
|
};
|