@netnodeag/kraftwerk 0.39.0 → 0.39.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 (24) hide show
  1. package/dist/inspector/chat/acp.js +6 -0
  2. package/dist/inspector/chat/sessions.d.ts +14 -0
  3. package/dist/inspector/chat/sessions.js +44 -10
  4. package/dist/inspector/server.js +7 -1
  5. package/dist/inspector/settings.js +8 -1
  6. package/dist/inspector/vibeables.d.ts +6 -5
  7. package/dist/inspector/vibeables.js +131 -12
  8. package/inspector/dist/assets/{dist-Dd-u7fXY.js → dist-BAHymHVh.js} +1 -1
  9. package/inspector/dist/assets/{dist-CI-2-8Tz.js → dist-BUzvZfC2.js} +1 -1
  10. package/inspector/dist/assets/{dist-BECiZEqH.js → dist-CMK0Dv1Q.js} +1 -1
  11. package/inspector/dist/assets/dist-CWxCXHGC.js +1 -0
  12. package/inspector/dist/assets/{dist-DTW4FhJ_.js → dist-Cn3OeZ9x.js} +1 -1
  13. package/inspector/dist/assets/{dist-3Nzkrylv.js → dist-D01HLRsD.js} +1 -1
  14. package/inspector/dist/assets/{dist-D3tgdU3w.js → dist-D0WPeXAf.js} +1 -1
  15. package/inspector/dist/assets/{dist-DXIk6Ebu.js → dist-DQujkoko.js} +1 -1
  16. package/inspector/dist/assets/{dist-nc_mc1w0.js → dist-DYqdkWRb.js} +1 -1
  17. package/inspector/dist/assets/{dist-DwyxsFyN.js → dist-DfmPNdMI.js} +1 -1
  18. package/inspector/dist/assets/{dist-BCM6ehYa.js → dist-FnKl1oDp.js} +1 -1
  19. package/inspector/dist/assets/{editor-6yCJBeDg.js → editor-CVAfyi-9.js} +3 -3
  20. package/inspector/dist/assets/{index-DurTvcNI.js → index-Ba-uGp4N.js} +18 -18
  21. package/inspector/dist/assets/{index-Dom87GNV.css → index-CL7d8WY1.css} +1 -1
  22. package/inspector/dist/index.html +2 -2
  23. package/package.json +1 -1
  24. package/inspector/dist/assets/dist-BbhR2mEU.js +0 -1
@@ -59,6 +59,12 @@ export async function startAcpBackend(agent, cwd, hooks, tuning = {}) {
59
59
  stderr = stderr.slice(-20_000);
60
60
  });
61
61
  let dead = false;
