@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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. 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.0",
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
- for (const [name, s] of Object.entries(raw?.services || {})) {
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
- app: { cwd: ".", run: `${pm} run ${script}`, readyTimeoutMs: 90_000 },
116
+ [name]: { cwd: ".", run: `${pm} run ${script}`, readyTimeoutMs: 90_000 },
102
117
  },
103
- preview: "app",
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 Object.values(config.services)) {
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 Object.values(config.services)) {
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 = config.preview ? out.find((s) => s.name === config.preview) : null;
292
+ const previewSvc = previewName ? out.find((s) => s.name === previewName) : null;
258
293
  const lan = lanAddress();
259
294
  return {
260
295
  services: out,