@gleapai/kai-bridge 0.2.0 → 0.2.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/package.json +1 -1
- package/src/preview.mjs +42 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gleapai/kai-bridge",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Run Gleap Kai Code sessions on your own machine with your own Claude Code / Codex login — and preview your real dev servers from the dashboard or the phone.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/preview.mjs
CHANGED
|
@@ -24,7 +24,7 @@ import { spawn } from "node:child_process";
|
|
|
24
24
|
import { createServer, connect } from "node:net";
|
|
25
25
|
import { existsSync, mkdirSync, openSync, readFileSync } from "node:fs";
|
|
26
26
|
import { networkInterfaces } from "node:os";
|
|
27
|
-
import { join, resolve } from "node:path";
|
|
27
|
+
import { join, resolve, basename } from "node:path";
|
|
28
28
|
import YAML from "yaml";
|
|
29
29
|
|
|
30
30
|
export const DEV_CONFIG_PATHS = [".gleap/dev.yaml", ".gleap/dev.yml"];
|
|
@@ -45,7 +45,14 @@ export function readDevConfig(repoRoot) {
|
|
|
45
45
|
|
|
46
46
|
export function normalizeDevConfig(raw) {
|
|
47
47
|
const services = {};
|
|
48
|
-
|
|
48
|
+
// Accept both shapes: `services: {api: {...}}` and the list form
|
|
49
|
+
// `services: [- name: api ...]` people naturally write in YAML —
|
|
50
|
+
// Object.entries on an array yields index keys, which used to name
|
|
51
|
+
// services "0", "1".
|
|
52
|
+
const rawServices = Array.isArray(raw?.services)
|
|
53
|
+
? Object.fromEntries(raw.services.filter((s) => s && typeof s === "object").map((s, i) => [String(s.name || i), s]))
|
|
54
|
+
: raw?.services || {};
|
|
55
|
+
for (const [name, s] of Object.entries(rawServices)) {
|
|
49
56
|
if (!s || typeof s !== "object" || !s.run) continue;
|
|
50
57
|
services[name] = {
|
|
51
58
|
name,
|
|
@@ -94,13 +101,21 @@ export function detectDevConfig(repoRoot) {
|
|
|
94
101
|
: existsSync(join(repoRoot, "yarn.lock"))
|
|
95
102
|
? "yarn"
|
|
96
103
|
: "npm";
|
|
104
|
+
// Name the service after the package (else the repo folder):
|
|
105
|
+
// heuristic configs from two repos in one session used to both be
|
|
106
|
+
// called "app" and collide in the shared per-session port map — the
|
|
107
|
+
// second repo silently reused the first one's port and never booted.
|
|
108
|
+
// package.json name beats the folder because WORKTREE folders are
|
|
109
|
+
// named after the session slug — identical for every repo in it.
|
|
110
|
+
const slug = (v) => String(v || "").toLowerCase().replace(/^@[^/]+\//, "").replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
111
|
+
const name = slug(pkg.name) || slug(basename(repoRoot)) || "app";
|
|
97
112
|
return normalizeDevConfig({
|
|
98
113
|
services: {
|
|
99
114
|
// No declared port: ServiceRunner assigns a free one and exports
|
|
100
115
|
// PORT; readiness falls back to the port-listen probe.
|
|
101
|
-
|
|
116
|
+
[name]: { cwd: ".", run: `${pm} run ${script}`, readyTimeoutMs: 90_000 },
|
|
102
117
|
},
|
|
103
|
-
preview:
|
|
118
|
+
preview: name,
|
|
104
119
|
});
|
|
105
120
|
}
|
|
106
121
|
|
|
@@ -193,8 +208,28 @@ export class ServiceRunner {
|
|
|
193
208
|
async start(repoRoot, config, { mode = "worktree" } = {}) {
|
|
194
209
|
mkdirSync(this.logDir, { recursive: true });
|
|
195
210
|
const out = [];
|
|
211
|
+
// Same service name from a DIFFERENT repo is a collision, not a
|
|
212
|
+
// reuse: silently sharing the port map entry made the second repo
|
|
213
|
+
// ride the first one's port and never boot. Uniquify with the repo
|
|
214
|
+
// folder as prefix and say so — committed configs should pick
|
|
215
|
+
// collision-safe names, but the runtime must not break when they
|
|
216
|
+
// don't.
|
|
217
|
+
this.serviceRoots ??= new Map();
|
|
218
|
+
const services = Object.values(config.services).map((svc) => {
|
|
219
|
+
const owner = this.serviceRoots.get(svc.name);
|
|
220
|
+
if (owner && owner !== repoRoot) {
|
|
221
|
+
const unique = `${basename(repoRoot).toLowerCase().replace(/[^a-z0-9_-]+/g, "-")}-${svc.name}`;
|
|
222
|
+
this.onStatus(`Service name "${svc.name}" is already used by another repo in this session — running this one as "${unique}".`);
|
|
223
|
+
svc = { ...svc, name: unique };
|
|
224
|
+
}
|
|
225
|
+
this.serviceRoots.set(svc.name, repoRoot);
|
|
226
|
+
return svc;
|
|
227
|
+
});
|
|
228
|
+
const previewName = config.preview && !services.some((s) => s.name === config.preview)
|
|
229
|
+
? services.find((s) => s.name.endsWith(`-${config.preview}`))?.name ?? config.preview
|
|
230
|
+
: config.preview;
|
|
196
231
|
// Assign ports first so cross-references resolve.
|
|
197
|
-
for (const svc of
|
|
232
|
+
for (const svc of services) {
|
|
198
233
|
if (this.ports[svc.name]) continue;
|
|
199
234
|
const declared = svc.port;
|
|
200
235
|
if (mode === "local" && declared && (await isPortListening(declared))) {
|
|
@@ -206,7 +241,7 @@ export class ServiceRunner {
|
|
|
206
241
|
this.ports[svc.name] = await getFreePort();
|
|
207
242
|
}
|
|
208
243
|
}
|
|
209
|
-
for (const svc of
|
|
244
|
+
for (const svc of services) {
|
|
210
245
|
const port = this.ports[svc.name];
|
|
211
246
|
const logPath = join(this.logDir, `${svc.name}.log`);
|
|
212
247
|
if (this.adopted.has(svc.name)) {
|
|
@@ -254,7 +289,7 @@ export class ServiceRunner {
|
|
|
254
289
|
this.onStatus(ready ? `${svc.name} ready on http://localhost:${port}` : `${svc.name} did not become ready within ${Math.round(svc.readyTimeoutMs / 1000)}s — see ${logPath}`);
|
|
255
290
|
out.push({ name: svc.name, port, url: `http://localhost:${port}`, logPath, adopted: false, ready });
|
|
256
291
|
}
|
|
257
|
-
const previewSvc =
|
|
292
|
+
const previewSvc = previewName ? out.find((s) => s.name === previewName) : null;
|
|
258
293
|
const lan = lanAddress();
|
|
259
294
|
return {
|
|
260
295
|
services: out,
|