62
+ // Without a listener a failed spawn (cwd gone, node missing) is an
63
+ // unhandled 'error' event that takes the whole inspector down.
64
+ child.on("error", (err) => {
65
+ dead = true;
66
+ hooks.emit({ type: "error", message: `could not start the ${agent} agent: ${err.message}` });
67
+ });
62
68
  child.on("close", (code) => {
63
69
  dead = true;
64
70
  if (code !== 0 && code !== null) {
@@ -1,6 +1,20 @@
1
1
  import type { ChatAgentId, ChatMeta, ChatScope, StoredChatEvent } from "./types.js";
2
2
  /** Server shutdown: no agent subprocess may outlive the inspector. */
3
3
  export declare function disposeAllBackends(): void;
4
+ /**
5
+ * "## Vibeable" block: the chat has an app folder open in the preview pane.
6
+ * The agent works inside that folder, and everything it saves is what the
7
+ * user sees next — so the block spells out how the preview runs and what
8
+ * the sandbox rules out.
9
+ */
10
+ export declare function vibeableContext(slug: string, cwd: string): Promise<string>;
11
+ /**
12
+ * The folder the agent is actually spawned in. A vibeable that was removed
13
+ * leaves meta.cwd pointing nowhere; spawning there fails, so fall back to
14
+ * the scope's default while keeping meta.vibeable — the context then tells
15
+ * the agent the folder is gone instead of pretending it is there.
16
+ */
17
+ export declare function effectiveCwd(meta: ChatMeta): Promise<string>;
4
18
  export declare function createChat(opts: {
5
19
  agent: ChatAgentId;
6
20
  scope: ChatScope;
@@ -175,9 +175,21 @@ async function reposContext() {
175
175
  * user sees next — so the block spells out how the preview runs and what
176
176
  * the sandbox rules out.
177
177
  */
178
- async function vibeableContext(slug) {
179
- const st = await vibeableStatus(slug).catch(() => null);
178
+ export async function vibeableContext(slug, cwd) {
179
+ let st = null;
180
+ let failure = "";
181
+ try {
182
+ st = await vibeableStatus(slug);
183
+ }
184
+ catch (err) {
185
+ failure = err.message;
186
+ }
180
187
  if (!st) {
188
+ if (/are off/.test(failure)) {
189
+ return (`## Vibeable: ${slug}\nThis chat had the vibeable "${slug}" open, but vibeables have since been switched off in this ` +
190
+ `workspace's settings. Its folder still exists at ${cwd} and is your working directory; there is no preview pane any ` +
191
+ `more. Work in the folder if the user asks, and mention that the feature is off.`);
192
+ }
181
193
  return `## Vibeable\nThis chat had the vibeable "${slug}" open in its preview pane, but its folder is gone. Tell the user; do not recreate it unasked.`;
182
194
  }
183
195
  const dev = st.dev;
@@ -195,11 +207,12 @@ async function vibeableContext(slug) {
195
207
  `How the preview runs:\n` +
196
208
  `- Static mode (default): the inspector serves the folder directly at /vibeables/${slug}/ — index.html is the entry; plain HTML, CSS and ` +
197
209
  `JavaScript with no build step. Use relative URLs (./app.js, not /app.js). ES modules and CDN imports (https://esm.sh/<pkg>) work. ` +
198
- `The static preview is a sandboxed document: no localStorage, no cookies, no calls into the inspector's own /api. Keep state in memory, ` +
199
- `or move to dev mode when it must persist.\n` +
210
+ `The preview is a sandboxed document in both modes: no localStorage, no cookies, no calls into the inspector's own /api. Keep state in ` +
211
+ `memory, or move to dev mode and persist through your own server.\n` +
200
212
  `- Dev mode: when the app needs a build tool, a backend or a database, write ${VIBEABLE_CONFIG_FILE} with \`dev: <command>\` ` +
201
213
  `(e.g. \`npm run dev\`, \`node server.js\`, \`python3 -m http.server $PORT\`). kraftwerk starts it inside the folder with PORT in the ` +
202
- `environment and the pane embeds that server; the command must listen on $PORT (or declare \`port:\` when it cannot). Optional ` +
214
+ `environment and the pane reaches it through /vibeables/${slug}/ on the inspector (BASE_PATH holds that prefix — a dev server that ` +
215
+ `emits absolute URLs, like Vite, must use it as its base); the command must listen on $PORT (or declare \`port:\` when it cannot). Optional ` +
203
216
  `\`dir:\` picks the folder for static mode (e.g. dist). The app brings its own backend and storage — a small node server writing JSON ` +
204
217
  `files, SQLite, an external API — nothing of that is provided by kraftwerk.\n\n` +
205
218
  `Rules: the folder is part of this workspace and versioned with it — do not run git yourself, the user reviews and commits from the ` +
@@ -346,7 +359,7 @@ async function scopeContext(meta) {
346
359
  const [base, repos, vibe, skills] = await Promise.all([
347
360
  baseScopeContext(scope, agent),
348
361
  scope.kind === "agent" || scope.kind === "kraftwerk" ? reposContext() : Promise.resolve(""),
349
- meta.vibeable ? vibeableContext(meta.vibeable) : Promise.resolve(""),
362
+ meta.vibeable ? vibeableContext(meta.vibeable, meta.cwd) : Promise.resolve(""),
350
363
  availableSkills(scope),
351
364
  ]);
352
365
  return [base, repos, vibe, RENDERING_BLOCK, skillsBlock(skills)].filter(Boolean).join("\n\n");
@@ -406,7 +419,8 @@ async function ensureBackend(state) {
406
419
  });
407
420
  },
408
421
  };
409
- const { agent, cwd } = state.meta;
422
+ const { agent } = state.meta;
423
+ const cwd = await effectiveCwd(state.meta);
410
424
  const tuning = await backendTuning(state.meta);
411
425
  state.backend =
412
426
  agent === "pi"
@@ -420,6 +434,18 @@ async function ensureBackend(state) {
420
434
  /* ---------- public API (used by server.ts) ---------- */
421
435
  /** Where a chat's agent runs when no vibeable is open: the run folder for run chats, else the project root. */
422
436
  const defaultCwd = (scope) => (scope.kind === "run" ? safeRunDir(scope.runId) : getProjectRoot());
437
+ /**
438
+ * The folder the agent is actually spawned in. A vibeable that was removed
439
+ * leaves meta.cwd pointing nowhere; spawning there fails, so fall back to
440
+ * the scope's default while keeping meta.vibeable — the context then tells
441
+ * the agent the folder is gone instead of pretending it is there.
442
+ */
443
+ export async function effectiveCwd(meta) {
444
+ const st = await fs.stat(meta.cwd).catch(() => null);
445
+ if (st?.isDirectory())
446
+ return meta.cwd;
447
+ return defaultCwd(meta.scope);
448
+ }
423
449
  export async function createChat(opts) {
424
450
  const cwd = defaultCwd(opts.scope);
425
451
  const now = new Date().toISOString();
@@ -513,10 +539,11 @@ export async function setChatVibeable(id, slug) {
513
539
  const state = await loadState(id);
514
540
  if (!state)
515
541
  return { error: "not found", status: 404 };
542
+ const busyError = { error: "agent is still working — wait for the turn to finish", status: 409 };
516
543
  if (state.busy)
517
- return { error: "agent is still working — wait for the turn to finish", status: 409 };
544
+ return busyError;
545
+ let dir = null;
518
546
  if (slug) {
519
- let dir;
520
547
  try {
521
548
  dir = (await resolveVibeable(slug)).dir;
522
549
  }
@@ -524,6 +551,13 @@ export async function setChatVibeable(id, slug) {
524
551
  const msg = err.message;
525
552
  return { error: msg, status: /^no vibeable/.test(msg) ? 404 : /are off/.test(msg) ? 409 : 400 };
526
553
  }
554
+ }
555
+ // A message may have started a turn during the await above; its backend
556
+ // was spawned in the old cwd, so refuse rather than relabel it. From here
557
+ // on nothing yields until cwd, vibeable and backend agree.
558
+ if (state.busy)
559
+ return busyError;
560
+ if (slug && dir) {
527
561
  state.meta.vibeable = slug;
528
562
  state.meta.cwd = dir;
529
563
  }
@@ -532,9 +566,9 @@ export async function setChatVibeable(id, slug) {
532
566
  state.meta.cwd = defaultCwd(state.meta.scope);
533
567
  }
534
568
  state.meta.updatedAt = new Date().toISOString();
535
- await writeMeta(state.meta);
536
569
  dropBackend(state);
537
570
  state.needsContext = true;
571
+ await writeMeta(state.meta);
538
572
  return { meta: state.meta };
539
573
  }
540
574
  export async function resolvePermission(id, requestId, optionId) {
@@ -14,7 +14,7 @@ import { discoverWorkspaces, forgetProject, listWorkspacesDetailed, markProjectS
14
14
  import { gitCommit, gitDiff, gitFetch, gitPull, gitPush, gitStatus, startGitSync, } from "./git.js";
15
15
  import { getSettings, saveSettings } from "./settings.js";
16
16
  import { addRepo, listRepos, openRepos, removeRepo, updateRepo } from "./repos.js";
17
- import { createVibeable, deleteVibeable, disposeAllDevs, listVibeables, openVibeables, resolveVibeable, serveVibeable, startDev, stopDev, subscribeVibeable, vibeableStatus, } from "./vibeables.js";
17
+ import { createVibeable, deleteVibeable, disposeAllDevs, listVibeables, openVibeables, proxyUpgrade, resolveVibeable, serveVibeable, startDev, stopDev, subscribeVibeable, vibeableStatus, } from "./vibeables.js";
18
18
  import { searchAgents } from "./search.js";
19
19
  import { deleteRoutine, routineStatuses, runRoutineNow, saveRoutine, startRoutineScheduler, } from "./routines.js";
20
20
  /**
@@ -955,6 +955,12 @@ export function startInspector(opts) {
955
955
  json(res, { error: err.message }, 500);
956
956
  }
957
957
  });
958
+ // WebSocket upgrades exist for one reason: a vibeable's dev server (HMR).
959
+ server.on("upgrade", (req, socket, head) => {
960
+ if (!hostAllowed(req))
961
+ return void socket.destroy();
962
+ proxyUpgrade(req, socket, head);
963
+ });
958
964
  return new Promise((resolve, reject) => {
959
965
  server.once("error", reject);
960
966
  server.listen(opts.port, INSPECTOR_HOST, () => {
@@ -2,7 +2,7 @@ import { promises as fs } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { parseDocument } from "yaml";
4
4
  import { ignoreEntryFor, REPOS_DEFAULT_ROOT, resolveProject, VIBEABLES_DEFAULT_ROOT } from "../config.js";
5
- import { ensureVibeablesRoot } from "./vibeables.js";
5
+ import { disposeAllDevs, ensureVibeablesRoot } from "./vibeables.js";
6
6
  import { getProjectRoot } from "./context.js";
7
7
  import { ensureReposRoot } from "./repos.js";
8
8
  const GIT_DEFAULTS = { remote: "origin", interval: 300, autosync: "pull" };
@@ -142,6 +142,13 @@ export async function saveSettings(input) {
142
142
  doc.delete("vibeables");
143
143
  else
144
144
  doc.set("vibeables", vibeables);
145
+ // Off, or a different root: running dev servers belong to folders the
146
+ // API can no longer address, so they must not outlive this save.
147
+ const before = project.config.vibeables;
148
+ const rootBefore = before && before.enabled !== false ? before.root ?? VIBEABLES_DEFAULT_ROOT : undefined;
149
+ const rootAfter = vibeables && vibeables.enabled !== false ? vibeables.root ?? VIBEABLES_DEFAULT_ROOT : undefined;
150
+ if (rootBefore !== rootAfter)
151
+ disposeAllDevs();
145
152
  }
146
153
  await fs.writeFile(configPath, doc.toString());
147
154
  // Turning repositories on prepares the folder so the first clone and the
@@ -132,6 +132,12 @@ export declare function vibeableStatus(slug: string): Promise<VibeableStatus>;
132
132
  * trailing slash redirects so the page's relative links resolve.
133
133
  */
134
134
  export declare function serveVibeable(req: http.IncomingMessage, res: http.ServerResponse, url: URL): Promise<void>;
135
+ /**
136
+ * WebSocket upgrades under /vibeables/<slug>/ go to the dev server too — that
137
+ * is how Vite-style HMR reaches the pane. Anything else is closed: the
138
+ * inspector has no sockets of its own.
139
+ */
140
+ export declare function proxyUpgrade(req: http.IncomingMessage, socket: import("node:stream").Duplex, head: Buffer): void;
135
141
  /**
136
142
  * Stream a vibeable's events: file changes (debounced, node_modules and
137
143
  * the like excluded) and dev-server state. The watcher exists while
@@ -139,11 +145,6 @@ export declare function serveVibeable(req: http.IncomingMessage, res: http.Serve
139
145
  * where it cannot watch, the pane still has its manual reload.
140
146
  */
141
147
  export declare function subscribeVibeable(slug: string, dir: string, fn: (ev: VibeableEvent) => void): () => void;
142
- /**
143
- * Start the vibeable's dev command. Idempotent while it runs. The child
144
- * leads its own process group so stopping it also stops what it spawned
145
- * (npm → vite → esbuild).
146
- */
147
148
  export declare function startDev(slug: string): Promise<VibeableStatus>;
148
149
  /** SIGTERM the dev process group, SIGKILL what is still there after 3 s. */
149
150
  export declare function stopDev(slug: string): Promise<VibeableStatus>;
@@ -171,6 +171,7 @@ export async function createVibeable(name) {
171
171
  export async function deleteVibeable(slug) {
172
172
  const r = await resolveVibeable(slug);
173
173
  await stopDev(slug).catch(() => { });
174
+ dropChannel(slug);
174
175
  await fs.rm(r.dir, { recursive: true, force: true });
175
176
  }
176
177
  export async function vibeableStatus(slug) {
@@ -225,6 +226,19 @@ const MIME = {
225
226
  * no storage, and no same-origin calls into the inspector API.
226
227
  */
227
228
  const VIBEABLE_CSP = "sandbox allow-scripts allow-forms allow-popups allow-modals allow-downloads";
229
+ /**
230
+ * Headers every preview response carries. The sandbox makes the document's
231
+ * origin opaque, and an opaque origin fetches module scripts, imports and
232
+ * fetch() in CORS mode — without this allow header the app's own app.js
233
+ * never runs. Allowing any origin is safe: the files are the app's public
234
+ * sources, and the sandbox (not the origin) is what keeps them away from
235
+ * the inspector API.
236
+ */
237
+ const PREVIEW_HEADERS = {
238
+ "cache-control": "no-store",
239
+ "content-security-policy": VIBEABLE_CSP,
240
+ "access-control-allow-origin": "*",
241
+ };
228
242
  /**
229
243
  * GET /vibeables/<slug>/<file>. Dot segments (.env, .git) are never served;
230
244
  * a directory answers with its index.html, and a directory url without the
@@ -252,6 +266,12 @@ export async function serveVibeable(req, res, url) {
252
266
  catch (err) {
253
267
  return fail(404, err.message);
254
268
  }
269
+ // A running dev server owns the whole prefix: the pane and a new tab reach
270
+ // it through the inspector's origin, which is what a container or reverse
271
+ // proxy exposes — a random port on the host is not.
272
+ const dev = devs.get(slug);
273
+ if (dev && dev.exitCode === undefined && dev.child)
274
+ return proxyToDev(req, res, url, slug, dev.port);
255
275
  const rest = seg.slice(1).map((s) => {
256
276
  try {
257
277
  return decodeURIComponent(s);
@@ -285,8 +305,7 @@ export async function serveVibeable(req, res, url) {
285
305
  res.writeHead(200, {
286
306
  "content-type": MIME[ext] ?? "application/octet-stream",
287
307
  "content-length": st.size,
288
- "cache-control": "no-store",
289
- "content-security-policy": VIBEABLE_CSP,
308
+ ...PREVIEW_HEADERS,
290
309
  });
291
310
  if (req.method === "HEAD")
292
311
  return void res.end();
@@ -300,13 +319,77 @@ function placeholder(res, slug) {
300
319
  `body{margin:0;min-height:100vh;display:grid;place-items:center}main{text-align:center;padding:32px}` +
301
320
  `b{display:block;color:#1b1b1f;font-size:16px;font-weight:500;margin-bottom:4px}</style></head>` +
302
321
  `<body><main><b>${escapeHtml(slug)}</b>no index.html yet — the preview appears as soon as the agent writes one.</main></body></html>`;
303
- res.writeHead(404, {
304
- "content-type": "text/html; charset=utf-8",
305
- "cache-control": "no-store",
306
- "content-security-policy": VIBEABLE_CSP,
307
- });
322
+ res.writeHead(404, { "content-type": "text/html; charset=utf-8", ...PREVIEW_HEADERS });
308
323
  res.end(html);
309
324
  }
325
+ /* ---------- dev proxy ---------- */
326
+ /** "/vibeables/<slug>/x/y?q" → "/x/y?q" for the dev server. */
327
+ function devPath(url, slug) {
328
+ const prefix = `/vibeables/${encodeURIComponent(slug)}`;
329
+ const rest = url.pathname.startsWith(prefix) ? url.pathname.slice(prefix.length) : url.pathname;
330
+ return `${rest || "/"}${url.search}`;
331
+ }
332
+ /** Hop-by-hop headers must not be forwarded either way. */
333
+ const HOP = new Set(["connection", "keep-alive", "transfer-encoding", "te", "trailer", "upgrade", "proxy-connection"]);
334
+ function proxyToDev(req, res, url, slug, port) {
335
+ const headers = {};
336
+ for (const [k, v] of Object.entries(req.headers))
337
+ if (!HOP.has(k) && v !== undefined)
338
+ headers[k] = v;
339
+ headers.host = `127.0.0.1:${port}`;
340
+ const up = http.request({ host: "127.0.0.1", port, method: req.method, path: devPath(url, slug), headers }, (r) => {
341
+ const out = {};
342
+ for (const [k, v] of Object.entries(r.headers))
343
+ if (!HOP.has(k) && v !== undefined)
344
+ out[k] = v;
345
+ // The sandbox applies to the dev server's pages like to static files,
346
+ // and the inspector's rule wins over whatever the dev server sent.
347
+ Object.assign(out, PREVIEW_HEADERS);
348
+ if (r.statusCode && r.statusCode >= 300 && r.statusCode < 400 && typeof out.location === "string" && out.location.startsWith("/")) {
349
+ out.location = `/vibeables/${encodeURIComponent(slug)}${out.location}`;
350
+ }
351
+ res.writeHead(r.statusCode ?? 502, out);
352
+ r.pipe(res);
353
+ });
354
+ up.on("error", (err) => {
355
+ if (!res.headersSent) {
356
+ res.writeHead(502, { "content-type": "text/html; charset=utf-8", ...PREVIEW_HEADERS });
357
+ }
358
+ res.end(`<!doctype html><meta charset="utf-8"><p style="font:14px system-ui;color:#46464f;padding:24px">dev server not answering: ${escapeHtml(err.message)}</p>`);
359
+ });
360
+ req.pipe(up);
361
+ }
362
+ /**
363
+ * WebSocket upgrades under /vibeables/<slug>/ go to the dev server too — that
364
+ * is how Vite-style HMR reaches the pane. Anything else is closed: the
365
+ * inspector has no sockets of its own.
366
+ */
367
+ export function proxyUpgrade(req, socket, head) {
368
+ const url = new URL(req.url ?? "/", "http://localhost");
369
+ const seg = url.pathname.split("/");
370
+ const slug = seg[1] === "vibeables" ? decodeURIComponent(seg[2] ?? "") : "";
371
+ const dev = slug ? devs.get(slug) : undefined;
372
+ if (!dev || dev.exitCode !== undefined || !dev.child)
373
+ return void socket.destroy();
374
+ const upstream = net.connect(dev.port, "127.0.0.1", () => {
375
+ const lines = [`${req.method} ${devPath(url, slug)} HTTP/1.1`];
376
+ for (const [k, v] of Object.entries(req.headers)) {
377
+ if (v === undefined)
378
+ continue;
379
+ lines.push(`${k}: ${k === "host" ? `127.0.0.1:${dev.port}` : Array.isArray(v) ? v.join(", ") : v}`);
380
+ }
381
+ upstream.write(lines.join("\r\n") + "\r\n\r\n");
382
+ if (head.length)
383
+ upstream.write(head);
384
+ socket.pipe(upstream).pipe(socket);
385
+ });
386
+ const drop = () => {
387
+ socket.destroy();
388
+ upstream.destroy();
389
+ };
390
+ upstream.on("error", drop);
391
+ socket.on("error", drop);
392
+ }
310
393
  const channels = new Map();
311
394
  const IGNORED = /(^|[\\/])(\.git|node_modules|\.cache|\.vite|\.next)([\\/]|$)/;
312
395
  /**
@@ -340,12 +423,15 @@ export function subscribeVibeable(slug, dir, fn) {
340
423
  c.watcher.on("error", (err) => {
341
424
  for (const s of c.subs)
342
425
  s({ type: "error", message: `file watcher stopped: ${err.message}` });
343
- c.watcher?.close();
344
- c.watcher = null;
426
+ // Drop the channel: the next subscriber gets a fresh watch() attempt
427
+ // instead of joining a dead one that never reports anything.
428
+ dropChannel(slug);
345
429
  });
346
430
  }
347
431
  catch (err) {
432
+ dropChannel(slug);
348
433
  queueMicrotask(() => fn({ type: "error", message: `cannot watch ${dir}: ${err.message}` }));
434
+ return () => { };
349
435
  }
350
436
  }
351
437
  ch.subs.add(fn);
@@ -362,6 +448,16 @@ export function subscribeVibeable(slug, dir, fn) {
362
448
  }
363
449
  };
364
450
  }
451
+ /** Close a channel's watcher and forget it; subscribers are left to reconnect. */
452
+ function dropChannel(slug) {
453
+ const c = channels.get(slug);
454
+ if (!c)
455
+ return;
456
+ if (c.timer)
457
+ clearTimeout(c.timer);
458
+ c.watcher?.close();
459
+ channels.delete(slug);
460
+ }
365
461
  function broadcast(slug, ev) {
366
462
  const c = channels.get(slug);
367
463
  if (!c)
@@ -423,7 +519,17 @@ function pushLog(d, chunk) {
423
519
  * leads its own process group so stopping it also stops what it spawned
424
520
  * (npm → vite → esbuild).
425
521
  */
426
- export async function startDev(slug) {
522
+ /** Starts in flight per slug: two clicks (or two tabs) share one spawn instead of leaking a process. */
523
+ const starting = new Map();
524
+ export function startDev(slug) {
525
+ const inFlight = starting.get(slug);
526
+ if (inFlight)
527
+ return inFlight;
528
+ const p = startDevNow(slug).finally(() => starting.delete(slug));
529
+ starting.set(slug, p);
530
+ return p;
531
+ }
532
+ async function startDevNow(slug) {
427
533
  const r = await resolveVibeable(slug);
428
534
  if (r.configError)
429
535
  throw new Error(r.configError);
@@ -433,12 +539,24 @@ export async function startDev(slug) {
433
539
  if (existing && existing.exitCode === undefined && existing.child)
434
540
  return vibeableStatus(slug);
435
541
  const port = r.config.port ?? (await freePort());
542
+ // The pane reaches the server through /vibeables/<slug>/ on the inspector,
543
+ // so a dev server that emits absolute URLs needs that as its base.
544
+ const basePath = `/vibeables/${encodeURIComponent(slug)}/`;
436
545
  const child = spawn(r.config.dev, [], {
437
546
  cwd: r.dir,
438
547
  shell: true,
439
548
  detached: process.platform !== "win32",
440
549
  stdio: ["ignore", "pipe", "pipe"],
441
- env: { ...process.env, PORT: String(port), BROWSER: "none", FORCE_COLOR: "0", NO_COLOR: "1", CI: process.env.CI ?? "1" },
550
+ env: {
551
+ ...process.env,
552
+ PORT: String(port),
553
+ BASE_PATH: basePath,
554
+ PUBLIC_URL: basePath,
555
+ BROWSER: "none",
556
+ FORCE_COLOR: "0",
557
+ NO_COLOR: "1",
558
+ CI: process.env.CI ?? "1",
559
+ },
442
560
  });
443
561
  const d = { child, command: r.config.dev, port, ready: false, startedAt: new Date().toISOString(), log: [] };
444
562
  devs.set(slug, d);
@@ -490,7 +608,8 @@ function killGroup(d, signal) {
490
608
  }
491
609
  /** SIGTERM the dev process group, SIGKILL what is still there after 3 s. */
492
610
  export async function stopDev(slug) {
493
- await resolveVibeable(slug);
611
+ // Kill before resolving: a folder that vanished or a feature switched off
612
+ // must not leave the process running.
494
613
  const d = devs.get(slug);
495
614
  if (d && d.exitCode === undefined && d.child) {
496
615
  const exited = new Promise((resolve) => d.child.once("exit", () => resolve()));
@@ -1 +1 @@
1
- import{L as e,R as t,Ut as n,n as r,r as i,s as a,u as o}from"./dist-H3rerbMg.js";import{n as s}from"./dist-DwyxsFyN.js";import{r as c}from"./dist-CI-2-8Tz.js";var l=1,u=33,d=34,f=35,p=36,m=new r(e=>{let t=e.pos;for(;;){if(e.next==10){e.advance();break}if(e.next==123&&e.peek(1)==123||e.next<0)break;e.advance()}e.pos>t&&e.acceptToken(l)});function h(e,t,n){return new r(r=>{let i=r.pos;for(;r.next!=e&&r.next>=0&&(n||r.next!=38&&(r.next!=123||r.peek(1)!=123));)r.advance();r.pos>i&&r.acceptToken(t)})}var g=h(39,u,!1),_=h(34,d,!1),v=h(39,f,!0),y=h(34,p,!0),b=i.deserialize({version:14,states:"(jOVOqOOOeQpOOOvO!bO'#CaOOOP'#Cx'#CxQVOqOOO!OQpO'#CfO!WQpO'#ClO!]QpO'#CrO!bQpO'#CsOOQO'#Cv'#CvQ!gQpOOQ!lQpOOQ!qQpOOOOOV,58{,58{O!vOpO,58{OOOP-E6v-E6vO!{QpO,59QO#TQpO,59QOOQO,59W,59WO#YQpO,59^OOQO,59_,59_O#_QpOOO#_QpOOO#gQpOOOOOV1G.g1G.gO#oQpO'#CyO#tQpO1G.lOOQO1G.l1G.lO#|QpO1G.lOOQO1G.x1G.xO$UO`O'#DUO$ZOWO'#DUOOQO'#Co'#CoQOQpOOOOQO'#Cu'#CuO$`OtO'#CwO$qOrO'#CwOOQO,59e,59eOOQO-E6w-E6wOOQO7+$W7+$WO%SQpO7+$WO%[QpO7+$WOOOO'#Cp'#CpO%aOpO,59pOOOO'#Cq'#CqO%fOpO,59pOOOS'#Cz'#CzO%kOtO,59cOOQO,59c,59cOOOQ'#C{'#C{O%|OrO,59cO&_QpO<<GrOOQO<<Gr<<GrOOQO1G/[1G/[OOOS-E6x-E6xOOQO1G.}1G.}OOOQ-E6y-E6yOOQOAN=^AN=^",stateData:"&d~OvOS~OPROSQOVROWRO~OZTO[XO^VOaUOhWO~OR]OU^O~O[`O^aO~O[bO~O[cO~O[dO~ObeO~ObfO~ObgO~ORhO~O]kOwiO~O[lO~O_mO~OynOzoO~OysOztO~O[uO~O]wOwiO~O_yOwiO~OtzO~Os|O~OSQOV!OOW!OOr!OOy!QO~OSQOV!ROW!ROq!ROz!QO~O_!TOwiO~O]!UO~Oy!VO~Oz!VO~OSQOV!OOW!OOr!OOy!XO~OSQOV!ROW!ROq!ROz!XO~O]!ZO~O",goto:"#dyPPPPPzPPPP!WPPPPP!WPP!Z!^!a!d!dP!g!j!m!p!v#Q#WPPPPPPPP#^SROSS!Os!PT!Rt!SRYPRqeR{nR}oRZPRqfR[PRqgQSOR_SQj`SvjxRxlQ!PsR!W!PQ!StR!Y!SQpeRrf",nodeNames:`⚠ Text Content }} {{ Interpolation InterpolationContent Entity InvalidEntity Attribute BoundAttributeName [ Identifier ] ( ) ReferenceName # Is ExpressionAttributeValue AttributeInterpolation AttributeInterpolation EventName DirectiveName * StatementAttributeValue AttributeName AttributeValue`,maxTerm:42,nodeProps:[[`openedBy`,3,`{{`,15,`(`],[`closedBy`,4,`}}`,14,`)`],[`isolate`,-4,5,19,25,27,``]],skippedNodes:[0],repeatNodeCount:4,tokenData:"0r~RyOX#rXY$mYZ$mZ]#r]^$m^p#rpq$mqr#rrs%jst&Qtv#rvw&hwx)zxy*byz*xz{+`{}#r}!O+v!O!P-]!P!Q#r!Q![+v![!]+v!]!_#r!_!`-s!`!c#r!c!}+v!}#O.Z#O#P#r#P#Q.q#Q#R#r#R#S+v#S#T#r#T#o+v#o#p/X#p#q#r#q#r0Z#r%W#r%W;'S+v;'S;:j-V;:j;=`$g<%lO+vQ#wTUQO#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rQ$ZSO#q#r#r;'S#r;'S;=`$g<%lO#rQ$jP;=`<%l#rR$t[UQvPOX#rXY$mYZ$mZ]#r]^$m^p#rpq$mq#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rR%qTyPUQO#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rR&XTaPUQO#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rR&oXUQWPOp'[pq#rq!]'[!]!^#r!^#q'[#q#r(d#r;'S'[;'S;=`)t<%lO'[R'aXUQOp'[pq#rq!]'[!]!^'|!^#q'[#q#r(d#r;'S'[;'S;=`)t<%lO'[R(TTVPUQO#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rR(gXOp'[pq#rq!]'[!]!^'|!^#q'[#q#r)S#r;'S'[;'S;=`)t<%lO'[P)VUOp)Sq!])S!]!^)i!^;'S)S;'S;=`)n<%lO)SP)nOVPP)qP;=`<%l)SR)wP;=`<%l'[R*RTzPUQO#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rR*iT^PUQO#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rR+PT_PUQO#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rR+gThPUQO#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rR+}b[PUQO}#r}!O+v!O!Q#r!Q![+v![!]+v!]!c#r!c!}+v!}#R#r#R#S+v#S#T#r#T#o+v#o#q#r#q#r$W#r%W#r%W;'S+v;'S;:j-V;:j;=`$g<%lO+vR-YP;=`<%l+vR-dTwPUQO#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rR-zTUQbPO#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rR.bTZPUQO#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rR.xT]PUQO#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rR/^VUQO#o#r#o#p/s#p#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rR/zTSPUQO#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#r~0^TO#q#r#q#r0m#r;'S#r;'S;=`$g<%lO#r~0rOR~",tokenizers:[m,g,_,v,y,0,1],topRules:{Content:[0,2],Attribute:[1,9]},tokenPrec:0}),x=c.parser.configure({top:`SingleExpression`}),S=b.configure({props:[e({Text:t.content,Is:t.definitionOperator,AttributeName:t.attributeName,"AttributeValue ExpressionAttributeValue StatementAttributeValue":t.attributeValue,Entity:t.character,InvalidEntity:t.invalid,"BoundAttributeName/Identifier":t.attributeName,"EventName/Identifier":t.special(t.attributeName),"ReferenceName/Identifier":t.variableName,"DirectiveName/Identifier":t.keyword,"{{ }}":t.brace,"( )":t.paren,"[ ]":t.bracket,"# '*'":t.punctuation})]}),C={parser:x},w={parser:c.parser},T=S.configure({wrap:n((e,t)=>e.name==`InterpolationContent`?C:null)}),E=S.configure({wrap:n((e,t)=>e.name==`InterpolationContent`?C:e.name==`AttributeInterpolation`?e.node.parent?.name==`StatementAttributeValue`?w:C:null),top:`Attribute`}),D={parser:T},O={parser:E},k=s({selfClosingTags:!0});function A(e){return e.configure({wrap:n(M)},`angular`)}var j=A(k.language);function M(e,t){switch(e.name){case`Attribute`:return/^[*#(\[]|\{\{/.test(t.read(e.from,e.to))?O:null;case`Text`:return D}return null}function N(e={}){let t=k;if(e.base){if(e.base.language.name!=`html`||!(e.base.language instanceof a))throw RangeError(`The base option must be the result of calling html(...)`);t=e.base}return new o(t.language==k.language?j:A(t.language),[t.support,t.language.data.of({closeBrackets:{brackets:[`[`,`{`,`"`]},indentOnInput:/^\s*[\}\]]$/})])}export{N as angular};
1
+ import{L as e,R as t,Ut as n,n as r,r as i,s as a,u as o}from"./dist-H3rerbMg.js";import{n as s}from"./dist-DfmPNdMI.js";import{r as c}from"./dist-BUzvZfC2.js";var l=1,u=33,d=34,f=35,p=36,m=new r(e=>{let t=e.pos;for(;;){if(e.next==10){e.advance();break}if(e.next==123&&e.peek(1)==123||e.next<0)break;e.advance()}e.pos>t&&e.acceptToken(l)});function h(e,t,n){return new r(r=>{let i=r.pos;for(;r.next!=e&&r.next>=0&&(n||r.next!=38&&(r.next!=123||r.peek(1)!=123));)r.advance();r.pos>i&&r.acceptToken(t)})}var g=h(39,u,!1),_=h(34,d,!1),v=h(39,f,!0),y=h(34,p,!0),b=i.deserialize({version:14,states:"(jOVOqOOOeQpOOOvO!bO'#CaOOOP'#Cx'#CxQVOqOOO!OQpO'#CfO!WQpO'#ClO!]QpO'#CrO!bQpO'#CsOOQO'#Cv'#CvQ!gQpOOQ!lQpOOQ!qQpOOOOOV,58{,58{O!vOpO,58{OOOP-E6v-E6vO!{QpO,59QO#TQpO,59QOOQO,59W,59WO#YQpO,59^OOQO,59_,59_O#_QpOOO#_QpOOO#gQpOOOOOV1G.g1G.gO#oQpO'#CyO#tQpO1G.lOOQO1G.l1G.lO#|QpO1G.lOOQO1G.x1G.xO$UO`O'#DUO$ZOWO'#DUOOQO'#Co'#CoQOQpOOOOQO'#Cu'#CuO$`OtO'#CwO$qOrO'#CwOOQO,59e,59eOOQO-E6w-E6wOOQO7+$W7+$WO%SQpO7+$WO%[QpO7+$WOOOO'#Cp'#CpO%aOpO,59pOOOO'#Cq'#CqO%fOpO,59pOOOS'#Cz'#CzO%kOtO,59cOOQO,59c,59cOOOQ'#C{'#C{O%|OrO,59cO&_QpO<<GrOOQO<<Gr<<GrOOQO1G/[1G/[OOOS-E6x-E6xOOQO1G.}1G.}OOOQ-E6y-E6yOOQOAN=^AN=^",stateData:"&d~OvOS~OPROSQOVROWRO~OZTO[XO^VOaUOhWO~OR]OU^O~O[`O^aO~O[bO~O[cO~O[dO~ObeO~ObfO~ObgO~ORhO~O]kOwiO~O[lO~O_mO~OynOzoO~OysOztO~O[uO~O]wOwiO~O_yOwiO~OtzO~Os|O~OSQOV!OOW!OOr!OOy!QO~OSQOV!ROW!ROq!ROz!QO~O_!TOwiO~O]!UO~Oy!VO~Oz!VO~OSQOV!OOW!OOr!OOy!XO~OSQOV!ROW!ROq!ROz!XO~O]!ZO~O",goto:"#dyPPPPPzPPPP!WPPPPP!WPP!Z!^!a!d!dP!g!j!m!p!v#Q#WPPPPPPPP#^SROSS!Os!PT!Rt!SRYPRqeR{nR}oRZPRqfR[PRqgQSOR_SQj`SvjxRxlQ!PsR!W!PQ!StR!Y!SQpeRrf",nodeNames:`⚠ Text Content }} {{ Interpolation InterpolationContent Entity InvalidEntity Attribute BoundAttributeName [ Identifier ] ( ) ReferenceName # Is ExpressionAttributeValue AttributeInterpolation AttributeInterpolation EventName DirectiveName * StatementAttributeValue AttributeName AttributeValue`,maxTerm:42,nodeProps:[[`openedBy`,3,`{{`,15,`(`],[`closedBy`,4,`}}`,14,`)`],[`isolate`,-4,5,19,25,27,``]],skippedNodes:[0],repeatNodeCount:4,tokenData:"0r~RyOX#rXY$mYZ$mZ]#r]^$m^p#rpq$mqr#rrs%jst&Qtv#rvw&hwx)zxy*byz*xz{+`{}#r}!O+v!O!P-]!P!Q#r!Q![+v![!]+v!]!_#r!_!`-s!`!c#r!c!}+v!}#O.Z#O#P#r#P#Q.q#Q#R#r#R#S+v#S#T#r#T#o+v#o#p/X#p#q#r#q#r0Z#r%W#r%W;'S+v;'S;:j-V;:j;=`$g<%lO+vQ#wTUQO#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rQ$ZSO#q#r#r;'S#r;'S;=`$g<%lO#rQ$jP;=`<%l#rR$t[UQvPOX#rXY$mYZ$mZ]#r]^$m^p#rpq$mq#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rR%qTyPUQO#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rR&XTaPUQO#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rR&oXUQWPOp'[pq#rq!]'[!]!^#r!^#q'[#q#r(d#r;'S'[;'S;=`)t<%lO'[R'aXUQOp'[pq#rq!]'[!]!^'|!^#q'[#q#r(d#r;'S'[;'S;=`)t<%lO'[R(TTVPUQO#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rR(gXOp'[pq#rq!]'[!]!^'|!^#q'[#q#r)S#r;'S'[;'S;=`)t<%lO'[P)VUOp)Sq!])S!]!^)i!^;'S)S;'S;=`)n<%lO)SP)nOVPP)qP;=`<%l)SR)wP;=`<%l'[R*RTzPUQO#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rR*iT^PUQO#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rR+PT_PUQO#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rR+gThPUQO#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rR+}b[PUQO}#r}!O+v!O!Q#r!Q![+v![!]+v!]!c#r!c!}+v!}#R#r#R#S+v#S#T#r#T#o+v#o#q#r#q#r$W#r%W#r%W;'S+v;'S;:j-V;:j;=`$g<%lO+vR-YP;=`<%l+vR-dTwPUQO#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rR-zTUQbPO#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rR.bTZPUQO#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rR.xT]PUQO#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rR/^VUQO#o#r#o#p/s#p#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#rR/zTSPUQO#q#r#q#r$W#r;'S#r;'S;=`$g<%lO#r~0^TO#q#r#q#r0m#r;'S#r;'S;=`$g<%lO#r~0rOR~",tokenizers:[m,g,_,v,y,0,1],topRules:{Content:[0,2],Attribute:[1,9]},tokenPrec:0}),x=c.parser.configure({top:`SingleExpression`}),S=b.configure({props:[e({Text:t.content,Is:t.definitionOperator,AttributeName:t.attributeName,"AttributeValue ExpressionAttributeValue StatementAttributeValue":t.attributeValue,Entity:t.character,InvalidEntity:t.invalid,"BoundAttributeName/Identifier":t.attributeName,"EventName/Identifier":t.special(t.attributeName),"ReferenceName/Identifier":t.variableName,"DirectiveName/Identifier":t.keyword,"{{ }}":t.brace,"( )":t.paren,"[ ]":t.bracket,"# '*'":t.punctuation})]}),C={parser:x},w={parser:c.parser},T=S.configure({wrap:n((e,t)=>e.name==`InterpolationContent`?C:null)}),E=S.configure({wrap:n((e,t)=>e.name==`InterpolationContent`?C:e.name==`AttributeInterpolation`?e.node.parent?.name==`StatementAttributeValue`?w:C:null),top:`Attribute`}),D={parser:T},O={parser:E},k=s({selfClosingTags:!0});function A(e){return e.configure({wrap:n(M)},`angular`)}var j=A(k.language);function M(e,t){switch(e.name){case`Attribute`:return/^[*#(\[]|\{\{/.test(t.read(e.from,e.to))?O:null;case`Text`:return D}return null}function N(e={}){let t=k;if(e.base){if(e.base.language.name!=`html`||!(e.base.language instanceof a))throw RangeError(`The base option must be the result of calling html(...)`);t=e.base}return new o(t.language==k.language?j:A(t.language),[t.support,t.language.data.of({closeBrackets:{brackets:[`[`,`{`,`"`]},indentOnInput:/^\s*[\}\]]$/})])}export{N as angular};