mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-21 16:02:28 +00:00
feat(cli): add localhost reverse proxy to wmill dev for Claude Desktop preview
Adds a reverse proxy to `wmill dev` that serves the Windmill UI on localhost, enabling Claude Desktop/Preview to open dev pages. Each connected dev page can watch a specific file via the `path` URL param and `setWatch` WebSocket message. Key changes: - CLI: single-port proxy (default :3100) that forwards HTTP to remote Windmill, handles /ws_dev locally for dev file changes, and proxies /ws/* to remote - CLI: per-client watch filtering so multiple tabs can watch different files - CLI: flow broadcasts now include a `path` field - Frontend: Dev.svelte connects to same-origin /ws_dev when no port param, sends setWatch on connect, filters messages client-side as safety net - CLI init: generates .claude/skills/dev-preview/SKILL.md and .claude/launch.json Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+190
-31
@@ -6,6 +6,7 @@ import { WebSocket, WebSocketServer } from "ws";
|
||||
|
||||
import * as getPort from "get-port";
|
||||
import * as http from "node:http";
|
||||
import * as https from "node:https";
|
||||
import * as open from "open";
|
||||
import { readFile, realpath } from "node:fs/promises";
|
||||
import { watch } from "node:fs";
|
||||
@@ -32,7 +33,9 @@ import { listSyncCodebases } from "../../utils/codebase.ts";
|
||||
import { createPreviewLocalScriptReader } from "../../utils/local_path_scripts.ts";
|
||||
|
||||
const PORT = 3001;
|
||||
async function dev(opts: GlobalOptions & SyncOptions) {
|
||||
const PROXY_PORT = 3100;
|
||||
|
||||
async function dev(opts: GlobalOptions & SyncOptions & { proxyPort?: number }) {
|
||||
opts = await mergeConfigWithConfigFile(opts);
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
@@ -106,12 +109,14 @@ async function dev(opts: GlobalOptions & SyncOptions) {
|
||||
codebases,
|
||||
});
|
||||
await replaceAllPathScriptsWithLocal(localFlow.value, localScriptReader, log);
|
||||
const wmFlowPath = localPath.replace(/\.flow\/$/, "").replace(/\/$/, "");
|
||||
currentLastEdit = {
|
||||
type: "flow",
|
||||
flow: localFlow,
|
||||
uriPath: localPath,
|
||||
path: wmFlowPath,
|
||||
};
|
||||
log.info("Updated " + localPath);
|
||||
log.info("Updated " + wmFlowPath);
|
||||
broadcastChanges(currentLastEdit);
|
||||
} else if (typ == "script") {
|
||||
const content = await readFile(cpath, "utf-8");
|
||||
@@ -153,38 +158,49 @@ async function dev(opts: GlobalOptions & SyncOptions) {
|
||||
type: "flow";
|
||||
flow: OpenFlow;
|
||||
uriPath: string;
|
||||
path: string;
|
||||
};
|
||||
|
||||
const connectedClients: Set<WebSocket> = new Set();
|
||||
// Map each connected client to its optional watchPath filter
|
||||
const clientWatchPaths: Map<WebSocket, string | undefined> = new Map();
|
||||
|
||||
// Function to send a message to all connected clients
|
||||
function getEditPath(lastEdit: LastEditScript | LastEditFlow): string {
|
||||
return lastEdit.path;
|
||||
}
|
||||
|
||||
function normalizePath(p: string): string {
|
||||
return p.replace(/\.flow\/?$/, "").replace(/\/$/, "");
|
||||
}
|
||||
|
||||
// Send file changes to clients, filtered by their watchPath
|
||||
function broadcastChanges(lastEdit: LastEditScript | LastEditFlow) {
|
||||
for (const client of connectedClients.values()) {
|
||||
client.send(JSON.stringify(lastEdit));
|
||||
const editPath = normalizePath(getEditPath(lastEdit));
|
||||
const msg = JSON.stringify(lastEdit);
|
||||
for (const [client, watchPath] of clientWatchPaths.entries()) {
|
||||
if (watchPath === undefined || normalizePath(watchPath) === editPath) {
|
||||
client.send(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function startApp() {
|
||||
const server = http.createServer((_req, res) => {
|
||||
res.writeHead(200);
|
||||
res.end();
|
||||
});
|
||||
const wss = new WebSocketServer({ server });
|
||||
|
||||
// WebSocket server event listeners
|
||||
function setupDevWs(wss: WebSocketServer) {
|
||||
wss.on("connection", (ws: WebSocket) => {
|
||||
connectedClients.add(ws);
|
||||
console.log("New client connected");
|
||||
clientWatchPaths.set(ws, undefined);
|
||||
console.log("New dev client connected");
|
||||
|
||||
ws.on("open", () => {
|
||||
if (currentLastEdit) {
|
||||
broadcastChanges(currentLastEdit);
|
||||
// Send the current state to the new client
|
||||
const watchPath = clientWatchPaths.get(ws);
|
||||
if (watchPath === undefined || normalizePath(watchPath) === normalizePath(getEditPath(currentLastEdit))) {
|
||||
ws.send(JSON.stringify(currentLastEdit));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ws.on("close", () => {
|
||||
connectedClients.delete(ws);
|
||||
console.log("Client disconnected");
|
||||
clientWatchPaths.delete(ws);
|
||||
console.log("Dev client disconnected");
|
||||
});
|
||||
|
||||
ws.on("message", (message: WebSocket.RawData) => {
|
||||
@@ -198,37 +214,176 @@ async function dev(opts: GlobalOptions & SyncOptions) {
|
||||
|
||||
if (data.type === "load") {
|
||||
loadPaths([data.path]);
|
||||
} else if (data.type === "setWatch") {
|
||||
const path = data.path as string;
|
||||
clientWatchPaths.set(ws, path);
|
||||
console.log(`Client watching: ${path}`);
|
||||
ws.send(JSON.stringify({ type: "watchSet", path }));
|
||||
// Send current state for the watched path if available
|
||||
if (currentLastEdit && normalizePath(getEditPath(currentLastEdit)) === normalizePath(path)) {
|
||||
ws.send(JSON.stringify(currentLastEdit));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Start the server
|
||||
const port = await getPort.default({ port: 3001 });
|
||||
const url =
|
||||
async function startLegacyServer(): Promise<number> {
|
||||
const server = http.createServer((_req, res) => {
|
||||
res.writeHead(200);
|
||||
res.end();
|
||||
});
|
||||
const wss = new WebSocketServer({ server });
|
||||
setupDevWs(wss);
|
||||
|
||||
const port = await getPort.default({ port: PORT });
|
||||
return new Promise((resolve) => {
|
||||
server.listen(port, () => {
|
||||
console.log(`Legacy dev server listening on port ${port}`);
|
||||
resolve(port);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function startProxyServer(remoteUrl: string, proxyPort: number, wsPort: number) {
|
||||
const remote = new URL(remoteUrl);
|
||||
const isHttps = remote.protocol === "https:";
|
||||
const remoteHost = remote.hostname;
|
||||
const remotePort = remote.port ? parseInt(remote.port) : (isHttps ? 443 : 80);
|
||||
const httpModule = isHttps ? https : http;
|
||||
|
||||
// Dev WebSocket server (handles /ws_dev path)
|
||||
const devWss = new WebSocketServer({ noServer: true });
|
||||
setupDevWs(devWss);
|
||||
|
||||
// Separate WSS for proxied WebSocket connections (not tracked as dev clients)
|
||||
const proxyWss = new WebSocketServer({ noServer: true });
|
||||
|
||||
const proxyServer = http.createServer((clientReq, clientRes) => {
|
||||
const proxyOpts: http.RequestOptions = {
|
||||
hostname: remoteHost,
|
||||
port: remotePort,
|
||||
path: clientReq.url,
|
||||
method: clientReq.method,
|
||||
headers: {
|
||||
...clientReq.headers,
|
||||
host: remote.host,
|
||||
},
|
||||
};
|
||||
|
||||
const proxyReq = httpModule.request(proxyOpts, (proxyRes) => {
|
||||
// Rewrite Set-Cookie domain to localhost
|
||||
const setCookie = proxyRes.headers["set-cookie"];
|
||||
if (setCookie) {
|
||||
proxyRes.headers["set-cookie"] = setCookie.map((cookie) =>
|
||||
cookie.replace(/domain=[^;]+/gi, "domain=localhost")
|
||||
);
|
||||
}
|
||||
clientRes.writeHead(proxyRes.statusCode ?? 502, proxyRes.headers);
|
||||
proxyRes.pipe(clientRes, { end: true });
|
||||
});
|
||||
|
||||
proxyReq.on("error", (err) => {
|
||||
console.error("Proxy error:", err.message);
|
||||
clientRes.writeHead(502);
|
||||
clientRes.end("Bad Gateway");
|
||||
});
|
||||
|
||||
clientReq.pipe(proxyReq, { end: true });
|
||||
});
|
||||
|
||||
// Handle WebSocket upgrades
|
||||
proxyServer.on("upgrade", (req, socket, head) => {
|
||||
const pathname = req.url?.split("?")[0] ?? "";
|
||||
|
||||
if (pathname === "/ws_dev") {
|
||||
// Handle locally: dev file-change WebSocket
|
||||
devWss.handleUpgrade(req, socket, head, (ws) => {
|
||||
devWss.emit("connection", ws, req);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Proxy all other WebSocket paths to remote
|
||||
if (pathname.startsWith("/ws/") || pathname.startsWith("/ws_mp/") || pathname.startsWith("/ws_debug/")) {
|
||||
const wsProtocol = isHttps ? "wss" : "ws";
|
||||
const remoteWsUrl = `${wsProtocol}://${remote.host}${req.url}`;
|
||||
const remoteWs = new WebSocket(remoteWsUrl, {
|
||||
headers: {
|
||||
...req.headers,
|
||||
host: remote.host,
|
||||
},
|
||||
});
|
||||
|
||||
remoteWs.on("open", () => {
|
||||
proxyWss.handleUpgrade(req, socket, head, (clientWs) => {
|
||||
clientWs.on("message", (data) => {
|
||||
if (remoteWs.readyState === WebSocket.OPEN) {
|
||||
remoteWs.send(data);
|
||||
}
|
||||
});
|
||||
remoteWs.on("message", (data) => {
|
||||
if (clientWs.readyState === WebSocket.OPEN) {
|
||||
clientWs.send(data);
|
||||
}
|
||||
});
|
||||
clientWs.on("close", () => remoteWs.close());
|
||||
remoteWs.on("close", () => clientWs.close());
|
||||
});
|
||||
});
|
||||
|
||||
remoteWs.on("error", (err) => {
|
||||
console.error("WebSocket proxy error:", err.message);
|
||||
socket.destroy();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Unknown WS path — destroy
|
||||
socket.destroy();
|
||||
});
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
proxyServer.listen(proxyPort, () => {
|
||||
console.log(`Dev proxy listening on http://localhost:${proxyPort}`);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function startApp() {
|
||||
const wsPort = await startLegacyServer();
|
||||
|
||||
const proxyPort = opts.proxyPort ?? PROXY_PORT;
|
||||
await startProxyServer(workspace.remote, proxyPort, wsPort);
|
||||
|
||||
const legacyUrl =
|
||||
`${workspace.remote}dev?workspace=${workspace.workspaceId}&local=true&wm_token=${workspace.token}` +
|
||||
(port === PORT ? "" : `&port=${port}`);
|
||||
(wsPort === PORT ? "" : `&port=${wsPort}`);
|
||||
|
||||
const proxyUrl =
|
||||
`http://localhost:${proxyPort}/dev?workspace=${workspace.workspaceId}&local=true&wm_token=${workspace.token}`;
|
||||
|
||||
console.log(`\nLegacy dev URL: ${legacyUrl}`);
|
||||
console.log(`Proxy dev URL: ${proxyUrl}`);
|
||||
console.log(`\nTo watch a specific file: ${proxyUrl}&path=<wm_path>`);
|
||||
|
||||
console.log(`Go to ${url}`);
|
||||
try {
|
||||
open.openApp(open.apps.browser, { arguments: [url] }).catch((error) => {
|
||||
open.openApp(open.apps.browser, { arguments: [legacyUrl] }).catch((error) => {
|
||||
console.error(
|
||||
`Failed to open browser, please navigate to ${url}, error: ${error}`
|
||||
`Failed to open browser, please navigate to ${legacyUrl}, error: ${error}`
|
||||
);
|
||||
});
|
||||
console.log("Opened browser for you");
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Failed to open browser, please navigate to ${url}, ${error}`
|
||||
`Failed to open browser, please navigate to ${legacyUrl}, ${error}`
|
||||
);
|
||||
}
|
||||
|
||||
console.log(
|
||||
"Dev server will automatically point to the last script edited locally"
|
||||
);
|
||||
|
||||
server.listen(port, () => {
|
||||
console.log(`Server listening on port ${port}`);
|
||||
});
|
||||
}
|
||||
|
||||
await Promise.all([startApp(), watchChanges()]);
|
||||
@@ -241,6 +396,10 @@ const command = new Command()
|
||||
"--includes <pattern...:string>",
|
||||
"Filter paths givena glob pattern or path"
|
||||
)
|
||||
.option(
|
||||
"--proxy-port <port:number>",
|
||||
"Port for the localhost reverse proxy (default: 3100)"
|
||||
)
|
||||
.action(dev as any);
|
||||
|
||||
export default command;
|
||||
|
||||
@@ -346,6 +346,30 @@ async function initAction(opts: InitOptions) {
|
||||
log.warn(`Could not create skills: ${skillError}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Create .claude/launch.json for Claude Preview dev server
|
||||
try {
|
||||
const launchJsonPath = ".claude/launch.json";
|
||||
const launchJson = {
|
||||
version: "0.0.1",
|
||||
configurations: [
|
||||
{
|
||||
name: "windmill-dev",
|
||||
runtimeExecutable: "wmill",
|
||||
runtimeArgs: ["dev"],
|
||||
port: 3100,
|
||||
},
|
||||
],
|
||||
};
|
||||
await writeFile(launchJsonPath, JSON.stringify(launchJson, null, 2) + "\n", "utf-8");
|
||||
log.info(colors.green("Created .claude/launch.json"));
|
||||
} catch (launchError) {
|
||||
if (launchError instanceof Error) {
|
||||
log.warn(`Could not create launch.json: ${launchError.message}`);
|
||||
} else {
|
||||
log.warn(`Could not create launch.json: ${launchError}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
log.warn(`Could not create guidance files: ${error.message}`);
|
||||
|
||||
+47
-10
@@ -32,6 +32,7 @@ export const SKILLS: SkillMetadata[] = [
|
||||
{ name: "schedules", description: "MUST use when configuring schedules." },
|
||||
{ name: "resources", description: "MUST use when managing resources." },
|
||||
{ name: "cli-commands", description: "MUST use when using the CLI." },
|
||||
{ name: "dev-preview", description: "Use when previewing Windmill scripts/flows locally via wmill dev." },
|
||||
];
|
||||
|
||||
// Skill content for each skill (loaded inline for bundling)
|
||||
@@ -4602,18 +4603,9 @@ Tell the user they can run these commands (do NOT run them yourself):
|
||||
| \`wmill app dev\` | Start dev server with live reload |
|
||||
| \`wmill app generate-agents\` | Refresh AGENTS.md and DATATABLES.md |
|
||||
| \`wmill app generate-locks\` | Generate lock files for backend runnables |
|
||||
| \`wmill sync push --extra-includes "f/<folder>/<app>.raw_app/**" --yes\` | Deploy this specific raw app to Windmill (never do a blanket \`wmill sync push\`) |
|
||||
| \`wmill sync push\` | Deploy app to Windmill |
|
||||
| \`wmill sync pull\` | Pull latest from Windmill |
|
||||
|
||||
## Svelte 5 Event Handling
|
||||
|
||||
When building Svelte 5 raw apps, be aware of event delegation:
|
||||
|
||||
- The Svelte runtime version in \`node_modules/svelte\` **must match** the compiler version used by \`wmill sync push\`. If you get \`$.delegated is undefined\` errors at runtime, run \`npm install svelte@latest\` in the raw app folder and re-push.
|
||||
- \`onclick\` on \`<div>\`, \`<span>\`, and other non-interactive elements uses Svelte's event delegation system. If the runtime doesn't support it, you'll get errors.
|
||||
- \`onclick\` on \`<button>\` elements is native and generally works fine.
|
||||
- For modal overlays or click-outside patterns, prefer using \`<button>\` elements styled as overlays, or ensure the Svelte runtime is up to date.
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Check DATATABLES.md** for existing tables before creating new ones
|
||||
@@ -5019,6 +5011,7 @@ Launch a dev server that will spawn a webserver with HMR
|
||||
|
||||
**Options:**
|
||||
- \`--includes <pattern...:string>\` - Filter paths givena glob pattern or path
|
||||
- \`--proxy-port <port:number>\` - Port for the localhost reverse proxy (default: 3100)
|
||||
|
||||
### docs
|
||||
|
||||
@@ -5478,6 +5471,50 @@ workspace related commands
|
||||
- \`workspace delete-fork <fork_name:string>\` - Delete a forked workspace and git branch
|
||||
- \`-y --yes\` - Skip confirmation prompt
|
||||
|
||||
`,
|
||||
"dev-preview": `---
|
||||
name: dev-preview
|
||||
description: Use when previewing Windmill scripts/flows locally via wmill dev.
|
||||
---
|
||||
|
||||
# Dev Preview
|
||||
|
||||
Preview Windmill scripts and flows locally via \`wmill dev\`.
|
||||
|
||||
## Setup
|
||||
|
||||
Start the dev server (includes a localhost reverse proxy):
|
||||
|
||||
\`\`\`bash
|
||||
wmill dev
|
||||
\`\`\`
|
||||
|
||||
This starts:
|
||||
- A file watcher on the local repo
|
||||
- A reverse proxy on \`localhost:3100\` forwarding to the remote Windmill instance
|
||||
- A dev WebSocket on \`/ws_dev\` for live file updates
|
||||
|
||||
## Preview a specific file
|
||||
|
||||
Open in browser or Claude Preview:
|
||||
|
||||
\`\`\`
|
||||
http://localhost:3100/dev?path={wm_path}&local=true
|
||||
\`\`\`
|
||||
|
||||
Where \`wm_path\` is the Windmill path derived from the local file:
|
||||
- **Scripts**: strip the file extension — \`u/admin/my_script.ts\` → \`u/admin/my_script\`
|
||||
- **Flows**: strip the \`.flow/\` suffix — \`f/my_flow.flow/flow.yaml\` → \`f/my_flow\`
|
||||
|
||||
The \`path\` parameter locks the preview to that file. Other file changes are ignored.
|
||||
|
||||
## Switch files
|
||||
|
||||
Navigate to a new URL with a different \`path\` param.
|
||||
|
||||
## Legacy mode
|
||||
|
||||
Without the \`path\` parameter, the dev page shows the last-edited file (original behavior).
|
||||
`,
|
||||
};
|
||||
|
||||
|
||||
@@ -331,6 +331,9 @@
|
||||
}
|
||||
})
|
||||
|
||||
const filterPath =
|
||||
searchParams?.get('path') ?? sessionStorage.getItem('wm_dev_watch_path') ?? undefined
|
||||
|
||||
function connectWs() {
|
||||
try {
|
||||
if (socket) {
|
||||
@@ -339,9 +342,19 @@
|
||||
} catch (e) {
|
||||
console.error('Failed to close websocket', e)
|
||||
}
|
||||
const port = searchParams?.get('port') || '3001'
|
||||
const port = searchParams?.get('port')
|
||||
// If port is explicitly set, use legacy ws://localhost:{port}/ws
|
||||
// Otherwise connect to same-origin /ws_dev (works through the proxy)
|
||||
const wsUrl = port ? `ws://localhost:${port}/ws` : `ws://${location.host}/ws_dev`
|
||||
try {
|
||||
socket = new WebSocket(`ws://localhost:${port}/ws`)
|
||||
socket = new WebSocket(wsUrl)
|
||||
|
||||
socket.addEventListener('open', () => {
|
||||
if (filterPath) {
|
||||
socket?.send(JSON.stringify({ type: 'setWatch', path: filterPath }))
|
||||
sessionStorage.setItem('wm_dev_watch_path', filterPath)
|
||||
}
|
||||
})
|
||||
|
||||
// Listen for messages
|
||||
socket.addEventListener('message', (event) => {
|
||||
@@ -356,6 +369,10 @@
|
||||
console.log('Received invalid JSON: ' + msg)
|
||||
return
|
||||
}
|
||||
if (data.type == 'watchSet') {
|
||||
// Confirmation from server, no action needed
|
||||
return
|
||||
}
|
||||
if (data.type == 'script') {
|
||||
replaceScript(data)
|
||||
} else if (data.type == 'flow') {
|
||||
|
||||
@@ -57,6 +57,7 @@ Launch a dev server that will spawn a webserver with HMR
|
||||
|
||||
**Options:**
|
||||
- `--includes <pattern...:string>` - Filter paths givena glob pattern or path
|
||||
- `--proxy-port <port:number>` - Port for the localhost reverse proxy (default: 3100)
|
||||
|
||||
### docs
|
||||
|
||||
|
||||
@@ -1431,6 +1431,7 @@ Launch a dev server that will spawn a webserver with HMR
|
||||
|
||||
**Options:**
|
||||
- \`--includes <pattern...:string>\` - Filter paths givena glob pattern or path
|
||||
- \`--proxy-port <port:number>\` - Port for the localhost reverse proxy (default: 3100)
|
||||
|
||||
### docs
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ Launch a dev server that will spawn a webserver with HMR
|
||||
|
||||
**Options:**
|
||||
- `--includes <pattern...:string>` - Filter paths givena glob pattern or path
|
||||
- `--proxy-port <port:number>` - Port for the localhost reverse proxy (default: 3100)
|
||||
|
||||
### docs
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
name: dev-preview
|
||||
description: Use when previewing Windmill scripts/flows locally via wmill dev.
|
||||
---
|
||||
|
||||
# Dev Preview
|
||||
|
||||
Preview Windmill scripts and flows locally via `wmill dev`.
|
||||
|
||||
## Setup
|
||||
|
||||
Start the dev server (includes a localhost reverse proxy):
|
||||
|
||||
```bash
|
||||
wmill dev
|
||||
```
|
||||
|
||||
This starts:
|
||||
- A file watcher on the local repo
|
||||
- A reverse proxy on `localhost:3100` forwarding to the remote Windmill instance
|
||||
- A dev WebSocket on `/ws_dev` for live file updates
|
||||
|
||||
## Preview a specific file
|
||||
|
||||
Open in browser or Claude Preview:
|
||||
|
||||
```
|
||||
http://localhost:3100/dev?path={wm_path}&local=true
|
||||
```
|
||||
|
||||
Where `wm_path` is the Windmill path derived from the local file:
|
||||
- **Scripts**: strip the file extension — `u/admin/my_script.ts` → `u/admin/my_script`
|
||||
- **Flows**: strip the `.flow/` suffix — `f/my_flow.flow/flow.yaml` → `f/my_flow`
|
||||
|
||||
The `path` parameter locks the preview to that file. Other file changes are ignored.
|
||||
|
||||
## Switch files
|
||||
|
||||
Navigate to a new URL with a different `path` param.
|
||||
|
||||
## Legacy mode
|
||||
|
||||
Without the `path` parameter, the dev page shows the last-edited file (original behavior).
|
||||
@@ -0,0 +1,38 @@
|
||||
# Dev Preview
|
||||
|
||||
Preview Windmill scripts and flows locally via `wmill dev`.
|
||||
|
||||
## Setup
|
||||
|
||||
Start the dev server (includes a localhost reverse proxy):
|
||||
|
||||
```bash
|
||||
wmill dev
|
||||
```
|
||||
|
||||
This starts:
|
||||
- A file watcher on the local repo
|
||||
- A reverse proxy on `localhost:3100` forwarding to the remote Windmill instance
|
||||
- A dev WebSocket on `/ws_dev` for live file updates
|
||||
|
||||
## Preview a specific file
|
||||
|
||||
Open in browser or Claude Preview:
|
||||
|
||||
```
|
||||
http://localhost:3100/dev?path={wm_path}&local=true
|
||||
```
|
||||
|
||||
Where `wm_path` is the Windmill path derived from the local file:
|
||||
- **Scripts**: strip the file extension — `u/admin/my_script.ts` → `u/admin/my_script`
|
||||
- **Flows**: strip the `.flow/` suffix — `f/my_flow.flow/flow.yaml` → `f/my_flow`
|
||||
|
||||
The `path` parameter locks the preview to that file. Other file changes are ignored.
|
||||
|
||||
## Switch files
|
||||
|
||||
Navigate to a new URL with a different `path` param.
|
||||
|
||||
## Legacy mode
|
||||
|
||||
Without the `path` parameter, the dev page shows the last-edited file (original behavior).
|
||||
@@ -726,6 +726,11 @@ SKILL_DEFINITIONS = [
|
||||
'description': 'MUST use when using the CLI.',
|
||||
'content_key': 'cli_commands',
|
||||
},
|
||||
{
|
||||
'name': 'dev-preview',
|
||||
'description': 'Use when previewing Windmill scripts/flows locally via wmill dev.',
|
||||
'content_key': 'dev_preview',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -755,6 +760,7 @@ def generate_skills(
|
||||
'schedules': read_markdown_file(base_dir / "schedules.md"),
|
||||
'resources': read_markdown_file(base_dir / "resources.md"),
|
||||
'cli_commands': cli_commands,
|
||||
'dev_preview': read_markdown_file(base_dir / "dev-preview.md"),
|
||||
}
|
||||
|
||||
# CLI intro for script skills
|
||||
|
||||
Reference in New Issue
Block a user