@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.
- package/dist/inspector/chat/acp.js +6 -0
- package/dist/inspector/chat/sessions.d.ts +14 -0
- package/dist/inspector/chat/sessions.js +44 -10
- package/dist/inspector/server.js +7 -1
- package/dist/inspector/settings.js +8 -1
- package/dist/inspector/vibeables.d.ts +6 -5
- package/dist/inspector/vibeables.js +131 -12
- package/inspector/dist/assets/{dist-Dd-u7fXY.js → dist-BAHymHVh.js} +1 -1
- package/inspector/dist/assets/{dist-CI-2-8Tz.js → dist-BUzvZfC2.js} +1 -1
- package/inspector/dist/assets/{dist-BECiZEqH.js → dist-CMK0Dv1Q.js} +1 -1
- package/inspector/dist/assets/dist-CWxCXHGC.js +1 -0
- package/inspector/dist/assets/{dist-DTW4FhJ_.js → dist-Cn3OeZ9x.js} +1 -1
- package/inspector/dist/assets/{dist-3Nzkrylv.js → dist-D01HLRsD.js} +1 -1
- package/inspector/dist/assets/{dist-D3tgdU3w.js → dist-D0WPeXAf.js} +1 -1
- package/inspector/dist/assets/{dist-DXIk6Ebu.js → dist-DQujkoko.js} +1 -1
- package/inspector/dist/assets/{dist-nc_mc1w0.js → dist-DYqdkWRb.js} +1 -1
- package/inspector/dist/assets/{dist-DwyxsFyN.js → dist-DfmPNdMI.js} +1 -1
- package/inspector/dist/assets/{dist-BCM6ehYa.js → dist-FnKl1oDp.js} +1 -1
- package/inspector/dist/assets/{editor-6yCJBeDg.js → editor-CVAfyi-9.js} +3 -3
- package/inspector/dist/assets/{index-DurTvcNI.js → index-Ba-uGp4N.js} +18 -18
- package/inspector/dist/assets/{index-Dom87GNV.css → index-CL7d8WY1.css} +1 -1
- package/inspector/dist/index.html +2 -2
- package/package.json +1 -1
- 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
|
-
|
|
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
|
|
199
|
-
`or move to dev mode
|
|
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
|
|
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
|
|
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
|
|
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) {
|
package/dist/inspector/server.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
344
|
-
|
|
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
|
-
|
|
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: {
|
|
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
|
-
|
|
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-
|
|
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};
|