mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-07 00:01:49 +00:00
feat(cli): wmill dev with per-flow proxy and responsive Dev UI (#8529)
* feat(cli): add `wmill flow dev` subcommand with per-flow reverse proxy and launch.json Also generates .claude/launch.json for existing flow folders during `wmill init`. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: responsive dev layout and hide splitter for single-pane views Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: clamp flow graph height between minHeight and maxHeight Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(cli): enhance app new with Claude Desktop integration and better defaults - Add .claude/launch.json to generated app scaffold for Claude Code preview support - Add "Open in Claude Desktop?" prompt that creates a CLI session and opens it in Claude Desktop Code mode via the claude://resume deep link - Improve default CSS template with body background, system fonts, and padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(cli): handle both .flow and __flow suffixes in wmill dev The flow detection in loadPaths only checked the configured suffix (dotted or non-dotted), so users with nonDottedPaths=true who had .flow folders (or vice versa) would see inline script edits treated as standalone script changes instead of flow changes. Now checks both suffix forms everywhere: type classification, folder path extraction, path stripping, and loadWmPath lookup. Also adds raw_app launch.json generation to init and sync pull. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs(cli): update generated skills with dev workflow and preview commands Update cli-commands, write-flow, and raw-app skills to document the new local dev workflow (wmill dev --path, --proxy-port, .claude/launch.json). Add wmill script preview and wmill flow preview to all script/flow skills so agents know how to test without deploying. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(cli): include path in dev URL and use open.default for browser - Append &path= to the printed/opened URL when --path is specified - Use open.default(url) instead of open.openApp for more reliable browser opening Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(cli): add Claude CLI/Desktop detection hints in wmill flow new Show contextual instructions for previewing flows based on available tools. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: regenerate auto-generated CLI skills for new dev flags Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(cli): handle mixed flow suffixes in dev file watcher The ignore() function uses isFlowPath() which only checks the configured suffix (__flow or .flow), causing files in the other variant to be silently ignored. Bypass the ignore check for any file inside a flow folder and force flow type detection regardless of suffix configuration. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(cli): drop default proxy in flow folders, open browser, add --no-browser Manual `wmill dev` in a flow folder should not implicitly enable the reverse proxy. Both proxy and legacy modes now open the browser; the new --no-browser flag opts out. Claude Code launch.json templates pass --no-browser so the IDE preview doesn't fight a system browser window. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(cli): gate dev broadcasts by --path and push currentLastEdit on connect When --path (or auto-detected flow path) is set, drop file events for any other path so the dev page stays locked to the requested resource and currentLastEdit can never reflect an unrelated edit. The connection handler proactively pushes currentLastEdit so the page renders without waiting for the first file change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(frontend): prefer WebSocket for flow round-trip when wmill dev is connected updateFlow used isInIframe priority, which routed Claude Code's iframe preview through postMessage (no listener) and silently dropped flow edits. Flip the priority: when the wmill dev WebSocket is open, use it (covers standalone tabs and Claude Code's preview); fall back to postMessage only when no WS is connected (the VS Code extension's iframe URL has no `local=true`, so it never opens one). Also stop assigning lastSent before a channel actually accepted the message, so a CONNECTING WS doesn't silently swallow the first change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(dev): url is source of truth for path; add workspace file picker Drops the server-side --path gate added in3c2d5155e1. The dev page now filters by its URL's ?path= and the CLI is a dumb broadcaster, which lets multiple tabs each watch different paths. When the URL has no ?path=, the page asks the CLI for a list of workspace items (flows, scripts, raw_apps) via a new {type:'listPaths'} WS message and renders a picker. Clicking a flow or script soft-updates the URL via history.pushState and loads it; raw_apps surface a hint to use `wmill app dev` since they don't render here. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(dev): picker uses homepage tree view with summaries Replace the hand-rolled Button-list picker with a TreeView-style layout that mirrors the Windmill homepage: folder/user tree grouping via `groupItems`, item rows rendered through the shared `Row.svelte` (no actions, no favourites, no link — just the visual), a `SearchItems` fuzzy filter with the same search input styling and placeholder as the homepage, and `group-open:` chevron toggling on native <details>. The CLI's listWorkspacePaths now also reads summaries from each item's metadata (flow.yaml for flows, <script>.script.yaml for scripts) in parallel so the picker shows summaries as the primary row label, same as the homepage. Raw apps have no standard manifest so they show the path only. Additional polish: title shows "<workspace> (local)" instead of generic text, subtitle trimmed, item-wrapper owns the border-b so Row's internal last:border-b-0 doesn't zero it out, summary border gated on group-open: to avoid doubled lines when a folder is collapsed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(cli): wmill dev --no-browser was a no-op Cliffy's `.option("--no-browser", ...)` creates an option named `browser` (boolean, default undefined) that becomes `false` when the flag is passed. The previous code checked `opts.noBrowser`, which Cliffy never populates, so the guard silently no-op'd and the browser always opened. Rename to `browser` and check `=== false` explicitly, matching the `wmill app dev --no-open` convention. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(dev): picker warns when wmill dev server is unreachable Track WS state in Dev.svelte (connecting/open/closed) — 'closed' is set on either the WS error or close event. When closed, the picker replaces the toggle + search + tree with a warning Alert telling the user to run `wmill dev` from the workspace root. Toggle and search are hidden rather than rendered disabled because there's nothing to filter anyway. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(cli): rename wmill dev --no-browser to --no-open Match the pre-existing `wmill app dev --no-open` flag. Having `--no-browser` on one dev command and `--no-open` on the other was just an oversight from my earlier change. All three launch.json templates (init, flow new, sync pull) switch to `--no-open`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(cli): wmill init creates root .claude/launch.json for the picker Adds a workspace-root .claude/launch.json so Claude Code can launch `wmill dev` from the project root and land on the file picker (no --path → picker mode). Per-flow and per-raw_app launch.json files are already generated by the existing scans. Skipped (with a gray log) if the file already exists, so the user's customizations are preserved. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(cli): add skipClaudeAssets wmill.yaml flag When `skipClaudeAssets: true` is set in wmill.yaml, all generators that previously wrote Claude-specific assets become no-ops: - writeAiGuidanceFiles skips CLAUDE.md and .claude/skills/ (AGENTS.md is still written — vendor-neutral) - wmill init skips the root .claude/launch.json + per-flow + per-raw_app launch.json scans - wmill sync pull skips the per-flow + per-raw_app launch.json scans - wmill flow new skips the new flow's .claude/launch.json - wmill app new skips the new raw_app's .claude/ folder + launch.json The flag is added to SyncOptions, DEFAULT_SYNC_OPTIONS, and the generated wmill.yaml template (commented out — opt-in). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(cli): wmill init removes existing Claude assets when skipClaudeAssets is set Re-running `wmill init` with `skipClaudeAssets: true` now removes previously-generated Claude assets so the workspace state matches the config. Narrow scope, no confirmation: - per-flow / per-raw_app .claude/launch.json (each parent .claude/ collapsed if empty) - root .claude/launch.json - .claude/skills/ (wholly ours; safe to remove the subtree) - root .claude/ collapsed if empty - CLAUDE.md only if its content matches the default ("Instructions are in @AGENTS.md\n"); otherwise left in place with a note Each removal is logged in yellow under a single gray intro line that prints lazily on the first removal — a clean tree stays silent. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(cli): wmill workspace add browser open silently no-ops `open.openApp(open.apps.browser, { arguments: [url] })` resolves its Promise even when the OS-level launch does nothing, so the CLI prints "Opened browser for you" but no tab appears. Same pattern was already fixed in `dev.ts` by commit3272c29c2e— use `open.default(url)`, which delegates to the native URL opener (`open` on macOS, `xdg-open` on Linux, `start` on Windows) and actually rejects on failure. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(cli): wmill init workspace prompt no longer duplicates active profile name Cliffy's Select.prompt renders `default: X` as `(X)` next to the question header, which duplicates whichever workspace name the default points to. Drop `default` and instead reorder the list so the active profile is first (cursor-preselected by virtue of position) and append "— active" to its label so the indicator lives where it's contextually relevant. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(skills): expand preview-vs-run guidance for write-flow + all write-script-* skills Both `wmill flow preview/run` and `wmill script preview/run` have the same intent split — preview hits the local file, run hits the deployed version, sync push deploys. The skills' "after writing" sections used to terse-list the commands and just say "do not run them yourself", which encouraged the wrong reflex of `sync push` + `run` to "test". Rewrite the section in both `system_prompts/base/flow-base.md` (drives write-flow) and the `script_cli_intro` block in `system_prompts/generate.py` (drives all write-script-<lang>) to: - explicitly list `preview` as the default for local iteration, - spell out the few cases when `run` or `sync push` are appropriate, - offer to test as a one-sentence next step (no multi-option menus), - mark `preview` as safe to run autonomously. Regenerate auto-generated/ + cli/src/guidance/skills.ts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(cli): wmill dev — clearer mode names and accurate startup messaging - Rename `startLegacyServer` to `startDirectServer`. "Legacy" implied it was on the way out; the two modes (proxy vs direct WS) actually serve different topologies and both stay. Add comments above each section spelling out who they're for: proxy mode for embedders that require a localhost origin (Claude Code preview), direct mode for standalone browser tabs and the VS Code extension iframe. - Replace the stale "Dev server will automatically point to the last script edited locally" log line. Now print path-aware text: - with --path (or auto-detected): "Watching <path> — edits will live -reload in the dev page" - without: "Open the dev page and pick a flow or script to preview — edits will live-reload" plus a hint about --path Mirror the same in proxy mode after the listen callback. - Drop the redundant "Go to <url>" line when --no-open isn't passed (maybeOpenBrowser already prints "Opened browser at <url>"). - Rename "Server listening on port 3001" to "Dev WebSocket listening on ws://localhost:<port>/ws" so the line's purpose is obvious. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(cli): drop per-folder .claude/launch.json generation Stop creating `.claude/launch.json` inside every flow folder, raw_app folder, and at `wmill flow new`/`wmill app new` time. The workspace- root `.claude/launch.json` from `wmill init` stays — it's the picker entry point and the one place where the deterministic "click → preview" UX is high-value. Removed from: - `wmill init` — per-flow + per-raw_app scans - `wmill sync pull` — per-flow + per-raw_app scans (also drops the now-unused `node:fs` mkdirSync/writeFileSync import) - `wmill flow new` — bootstrap no longer scaffolds `.claude/` - `wmill app new` — same; also drops the `.claude/launch.json` lines from the post-create directory listing Skills already give the agent the right CLI commands, so per-folder launch.json was redundant context. Existing files in user projects keep working but won't be regenerated; `wmill init` with `skipClaudeAssets: true` cleans them up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(cli): wmill app new flags + tighten raw-app skill for AI agents `wmill app new` is interactive by default, which hangs forever when an AI agent tries to use it. Add flags so the wizard can be bypassed end-to-end: - `--summary <text>`, `--path <path>`, `--framework <react19|react18| svelte5|vue>` (required for non-interactive) - `--datatable <name>` (opt into the datatable wizard) - `--schema <name>` (creates schema with CREATE SCHEMA IF NOT EXISTS if it doesn't already exist; only valid with --datatable) - `--overwrite` (replace existing directory without prompting) - `--no-open-in-desktop` (suppress the Claude Desktop offer) Mode is auto-detected: providing any of --summary/--path/--framework puts the run into non-interactive mode where the datatable wizard, overwrite prompt, and Claude Desktop prompt all skip silently (or fail fast on conflict instead of waiting for stdin). Each provided flag is validated upfront with a clear error message. Skill side: rewrite `system_prompts/base/raw-app.md`'s "Creating a Raw App" section so the AI agent knows it should run the command itself with flags (not tell the user to run it interactively). Direct the agent to use `AskUserQuestion` with one bundled call to gather any missing summary/path/framework — refuse to invent values, refuse to default. Anti-patterns spelled out explicitly. AGENTS.md template (`cli/src/guidance/core.ts`) had a contradicting line ("MUST ask the user to run wmill app new in its terminal first") that was loaded eagerly into agent context and overrode the skill — replaced with the same agent-driven guidance, pointing to the raw-app skill for the full procedure. Regenerate auto-generated/ + cli/src/guidance/skills.ts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(cli): per-target preview launch.json + agent uses wmill flow new Refactor the agent's dev/preview workflow: - Drop root .claude/launch.json generation from `wmill init`. Sharing one generic entry across sessions caused preview collisions; agents now add per-target named entries (windmill: <wmill_path>) on demand. - New `preview` skill in system_prompts/base/preview.md. Branches on whether `mcp__Claude_Preview__*` MCP tools are available: with them, add a per-target launch.json entry pinning its own port + --proxy-port + --path + --no-open and invoke the MCP preview tool; without them, start `wmill dev --path <X> --no-open` directly and hand the URL the CLI prints to the user. Never touch launch.json in the direct case. - Agents must run `wmill flow new <path>` themselves to scaffold flows (folder + flow.yaml with the right suffix), parallel to the existing `wmill app new` rule. Missing path/summary trigger AskUserQuestion; no inventing values. - write-flow skill: 4-step Creating a Flow procedure that opens the visual preview *before* editing flow.yaml so the user watches the flow take shape via live reload. - `wmill flow new` always prints the `wmill dev --path <X>` preview hint; drop the Claude CLI/Desktop detection branches. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(skills): open app preview before editing in raw-app skill Mirrors the flow skill's Step 3 — opening `wmill app dev` via the preview skill before touching App.tsx so the user watches the app take shape via live reload, instead of seeing the finished result at the end. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(dev): guard WS replaceFlow with lockChanges to prevent echo The postMessage handler at Dev.svelte:306-312 wraps replaceFlow with lockChanges = true (cleared 500 ms later) so the $effect on flowStore.val doesn't immediately re-serialize and re-send the freshly received payload. The WebSocket handler did not, so on the initial flow push (dev.ts:568-574 sends currentLastEdit on connect), the client would echo back to handleFlowRoundTrip, which runs the orphan-file scan. On content equality the write was a no-op, but the scan could still delete files the server did not list. Mirror the same lockChanges/timeout pattern in the WS replaceData handler. Apply to both flow and script paths for symmetry. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(cli): correct wmill dev description + gate broadcasts server-side Two related fixes: 1. The 'auto-pushes them to the remote workspace' wording in the wmill dev description was wrong — the command never deploys, it only broadcasts file changes over WS for live preview. Reworded to call this out explicitly and point at 'wmill sync push' for the deploy case. 2. Move the path filter out of the client (Dev.svelte:491-495) and into broadcastChanges. Earlier the filter was client-side with the comment 'server stays a dumb broadcaster' even though commit3c2d5155was titled 'gate dev broadcasts by --path'. Doing the compare server-side aligns the implementation with the commit narrative, cuts WS traffic when --path is set, and keeps the per-tab semantics for the picker (each picker tab still gets the full 'paths' listing on first connect). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(cli): drop dead launch.json cleanup, fix description, narrow orphan scope Three review fixes: 1. cleanupClaudeAssets removed both root and per-folder .claude/launch.json files that this CLI never generates anymore. Per the user's "feature hasn't been released yet" guidance, no migration is needed — drop the dead scan and the root rm. Also drop the now-unused nonDottedPaths argument (and its flowSuffix / rawAppSuffix locals). 2. The skipClaudeAssets description in template.ts listed .claude/launch.json among the assets it skips, but launch.json is no longer generated. Drop it from the description string. 3. The dev round-trip's orphan cleanup deleted any non-dot file in a flow folder that wasn't in extractedPaths — including README.md, fixtures, TODO.md, etc. Restrict the deletion to files whose extension is in a known inline-script set (.ts/.js/.py/.go/.sh/.sql/.ps1/.php/.rs/.java/.cs/.r/.graphql). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(cli, frontend): dedupe flow suffix helpers, use UI components Five small follow-ups from the PR review: 1. dev.ts already had stripFolderSuffix() but three callsites were reimplementing the same .flow/__flow if-else inline. Add an isFlowFolderName(name) helper next to it and replace the duplicates in startProxyServer's cwd check, the file-watcher localPath strip, and normalizeWmPath. 2. Dev.svelte:866 was a <div onclick> with two svelte-ignore comments for the missing a11y handlers. Replace with a real <button type="button"> — kills the warnings, no visual change. 3. Dev.svelte:1283 was a raw <input type="text"> for the module summary. Replace with the existing <TextInput> component (same one the picker search at :1010 uses), per frontend/CLAUDE.md. 4. Dev.svelte:197 typed relativePaths as any[]; tighten to the actual union (string | [number, string])[] — the python helper returns tuples, the typescript one returns strings. 5. app/new.ts:822 fired exec("open <deeplink>") with no callback, so an OS that refused the URL scheme silently failed and we still logged "Opened in Claude Desktop!". Move the success log inside an exec callback that surfaces the error and prints the deep link for manual opening. Plus a brief comment above parseWatchPath explaining its resync contract (initial load + popstate + explicit pickPath, no generic pushState listener). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(cli): regenerate auto-gen for dev description; drop apostrophe to satisfy parser generate.py:326 extracts .description() with the regex [^"\']+ which bails on either quote type. Commit ff3a8e4ebd's new description had an apostrophe inside double quotes ('wmill sync push'), so the parser saw no description at all and the auto-generated files dropped the line entirely — which is what check-freshness caught on origin/main. Quickest path to green CI: rephrase the description without the inner apostrophe, then regenerate. The generator's regex is the real bug but fixing it is out of scope here. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(cli): seed app .claude/launch.json before opening Claude Desktop When the user accepts "Open in Claude Desktop?" in wmill app new, write a per-app .claude/launch.json (named "windmill: <appPath>") into the freshly-created app folder before the deep link fires. Entry runs 'wmill app dev --no-open --port ${PORT:-4001}' from the app folder (which is the cwd Claude Desktop opens with), so the user can hit play right away to launch the preview. Skip if .claude/launch.json already exists — never clobber user edits. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix toggles positions * fix(dev): gate picker mode on ?local= so VS Code iframe still renders content The VS Code extension iframe loads the dev page without ?path= and without ?local=true. After the picker rework, an empty watchPath flipped pickerMode on, so the page rendered the picker UI even though the extension was sending replaceScript / replaceFlow postMessages — leaving the user stuck on the picker forever. Picker mode only makes sense on the local dev page, where the wmill dev WebSocket can supply the workspace listing. Anywhere else (VS Code iframe, plain remote tab) the picker has no data source and no purpose. Add an isLocalDevPage check so the picker only shows when ?local=true is present. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(dev): mirror vscode extension's processFlowMessage round-trip Three changes that bring our wmill dev round-trip into lockstep with the windmill-vscode extension's processFlowMessage in src/extension.ts: 1. New cli/src/commands/dev/pathscript-restore.ts — verbatim port of the extension's src/utils/pathscript-restore.ts. Adds AI-agent tool walking that the previous local copy was missing (flows with PathScript-shaped tools weren't being preserved across round-trip). Header comment makes the cross-repo link explicit. 2. handleFlowRoundTrip rewritten to mirror processFlowMessage step- for-step: reads failure_module + preprocessor_module from the current flow.yaml, passes them to extractCurrentMapping, shares one pathAssigner across all extraction calls, extracts inline scripts from those special modules too, skips writing files whose content starts with !inline (treats as pointer directives), and only rewrites flow.yaml when the serialized YAML actually differs. 3. snapshotPathScripts / tagReplacedPathScripts callsites in loadPaths were passing the FlowFile wrapper instead of FlowFile.value — the helpers walk .modules / .failure_module / .preprocessor_module, which only exist on .value, so PathScript snapshots silently no-op'd on the file-watcher path. Pass .value at all four sites. Deliberate divergence from the extension: orphan-cleanup keeps the INLINE_SCRIPT_EXTS allow-list so README.md / fixtures aren't deleted. The extension's version still over-deletes; that's tracked separately. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(skills): offer visual preview after create instead of auto-opening Both write-flow and raw-app skills used to instruct the agent to open the visual preview without asking right after wmill flow new / wmill app new, on the rationale that live reload is most useful when the page is already up. In practice this surprised users — opening the dev page has side effects (browser window pop, possibly a launch.json entry under MCP-preview Branch A) that warrant consent. Change Step 3 in both skills from "open it without asking" to "offer it as a one-sentence next step" — same pattern the same skills already use for programmatic wmill flow preview offers. Two then- necessary anti-patterns ("just open it", "open it before editing") are dropped along with the auto-open instruction. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(cli): probe both ip stacks before binding wmill dev proxy / app dev port Node's default listen() has platform-dependent dual-stack behaviour. If the requested port is already held on the IPv4 stack, listen() can silently fall back to binding IPv6-only ([::1]:N). The OS then routes new localhost connections to the older IPv4 listener, so the user opens http://localhost:N and sees a stale prior server with no signal that anything is wrong. Bit us in practice: a leftover wmill dev --proxy-port 4000 served traffic for a freshly-started wmill app dev --port 4000. New helper at cli/src/utils/port-probe.ts probes both 0.0.0.0 and :: before binding. On collision it walks upward to the next free port (up to +20) and logs a prominent warning naming the holder when lsof / ss can find it: Port 4000 is already in use (held by PID 91418 `bun`). Using port 4001 instead. Wired into: - wmill dev --proxy-port: the resolved port flows into both proxyServer.listen() and the &port=N parameter in the redirect URL, so they always match. Bind explicitly to 0.0.0.0. - wmill app dev --port: only when the user passed --port explicitly (the default getPort.default(...) path already handles fallback). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(dev): pass placeholder via TextInput inputProps not as top-level prop `<TextInput>`'s top-level Props don't include `placeholder` — native input attributes go through the `inputProps` field. The previous `<TextInput placeholder="Summary" .../>` failed `npm run check` with "Object literal may only specify known properties, and '\"placeholder\"' does not exist in type 'Props<\"input\">'.". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * nit * fix(cli): sequential port probe + sync dev test regex with renamed log Two CI regressions on test-linux: 1. port-probe parallel race on Linux. isPortFreeOnBothStacks ran the IPv4 and IPv6 binds via Promise.all. On Linux the default is net.ipv6.bindv6only=0, so a bind(::, port) socket also takes the IPv4 stack on the same port. Concurrent v4 + v6 binds then race for v4 — one wins, the other gets EADDRINUSE on a port that is actually free. Walks 20 ports up, all fail the same way, throws, child exits. Tests that fetch http://localhost:port time out at 60s. Doesn't repro on macOS (bindv6only=1 by default — what I tested against). Probe sequentially so each bind fully releases before the next starts. 2. dev_server.test 1 regex out of sync. Commit018dc3861arenamed the startup log from "Server listening on port N" to "Dev WebSocket listening on ws://localhost:N/ws" but didn't update the test, which times out at 30s waiting for the old string. Update the regex to match the current log. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Update system_prompts/auto-generated/skills/write-script-graphql/SKILL.md Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> * fix(cli): address dev/app PR review — bugs 1-7 Per code review: 1. app new.ts — wrap claude --session-id exec in try/finally so the spinner setInterval is always cleared. On rejection control jumped to the outer catch and the spinner kept writing \r forever, garbling subsequent output. 2. app new.ts — make --overwrite actually wipe the dir before re-creating. Previously logged "Overwriting" but only skipped the prompt; leftover files from a different framework (e.g. App.tsx from a prior react18 install when re-scaffolding as svelte5) survived and produced a hybrid scaffold. 3. dev/dev.ts — anchor the flow-folder match on path segments. The substring checks (cpath.includes(".flow/") / "__flow/") also fired on names like notes_about__flow_design/readme.md. New isInsideFlowFolder + findFlowFolderPrefix split on "/" and check segment suffixes. Drops the now-unreachable script→flow fallback inside the else branch. 4. dev/dev.ts — direct mode also routes through resolveBindPort so it detects dual-stack collisions like the proxy mode does. Bare getPort only probes one stack, defeating the whole point of port-probe.ts. Also bind to BIND_HOST explicitly. Drops the unused getPort import. 5. dev/dev.ts — normalize opts.path once after mergeConfigWithConfigFile. broadcastChanges compared against a non-normalized opts.path, so --path f/foo/ or --path f/foo.flow silently dropped every broadcast. Also pulls normalizeWmPath to module scope (was a closure inside dev()). 6. dev/dev.ts — guard the initial-state ws.send with readyState === OPEN, matching the other branches' pattern. 7. dev/dev.ts — typo: "givena" → "given a". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(cli): address dev/app PR review — items 8-10 8. dev/dev.ts — derive INLINE_SCRIPT_EXTS from exts so adding a new script language to script.ts auto-extends orphan cleanup. Previously .gql, .nu, .rb were missing — flows using those languages would leave orphaned inline files behind. Excludes .yml because user fixtures commonly use it in flow folders, and leaving a stale .playbook.yml inline script is preferable to deleting a fixture. Keeps .js for hand-written flows that aren't in the exts list. 9. app/new.ts — wrap Claude Desktop install probe + prompt in process.platform === "darwin". The probe (ls /Applications/Claude.app) and the open command both only work on macOS — the explicit guard makes the platform scope grep-able. 10. app/new.ts — switch the deep-link spawn from exec(`open ${shell- escaped url}`) to execFile("open", [deepLink]). sessionId is a UUID and absAppDir is URI-encoded today so the old form was safe, but execFile removes the shell entirely. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(cli,dev): cubic review — port-probe error semantics, pluralize spacing [2] cli/src/utils/port-probe.ts — distinguish IPv6-unsupported from port collision in isPortFree. Previously every error code returned false, including EAFNOSUPPORT / EADDRNOTAVAIL on the IPv6 probe when the host has no v6 stack at all (IPv4-only containers). resolveBindPort would then walk all 20 ports getting the same error and throw. Treat only EADDRINUSE / EACCES as "not free"; everything else as free. [13] cli/src/commands/app/dev.ts — only probe both stacks when binding to localhost. The dual-stack collision risk is specific to localhost (which resolves to 127.0.0.1 + ::1); for an explicit IPv4 host there's only one stack to worry about, so don't move the user's requested port over a phantom v6 collision. [14] frontend/src/lib/components/Dev.svelte — pluralize already inserts a space between quantity and word, so " item" produced "3 items". Drop the leading space in both call sites. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(system_prompts): cubic review — preview args, skill scopes Source changes in base/ + generate.py, then regenerated auto-generated/ via python system_prompts/generate.py. Per cubic review: [4]+[7] generate.py — "pick plausible args from the `main` signature" was language-blind. SQL queries and Bash scripts use $1/$2 positional parameters, not a main(...) signature. Reword to call out both shapes explicitly so the wording survives across all 19 generated language skills (postgresql, bash, mysql, …) instead of just the ones that happen to have main(). [5] base/raw-app.md — the "CLI Commands" table said "Tell the user they can run these commands (do NOT run them yourself)" while the "Creating a Raw App" section above (added in this PR) tells the agent to run `wmill app new` itself. Carve `wmill app new` out of the table and add a one-line note pointing back to the create flow, so the guidance no longer self-contradicts. [10] base/preview.md — "These print a `Go to <url>` line on stdout" was wrong for `wmill app dev`, which prints "🚀 Dev server running at <url>". List both line shapes explicitly and suggest a loose http:// match for URL capture. [12] base/flow-base.md — "regenerate lock files for the flow you modified" misstated the default scope. `wmill generate-metadata` scans scripts, flows, and apps by default (see cli/src/commands/generate-metadata/generate-metadata.ts:71-73). Update wording to call out the default scope and how to narrow it. Also folds the cubic [1] graphql safety wording (originally a one-off edit on the auto-generated file ina895db7) back into generate.py itself, so it survives regeneration and applies to all language skills. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(system_prompts): cubic round 2 — language-specific placeholder syntax Round 1 wording was too narrow: - "$1, $2 placeholders for SQL queries and Bash" was wrong for MySQL (`?`), Snowflake (`?`), MSSQL (`@P1`), BigQuery (`@name`), and PowerShell (which uses `param(...)`, not main()). - The preview-skill URL match said "first `http://...` token" — remote workspaces serve HTTPS, so the regex would miss them. Source-only fixes in generate.py and base/preview.md, then regenerated auto-generated/ via python system_prompts/generate.py. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(flow): track maxHeight in FlowGraphV2 height effect cubic [3]: updateHeight() reads both minHeight and maxHeight, but the $effect only tracked minHeight. Changing maxHeight alone (e.g. when a parent shrinks the cap during a layout transition) left height frozen at the previously clamped value. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(system_prompts): tool-agnostic wording in base/ cubic [11]: system_prompts/README.md says these prompts must NOT contain tool usage instructions. Three base files violated this: - base/flow-base.md (4× AskUserQuestion). Worst offender — leaks into the frontend copilot via FLOW_BASE in prompts.ts (consumed by getFlowPrompt in frontend/src/lib/components/copilot/chat/flow/ core.ts:1287). Frontend has no AskUserQuestion tool, so the wording was both irrelevant and confusing there. - base/raw-app.md (5× AskUserQuestion + 1× mcp__Claude_Preview__). CLI-skill-only but covered by the same scope rule. - base/preview.md (5× mcp__Claude_Preview__). CLI-skill-only, same. Replaced with role descriptions: "ask the user (use a structured- question tool if your runtime has one)" and "a tool that can embed a localhost URL inside the IDE / chat surface". Kept one mention of mcp__Claude_Preview__ in preview.md as an illustrative example, since documentation of one runtime is fine — what's not fine is gating behaviour on a specific tool name. Source-only edits, then regenerated auto-generated/ via python system_prompts/generate.py. Verification: grep -r AskUserQuestion system_prompts/auto-generated/ now returns nothing. The remaining AskUserQuestion refs in cli/src/guidance/core.ts are hand-written CLI-only AGENTS.md content (not part of system_prompts), and Claude Code does have that tool, so those are correctly scoped. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(flow): drop .no-splitter CSS hack, use snippets to skip Splitpanes cubic [8]: the previous fix for "top pane is empty in aiagent / noEditor mode" was a CSS rule that hid `:global(.splitpanes__splitter)` inside .no-splitter. That cascaded into nested splitpanes too — the aiagent left/right tabs panel (line 1043), the debug-console editor split (line 877), and the doubly-nested debug panel (line 1472) all lost their resize handles. Refactor the layout instead. Extract top-pane and bottom-pane content as snippets, then conditionally render either: - just the bottom snippet (no Splitpanes wrapper) when the top pane would be empty (aiagent or noEditor), or - the original two-Pane Splitpanes layout otherwise. This removes the splitter at its root rather than hiding it, so nested splitters are unaffected. The bottom Pane's complex bind:size getter/setter (which returned 100 when aiagent) collapses to a simple binding now that the aiagent path no longer goes through the wrapping Pane at all. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * nit * fix(flow,preview): cubic round 3 — FlowPathViewer regression + preview skill rewrite [3149192182] FlowModuleComponent.svelte: my last refactor's "aiagent || noEditor" condition stripped the FlowPathViewer for noEditor + type === 'flow', because the top-pane snippet was no longer rendered. The flow-viewer pane is the only thing that *does* show in that mode, so it shouldn't have been collapsed. Tighten the condition to "aiagent || (noEditor && type !== 'flow')". [3149060930] system_prompts/base/preview.md: Branch A detection was too broad — "can embed or open a localhost URL" is strictly weaker than "can read .claude/launch.json and launch a configuration". Only the Claude Desktop / Code MCP integration does the latter; most embedders only do the former. Restructure preview.md around two orthogonal axes: 1. Mode (proxy vs direct) — driven by "does the embedder need a localhost URL?". Direct is the default; proxy is for embedders that sandbox cross-origin loads. 2. Who starts the server — you spawn `wmill dev` yourself, OR a launch.json-aware runtime (currently only the `mcp__Claude_Preview__*` MCP family) launches it on demand. The two compose into four common cases (regular browser tab, generic preview pane, localhost-only preview pane, Claude MCP), each with a clear instruction. The launch.json/MCP machinery is now scoped to a single section gated on actually having that tool in your tool list. Source-only edit in base/preview.md, then regenerated auto-generated/ via python system_prompts/generate.py. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Fix dev step display * nit --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
cubic-dev-ai[bot]
parent
e636f589a5
commit
eebe24d8b0
@@ -5,6 +5,7 @@ import { sep as SEP } from "node:path";
|
||||
import * as windmillUtils from "@windmill-labs/shared-utils";
|
||||
import { yamlParseFile } from "../../utils/yaml.ts";
|
||||
import * as getPort from "get-port";
|
||||
import { resolveBindPort } from "../../utils/port-probe.ts";
|
||||
import * as open from "open";
|
||||
import { GlobalOptions } from "../../types.ts";
|
||||
import * as http from "node:http";
|
||||
@@ -389,11 +390,23 @@ async function dev(opts: DevOptions, appFolder?: string) {
|
||||
// Dynamically import esbuild only when the dev command is called
|
||||
const esbuild = await import("esbuild");
|
||||
|
||||
const port = opts.port ??
|
||||
(await getPort.default({
|
||||
port: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((p) => p + DEFAULT_PORT),
|
||||
}));
|
||||
const host = opts.host ?? DEFAULT_HOST;
|
||||
// Probe both IPv4 and IPv6 stacks only when binding to localhost — that's
|
||||
// the case where the OS may route traffic to a leftover listener on the
|
||||
// other stack (see cli/src/utils/port-probe.ts). For an explicit IP host
|
||||
// there's only one stack to worry about, so don't move the user's
|
||||
// requested port over a phantom v6 collision.
|
||||
const probeBothStacks = host === DEFAULT_HOST;
|
||||
const port = opts.port !== undefined
|
||||
? (probeBothStacks
|
||||
? await resolveBindPort(opts.port, "--port", {
|
||||
info: (m) => log.info(m),
|
||||
warn: (m) => log.warn(m),
|
||||
})
|
||||
: opts.port)
|
||||
: await getPort.default({
|
||||
port: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((p) => p + DEFAULT_PORT),
|
||||
});
|
||||
const shouldOpen = opts.open ?? true;
|
||||
|
||||
// Detect frameworks to determine default entry point
|
||||
|
||||
+296
-44
@@ -1,4 +1,4 @@
|
||||
import { stat, writeFile, mkdir } from "node:fs/promises";
|
||||
import { stat, writeFile, mkdir, rm } from "node:fs/promises";
|
||||
import { Command } from "@cliffy/command";
|
||||
import { colors } from "@cliffy/ansi/colors";
|
||||
import { Confirm } from "@cliffy/prompt/confirm";
|
||||
@@ -12,6 +12,7 @@ import { resolveWorkspace } from "../../core/context.ts";
|
||||
import { requireLogin } from "../../core/auth.ts";
|
||||
import * as wmill from "../../../gen/services.gen.ts";
|
||||
import path from "node:path";
|
||||
import { execSync, exec, execFile } from "node:child_process";
|
||||
import {
|
||||
buildFolderPath,
|
||||
loadNonDottedPathsSetting,
|
||||
@@ -99,7 +100,18 @@ import "./index.css";
|
||||
|
||||
createApp(App).mount('#root')`;
|
||||
|
||||
const indexCss = `.myclass {
|
||||
const indexCss = `body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background-color: #f5f5f5;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
#root {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.myclass {
|
||||
border: 1px solid gray;
|
||||
padding: 2px;
|
||||
}`;
|
||||
@@ -239,7 +251,26 @@ interface DataConfig {
|
||||
schema?: string;
|
||||
}
|
||||
|
||||
async function newApp(opts: GlobalOptions) {
|
||||
type FrameworkKey = "react19" | "react18" | "svelte5" | "vue";
|
||||
|
||||
interface NewAppOptions extends GlobalOptions {
|
||||
/** App summary (short description). Skips the prompt when provided. */
|
||||
summary?: string;
|
||||
/** App path (e.g., `f/folder/my_app`). Skips the prompt when provided. */
|
||||
path?: string;
|
||||
/** Framework template. Skips the prompt when provided. */
|
||||
framework?: FrameworkKey;
|
||||
/** Datatable name to wire up. Skip the datatable wizard entirely if not provided. */
|
||||
datatable?: string;
|
||||
/** Schema to create when --datatable is set. If omitted, no schema is created. */
|
||||
schema?: string;
|
||||
/** Overwrite the target directory if it already exists, without prompting. */
|
||||
overwrite?: boolean;
|
||||
/** Suppress the "Open in Claude Desktop?" prompt. */
|
||||
openInDesktop?: boolean;
|
||||
}
|
||||
|
||||
async function newApp(opts: NewAppOptions) {
|
||||
log.info(colors.bold.cyan("Create a new Windmill Raw App"));
|
||||
log.info("");
|
||||
|
||||
@@ -284,17 +315,26 @@ async function newApp(opts: GlobalOptions) {
|
||||
);
|
||||
}
|
||||
|
||||
// Ask for summary
|
||||
const summary = await Input.prompt({
|
||||
message: "App summary (short description):",
|
||||
minLength: 1,
|
||||
validate: (value: string) => {
|
||||
if (value.trim().length === 0) {
|
||||
return "Summary cannot be empty";
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
// Ask for summary (skipped if --summary is provided)
|
||||
let summary: string;
|
||||
if (opts.summary !== undefined) {
|
||||
if (opts.summary.trim().length === 0) {
|
||||
log.error(colors.red("--summary cannot be empty"));
|
||||
return;
|
||||
}
|
||||
summary = opts.summary;
|
||||
} else {
|
||||
summary = await Input.prompt({
|
||||
message: "App summary (short description):",
|
||||
minLength: 1,
|
||||
validate: (value: string) => {
|
||||
if (value.trim().length === 0) {
|
||||
return "Summary cannot be empty";
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Build suggestions for path autocompletion
|
||||
const buildPathSuggestions = (input: string): string[] => {
|
||||
@@ -318,32 +358,55 @@ async function newApp(opts: GlobalOptions) {
|
||||
return suggestions;
|
||||
};
|
||||
|
||||
// Ask for path with validation
|
||||
// Ask for path with validation (skipped if --path is provided)
|
||||
let appPath: string;
|
||||
while (true) {
|
||||
appPath = await Input.prompt({
|
||||
message: "App path (e.g., f/my_folder/my_app or u/username/my_app):",
|
||||
minLength: 1,
|
||||
suggestions: buildPathSuggestions,
|
||||
});
|
||||
if (opts.path !== undefined) {
|
||||
const validation = validateAppPath(opts.path);
|
||||
if (!validation.valid) {
|
||||
log.error(colors.red(`Invalid --path: ${validation.error}`));
|
||||
return;
|
||||
}
|
||||
appPath = opts.path;
|
||||
} else {
|
||||
while (true) {
|
||||
appPath = await Input.prompt({
|
||||
message: "App path (e.g., f/my_folder/my_app or u/username/my_app):",
|
||||
minLength: 1,
|
||||
suggestions: buildPathSuggestions,
|
||||
});
|
||||
|
||||
const validation = validateAppPath(appPath);
|
||||
if (validation.valid) {
|
||||
break;
|
||||
const validation = validateAppPath(appPath);
|
||||
if (validation.valid) {
|
||||
break;
|
||||
}
|
||||
log.error(colors.red(`Invalid path: ${validation.error}`));
|
||||
}
|
||||
log.error(colors.red(`Invalid path: ${validation.error}`));
|
||||
}
|
||||
|
||||
// Ask for framework
|
||||
const framework = await Select.prompt({
|
||||
message: "Select a framework:",
|
||||
options: [
|
||||
{ name: "React 19 (Recommended)", value: "react19" },
|
||||
{ name: "React 18", value: "react18" },
|
||||
{ name: "Svelte 5", value: "svelte5" },
|
||||
{ name: "Vue 3", value: "vue" },
|
||||
],
|
||||
});
|
||||
// Ask for framework (skipped if --framework is provided)
|
||||
const VALID_FRAMEWORKS: FrameworkKey[] = ["react19", "react18", "svelte5", "vue"];
|
||||
let framework: string;
|
||||
if (opts.framework !== undefined) {
|
||||
if (!VALID_FRAMEWORKS.includes(opts.framework)) {
|
||||
log.error(
|
||||
colors.red(
|
||||
`Invalid --framework: ${opts.framework}. Must be one of: ${VALID_FRAMEWORKS.join(", ")}`
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
framework = opts.framework;
|
||||
} else {
|
||||
framework = await Select.prompt({
|
||||
message: "Select a framework:",
|
||||
options: [
|
||||
{ name: "React 19 (Recommended)", value: "react19" },
|
||||
{ name: "React 18", value: "react18" },
|
||||
{ name: "Svelte 5", value: "svelte5" },
|
||||
{ name: "Vue 3", value: "vue" },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
const template = templates[framework];
|
||||
if (!template) {
|
||||
@@ -356,7 +419,48 @@ async function newApp(opts: GlobalOptions) {
|
||||
let createSchemaSQL: string | undefined;
|
||||
let schemaName: string | undefined;
|
||||
|
||||
if (datatables.length > 0) {
|
||||
// Treat the run as non-interactive once any required-for-non-interactive flag is set.
|
||||
// In that mode, skip all datatable/overwrite/desktop prompts unless the user opted in
|
||||
// via the corresponding flag.
|
||||
const nonInteractive =
|
||||
opts.summary !== undefined ||
|
||||
opts.path !== undefined ||
|
||||
opts.framework !== undefined;
|
||||
|
||||
if (opts.datatable !== undefined) {
|
||||
// Non-interactive datatable + (optional) schema configuration
|
||||
if (datatables.length > 0 && !datatables.includes(opts.datatable)) {
|
||||
log.warn(
|
||||
colors.yellow(
|
||||
`--datatable '${opts.datatable}' is not in the workspace's datatable list (${datatables.join(", ")}). Continuing anyway.`
|
||||
)
|
||||
);
|
||||
}
|
||||
dataConfig.datatable = opts.datatable;
|
||||
if (opts.schema !== undefined) {
|
||||
if (!/^[a-z_][a-z0-9_]*$/.test(opts.schema)) {
|
||||
log.error(
|
||||
colors.red(
|
||||
`--schema must start with a letter or underscore and contain only lowercase letters, numbers, and underscores: ${opts.schema}`
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
schemaName = opts.schema;
|
||||
dataConfig.schema = schemaName;
|
||||
const existingSchemas = datatableSchemas.get(opts.datatable) ?? [];
|
||||
if (!existingSchemas.includes(schemaName)) {
|
||||
// Emit creation SQL only if the schema doesn't already exist
|
||||
createSchemaSQL = `-- Create schema for ${summary}
|
||||
-- This will be executed when you run 'wmill app dev' and confirm in the modal
|
||||
CREATE SCHEMA IF NOT EXISTS ${schemaName};
|
||||
`;
|
||||
}
|
||||
}
|
||||
dataConfig.tables = [];
|
||||
} else if (nonInteractive) {
|
||||
// Non-interactive run with no --datatable → skip datatable config silently
|
||||
} else if (datatables.length > 0) {
|
||||
log.info("");
|
||||
log.info(colors.bold.cyan("Data Configuration"));
|
||||
log.info(
|
||||
@@ -481,19 +585,37 @@ CREATE SCHEMA IF NOT EXISTS ${schemaName};
|
||||
const appDir = path.join(process.cwd(), folderName);
|
||||
|
||||
// Check if directory already exists
|
||||
let dirExists = false;
|
||||
try {
|
||||
await stat(appDir);
|
||||
const overwrite = await Confirm.prompt({
|
||||
message: `Directory '${folderName}' already exists. Overwrite?`,
|
||||
default: false,
|
||||
});
|
||||
if (!overwrite) {
|
||||
log.info(colors.yellow("Aborted."));
|
||||
return;
|
||||
}
|
||||
dirExists = true;
|
||||
} catch {
|
||||
// Directory doesn't exist, which is good
|
||||
}
|
||||
if (dirExists) {
|
||||
if (opts.overwrite) {
|
||||
log.warn(colors.yellow(`Overwriting existing '${folderName}' (--overwrite)`));
|
||||
} else if (nonInteractive) {
|
||||
log.error(
|
||||
colors.red(
|
||||
`Directory '${folderName}' already exists. Pass --overwrite to replace it.`
|
||||
)
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
const overwrite = await Confirm.prompt({
|
||||
message: `Directory '${folderName}' already exists. Overwrite?`,
|
||||
default: false,
|
||||
});
|
||||
if (!overwrite) {
|
||||
log.info(colors.yellow("Aborted."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Wipe before re-creating so leftover files from a different framework
|
||||
// (e.g. App.tsx from react18 when re-scaffolding as svelte5) don't survive.
|
||||
await rm(appDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
await mkdir(appDir, { recursive: true });
|
||||
await mkdir(path.join(appDir, "backend"), { recursive: true });
|
||||
@@ -662,10 +784,140 @@ This folder is for SQL migration files that will be applied to datatables during
|
||||
}
|
||||
log.info("");
|
||||
log.info(colors.gray(" 4. wmill sync push (to deploy when ready)"));
|
||||
|
||||
// Offer to open in Claude Desktop. macOS-only for now: the deep-link
|
||||
// handler below uses `open <url>`, and the install path probe checks
|
||||
// /Applications/Claude.app. Both are Mac-specific.
|
||||
let hasClaudeDesktop = false;
|
||||
if (process.platform === "darwin") {
|
||||
try {
|
||||
execSync("ls /Applications/Claude.app", { stdio: "ignore" });
|
||||
hasClaudeDesktop = true;
|
||||
} catch {
|
||||
// Claude Desktop not installed
|
||||
}
|
||||
}
|
||||
|
||||
if (hasClaudeDesktop && !nonInteractive && opts.openInDesktop !== false) {
|
||||
log.info("");
|
||||
const openInDesktop = await Confirm.prompt({
|
||||
message: "Open in Claude Desktop?",
|
||||
default: true,
|
||||
});
|
||||
|
||||
if (openInDesktop) {
|
||||
try {
|
||||
const absAppDir = path.resolve(appDir);
|
||||
|
||||
// Seed the app folder with a launch.json entry pointing at `wmill app dev`
|
||||
// so the freshly-opened Claude Desktop session can launch the preview
|
||||
// directly. Skip if the file already exists — never clobber user edits.
|
||||
const claudeDir = path.join(absAppDir, ".claude");
|
||||
const launchPath = path.join(claudeDir, "launch.json");
|
||||
if (!await stat(launchPath).catch(() => null)) {
|
||||
const launchJson = JSON.stringify({
|
||||
version: "0.0.1",
|
||||
configurations: [{
|
||||
name: `windmill: ${appPath}`,
|
||||
runtimeExecutable: "bash",
|
||||
runtimeArgs: ["-c", "wmill app dev --no-open --port ${PORT:-4000}"],
|
||||
port: 4000,
|
||||
autoPort: true,
|
||||
}],
|
||||
}, null, 2) + "\n";
|
||||
await mkdir(claudeDir, { recursive: true });
|
||||
await writeFile(launchPath, launchJson, "utf-8");
|
||||
log.info(colors.gray(`Seeded ${path.relative(process.cwd(), launchPath)}`));
|
||||
}
|
||||
|
||||
const sessionId = crypto.randomUUID();
|
||||
|
||||
// Create a persisted CLI session with welcome message (async to allow spinner)
|
||||
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
||||
let i = 0;
|
||||
const spinner = setInterval(() => {
|
||||
process.stdout.write(`\r${colors.gray(`${frames[i++ % frames.length]} Creating Claude session...`)}`);
|
||||
}, 80);
|
||||
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
exec(
|
||||
`claude --session-id "${sessionId}" -p "Say: Your app is ready, click on preview to test it!"`,
|
||||
{ cwd: absAppDir },
|
||||
(error) => (error ? reject(error) : resolve())
|
||||
);
|
||||
});
|
||||
} finally {
|
||||
// On exec rejection control jumps to the outer catch — without this
|
||||
// finally the spinner keeps writing to stdout and garbles output.
|
||||
clearInterval(spinner);
|
||||
process.stdout.write("\r" + " ".repeat(40) + "\r");
|
||||
}
|
||||
|
||||
// Import the session into Claude Desktop Code mode. Use execFile so
|
||||
// the deep link doesn't pass through a shell — `sessionId` is a UUID
|
||||
// and absAppDir is URI-encoded inside the URL today, but execFile
|
||||
// removes shell escaping concerns entirely.
|
||||
const deepLink = `claude://resume?session=${sessionId}&cwd=${encodeURIComponent(absAppDir)}`;
|
||||
execFile("open", [deepLink], (err) => {
|
||||
if (err) {
|
||||
log.warn(
|
||||
colors.yellow(
|
||||
`Could not open Claude Desktop deep link (${err.message}). Open it manually: ${deepLink}`
|
||||
)
|
||||
);
|
||||
} else {
|
||||
log.info(colors.bold.green("Opened in Claude Desktop!"));
|
||||
}
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
log.warn(
|
||||
colors.yellow(
|
||||
`Could not open in Claude Desktop: ${errorMessage}`
|
||||
)
|
||||
);
|
||||
log.info(
|
||||
colors.gray(
|
||||
"You can manually run: cd " + folderName + " && claude"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const command = new Command()
|
||||
.description("create a new raw app from a template")
|
||||
.option(
|
||||
"--summary <summary:string>",
|
||||
"App summary (short description). Skips the prompt when provided. Triggers non-interactive mode."
|
||||
)
|
||||
.option(
|
||||
"--path <path:string>",
|
||||
"App path (e.g., f/folder/my_app or u/username/my_app). Skips the prompt when provided. Triggers non-interactive mode."
|
||||
)
|
||||
.option(
|
||||
"--framework <framework:string>",
|
||||
"Framework template: react19 | react18 | svelte5 | vue. Skips the prompt when provided. Triggers non-interactive mode."
|
||||
)
|
||||
.option(
|
||||
"--datatable <datatable:string>",
|
||||
"Datatable to wire up. Without this flag in non-interactive mode, no datatable is configured."
|
||||
)
|
||||
.option(
|
||||
"--schema <schema:string>",
|
||||
"Schema to use with --datatable. Created (CREATE SCHEMA IF NOT EXISTS) if it doesn't already exist."
|
||||
)
|
||||
.option(
|
||||
"--overwrite",
|
||||
"Overwrite the target directory if it already exists, without prompting."
|
||||
)
|
||||
.option(
|
||||
"--no-open-in-desktop",
|
||||
"Do not prompt to open the new app in Claude Desktop."
|
||||
)
|
||||
.action(newApp as any);
|
||||
|
||||
export default command;
|
||||
|
||||
+689
-45
@@ -2,12 +2,13 @@ import { Command } from "@cliffy/command";
|
||||
import * as log from "../../core/log.ts";
|
||||
import { sep as SEP } from "node:path";
|
||||
import { yamlParseFile } from "../../utils/yaml.ts";
|
||||
import { stringify as yamlStringify } from "yaml";
|
||||
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 { realpath } from "node:fs/promises";
|
||||
import { access, readdir, realpath, stat, unlink, writeFile } from "node:fs/promises";
|
||||
import { readTextFile } from "../../utils/utils.ts";
|
||||
import { watch } from "node:fs";
|
||||
import { getTypeStrFromPath, GlobalOptions } from "../../types.ts";
|
||||
@@ -15,6 +16,7 @@ import { ignoreF } from "../sync/sync.ts";
|
||||
import { requireLogin } from "../../core/auth.ts";
|
||||
import { resolveWorkspace } from "../../core/context.ts";
|
||||
import {
|
||||
GLOBAL_CONFIG_OPT,
|
||||
SyncOptions,
|
||||
mergeConfigWithConfigFile,
|
||||
} from "../../core/conf.ts";
|
||||
@@ -23,18 +25,198 @@ import { inferContentTypeFromFilePath } from "../../utils/script_common.ts";
|
||||
import { OpenFlow } from "../../../gen/types.gen.ts";
|
||||
import { FlowFile } from "../flow/flow.ts";
|
||||
import { replaceInlineScripts, replaceAllPathScriptsWithLocal } from "../../../windmill-utils-internal/src/inline-scripts/replacer.ts";
|
||||
import { extractInlineScripts, extractCurrentMapping } from "../../../windmill-utils-internal/src/inline-scripts/extractor.ts";
|
||||
import { newPathAssigner } from "../../../windmill-utils-internal/src/path-utils/path-assigner.ts";
|
||||
import { parseMetadataFile } from "../../utils/metadata.ts";
|
||||
import {
|
||||
getFolderSuffixWithSep,
|
||||
getMetadataFileName,
|
||||
extractFolderPath,
|
||||
getNonDottedPaths,
|
||||
loadNonDottedPathsSetting,
|
||||
} from "../../utils/resource_folders.ts";
|
||||
import * as path from "node:path";
|
||||
import * as fs from "node:fs";
|
||||
import { listSyncCodebases } from "../../utils/codebase.ts";
|
||||
import { createPreviewLocalScriptReader } from "../../utils/local_path_scripts.ts";
|
||||
import { resolveBindPort, BIND_HOST } from "../../utils/port-probe.ts";
|
||||
import {
|
||||
snapshotPathScripts,
|
||||
tagReplacedPathScripts,
|
||||
restorePathScripts,
|
||||
} from "./pathscript-restore.ts";
|
||||
|
||||
const PORT = 3001;
|
||||
async function dev(opts: GlobalOptions & SyncOptions) {
|
||||
|
||||
type WmPathItem = {
|
||||
path: string;
|
||||
kind: "flow" | "script" | "raw_app";
|
||||
summary?: string;
|
||||
};
|
||||
|
||||
const FLOW_SUFFIXES = [".flow", "__flow"] as const;
|
||||
const APP_SUFFIXES = [".app", "__app", ".raw_app", "__raw_app"] as const;
|
||||
|
||||
// Extensions the dev round-trip might have written into a flow folder as
|
||||
// inline scripts. Derived from script.ts's `exts` so adding a new language
|
||||
// there auto-extends orphan cleanup; otherwise stale inline scripts of that
|
||||
// language would silently linger. Excludes `.yml` — user fixtures commonly
|
||||
// use it in flow folders, and leaving a stale `.playbook.yml` inline script
|
||||
// is preferable to deleting a fixture. `.js` is added explicitly for
|
||||
// hand-written flows that aren't in the `exts` list.
|
||||
//
|
||||
// Anything else (README.md, fixtures, .env*, TODO.md…) is preserved during
|
||||
// orphan cleanup so we don't trample user-added files.
|
||||
const INLINE_SCRIPT_EXTS = new Set([
|
||||
// path.extname(".py") === "" (Node treats ".py" as a hidden filename, not
|
||||
// an extension), so prefix with a dummy character before extracting.
|
||||
...exts.map((e) => path.extname("x" + e)).filter((e) => e !== ".yml"),
|
||||
".js",
|
||||
]);
|
||||
|
||||
function stripFolderSuffix(rel: string, suffixes: readonly string[]): string {
|
||||
for (const s of suffixes) {
|
||||
if (rel.endsWith(s)) return rel.slice(0, -s.length);
|
||||
}
|
||||
return rel;
|
||||
}
|
||||
|
||||
function isFlowFolderName(name: string): boolean {
|
||||
return FLOW_SUFFIXES.some((s) => name.endsWith(s));
|
||||
}
|
||||
|
||||
// Normalize a windmill path: strip trailing slash and any flow folder suffix
|
||||
// so f/foo, f/foo/, f/foo.flow, and f/foo.flow/ all compare equal.
|
||||
function normalizeWmPath(p: string): string {
|
||||
return stripFolderSuffix(p.replace(/\/$/, ""), FLOW_SUFFIXES);
|
||||
}
|
||||
|
||||
// Anchor on path segments — substring matches like cpath.includes(".flow/")
|
||||
// also fire on innocent names like "notes_about__flow_design/readme.md".
|
||||
function isInsideFlowFolder(cpath: string): boolean {
|
||||
return cpath.split("/").some(isFlowFolderName);
|
||||
}
|
||||
|
||||
// Return the path prefix up to and including the first flow-folder segment,
|
||||
// with a trailing slash. Returns undefined if no segment matches.
|
||||
function findFlowFolderPrefix(cpath: string): string | undefined {
|
||||
const segs = cpath.split("/");
|
||||
for (let i = 0; i < segs.length; i++) {
|
||||
if (isFlowFolderName(segs[i])) {
|
||||
return segs.slice(0, i + 1).join("/") + "/";
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async function listWorkspacePaths(): Promise<WmPathItem[]> {
|
||||
// Walk first, capturing each item's metadata file path. Then read summaries in
|
||||
// parallel — one tree pass plus N file reads is faster than a serialized walk.
|
||||
const items: (WmPathItem & { _metaPath?: string })[] = [];
|
||||
async function walk(dir: string, rel: string) {
|
||||
let entries;
|
||||
try {
|
||||
entries = await readdir(dir, { withFileTypes: true });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
||||
const childRel = rel ? `${rel}/${entry.name}` : entry.name;
|
||||
const childAbs = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
if (isFlowFolderName(entry.name)) {
|
||||
items.push({
|
||||
path: stripFolderSuffix(childRel, FLOW_SUFFIXES),
|
||||
kind: "flow",
|
||||
_metaPath: path.join(childAbs, "flow.yaml"),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (APP_SUFFIXES.some((s) => entry.name.endsWith(s))) {
|
||||
items.push({ path: stripFolderSuffix(childRel, APP_SUFFIXES), kind: "raw_app" });
|
||||
continue;
|
||||
}
|
||||
await walk(childAbs, childRel);
|
||||
} else if (entry.isFile()) {
|
||||
const matchedExt = exts.find((ext) => entry.name.endsWith(ext));
|
||||
if (matchedExt) {
|
||||
const noExtAbs = childAbs.slice(0, -matchedExt.length);
|
||||
items.push({
|
||||
path: childRel.slice(0, -matchedExt.length),
|
||||
kind: "script",
|
||||
_metaPath: noExtAbs + ".script.yaml",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
await walk(process.cwd(), "");
|
||||
|
||||
await Promise.all(
|
||||
items.map(async (item) => {
|
||||
if (!item._metaPath) return;
|
||||
try {
|
||||
const meta: any = await yamlParseFile(item._metaPath);
|
||||
if (typeof meta?.summary === "string" && meta.summary.length > 0) {
|
||||
item.summary = meta.summary;
|
||||
}
|
||||
} catch {
|
||||
// No metadata file or unparseable — leave summary undefined
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
items.sort((a, b) => a.path.localeCompare(b.path));
|
||||
return items.map(({ _metaPath, ...item }) => item);
|
||||
}
|
||||
|
||||
export interface DevOpts {
|
||||
proxyPort?: number;
|
||||
path?: string;
|
||||
open?: boolean;
|
||||
}
|
||||
|
||||
export async function dev(opts: GlobalOptions & SyncOptions & DevOpts) {
|
||||
// Auto-detect flow folder: if no --path and cwd is a flow folder, resolve path and chdir to workspace root
|
||||
if (!opts.path) {
|
||||
const cwd = process.cwd();
|
||||
const cwdBasename = path.basename(cwd);
|
||||
|
||||
// Need to init nonDottedPaths before checking suffix
|
||||
await loadNonDottedPathsSetting();
|
||||
|
||||
if (isFlowFolderName(cwdBasename)) {
|
||||
GLOBAL_CONFIG_OPT.noCdToRoot = true;
|
||||
|
||||
// Find workspace root
|
||||
let searchDir = cwd;
|
||||
let workspaceRoot: string | undefined;
|
||||
while (true) {
|
||||
const wmillYaml = path.join(searchDir, "wmill.yaml");
|
||||
if (fs.existsSync(wmillYaml)) {
|
||||
workspaceRoot = searchDir;
|
||||
break;
|
||||
}
|
||||
const parentDir = path.dirname(searchDir);
|
||||
if (parentDir === searchDir) break;
|
||||
searchDir = parentDir;
|
||||
}
|
||||
|
||||
if (workspaceRoot) {
|
||||
const relPath = path.relative(workspaceRoot, cwd).replaceAll("\\", "/");
|
||||
opts.path = stripFolderSuffix(relPath, FLOW_SUFFIXES);
|
||||
log.info(`Detected flow folder, path: ${opts.path}`);
|
||||
process.chdir(workspaceRoot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
opts = await mergeConfigWithConfigFile(opts);
|
||||
// Normalize once so broadcastChanges' equality check survives user input
|
||||
// like --path f/foo/ or --path f/foo.flow (and the same set via wmill.yaml).
|
||||
if (opts.path) {
|
||||
opts.path = normalizeWmPath(opts.path);
|
||||
}
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
@@ -71,12 +253,13 @@ async function dev(opts: GlobalOptions & SyncOptions) {
|
||||
});
|
||||
}
|
||||
|
||||
const flowFolderSuffix = getFolderSuffixWithSep("flow");
|
||||
const flowMetadataFile = getMetadataFileName("flow", "yaml");
|
||||
async function loadPaths(pathsToLoad: string[]) {
|
||||
const paths = pathsToLoad.filter((path) =>
|
||||
const paths = pathsToLoad.filter((p) =>
|
||||
exts.some(
|
||||
(ext) => path.endsWith(ext) || path.endsWith(flowFolderSuffix + flowMetadataFile)
|
||||
(ext) => p.endsWith(ext)
|
||||
|| p.endsWith(".flow/" + flowMetadataFile)
|
||||
|| p.endsWith("__flow/" + flowMetadataFile)
|
||||
)
|
||||
);
|
||||
if (paths.length == 0) {
|
||||
@@ -84,11 +267,26 @@ async function dev(opts: GlobalOptions & SyncOptions) {
|
||||
}
|
||||
const nativePath = (await realpath(paths[0])).replace(base + SEP, "");
|
||||
const cpath = nativePath.replaceAll("\\", "/");
|
||||
if (!ignore(nativePath, false)) {
|
||||
const typ = getTypeStrFromPath(cpath);
|
||||
// Bypass ignore for paths inside flow folders — ignore() only checks the configured
|
||||
// suffix (dotted or non-dotted), but the workspace may contain both kinds
|
||||
const insideFlow = isInsideFlowFolder(cpath);
|
||||
if (insideFlow || !ignore(nativePath, false)) {
|
||||
let typ: string;
|
||||
if (insideFlow) {
|
||||
// Force flow type for any file inside a flow folder — getTypeStrFromPath
|
||||
// only recognises the configured suffix (dotted or non-dotted) and would
|
||||
// mis-classify or throw for the other variant
|
||||
typ = "flow";
|
||||
} else {
|
||||
typ = getTypeStrFromPath(cpath);
|
||||
}
|
||||
log.info("Detected change in " + cpath + " (" + typ + ")");
|
||||
if (typ == "flow") {
|
||||
const localPath = extractFolderPath(cpath, "flow")!;
|
||||
// Try extractFolderPath, fallback to segment-anchored extraction for
|
||||
// mixed suffix cases (extractFolderPath only checks the configured suffix).
|
||||
let localPath = extractFolderPath(cpath, "flow") ?? findFlowFolderPrefix(cpath);
|
||||
if (!localPath) return;
|
||||
const wmFlowPath = stripFolderSuffix(localPath.replace(/\/$/, ""), FLOW_SUFFIXES);
|
||||
const localFlow = (await yamlParseFile(
|
||||
localPath + "flow.yaml"
|
||||
)) as FlowFile;
|
||||
@@ -100,24 +298,29 @@ async function dev(opts: GlobalOptions & SyncOptions) {
|
||||
SEP,
|
||||
undefined,
|
||||
);
|
||||
// Replace PathScript modules with local file content so dev mode uses local versions
|
||||
// Snapshot PathScript modules before replacement, then tag after.
|
||||
// Helpers walk `flowValue` (modules/failure_module/preprocessor_module),
|
||||
// so pass `.value`, not the FlowFile wrapper.
|
||||
snapshotPathScripts(localFlow.value);
|
||||
const localScriptReader = createPreviewLocalScriptReader({
|
||||
exts,
|
||||
defaultTs: opts.defaultTs,
|
||||
codebases,
|
||||
});
|
||||
await replaceAllPathScriptsWithLocal(localFlow.value, localScriptReader, log);
|
||||
tagReplacedPathScripts(localFlow.value);
|
||||
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 readTextFile(cpath);
|
||||
const splitted = cpath.split(".");
|
||||
const wmPath = splitted[0];
|
||||
const content = await readTextFile(cpath);
|
||||
const lang = inferContentTypeFromFilePath(cpath, opts.defaultTs);
|
||||
const typed =
|
||||
(await parseMetadataFile(
|
||||
@@ -154,38 +357,249 @@ async function dev(opts: GlobalOptions & SyncOptions) {
|
||||
type: "flow";
|
||||
flow: OpenFlow;
|
||||
uriPath: string;
|
||||
path: string;
|
||||
};
|
||||
|
||||
// Load a resource by its windmill path (e.g., "u/admin/my_script" or "f/my_flow")
|
||||
async function loadWmPath(wmPath: string): Promise<LastEditScript | LastEditFlow | undefined> {
|
||||
wmPath = normalizeWmPath(wmPath);
|
||||
// Try as flow — check both dotted and non-dotted suffixes
|
||||
let flowDir: string | undefined;
|
||||
let flowYaml: string | undefined;
|
||||
for (const suffix of [".flow", "__flow"]) {
|
||||
const candidate = wmPath + suffix + "/";
|
||||
try {
|
||||
await access(candidate + "flow.yaml");
|
||||
flowDir = candidate;
|
||||
flowYaml = candidate + "flow.yaml";
|
||||
break;
|
||||
} catch {}
|
||||
}
|
||||
try {
|
||||
if (!flowDir || !flowYaml) throw new Error("not a flow");
|
||||
const localFlow = (await yamlParseFile(flowYaml)) as FlowFile;
|
||||
await replaceInlineScripts(
|
||||
localFlow.value.modules,
|
||||
async (p: string) => await readTextFile(flowDir + p),
|
||||
log,
|
||||
flowDir,
|
||||
SEP,
|
||||
undefined,
|
||||
);
|
||||
snapshotPathScripts(localFlow.value);
|
||||
const localScriptReader = createPreviewLocalScriptReader({
|
||||
exts,
|
||||
defaultTs: opts.defaultTs,
|
||||
codebases,
|
||||
});
|
||||
await replaceAllPathScriptsWithLocal(localFlow.value, localScriptReader, log);
|
||||
tagReplacedPathScripts(localFlow.value);
|
||||
const edit: LastEditFlow = {
|
||||
type: "flow",
|
||||
flow: localFlow,
|
||||
uriPath: flowDir,
|
||||
path: wmPath,
|
||||
};
|
||||
currentLastEdit = edit;
|
||||
return edit;
|
||||
} catch {
|
||||
// Not a flow, try as script
|
||||
}
|
||||
|
||||
// Try as script
|
||||
for (const ext of exts) {
|
||||
const filePath = wmPath + ext;
|
||||
try {
|
||||
await access(filePath);
|
||||
const content = await readTextFile(filePath);
|
||||
const lang = inferContentTypeFromFilePath(filePath, opts.defaultTs);
|
||||
const typed = (await parseMetadataFile(removeExtensionToPath(filePath), undefined))?.payload;
|
||||
const edit: LastEditScript = {
|
||||
type: "script",
|
||||
content,
|
||||
path: wmPath,
|
||||
language: lang,
|
||||
tag: typed?.tag,
|
||||
lock: typed?.lock,
|
||||
};
|
||||
currentLastEdit = edit;
|
||||
return edit;
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
log.error(`Could not find file for path: ${wmPath}`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Handle flow edits from the dev UI — write changes back to disk
|
||||
// Mirrors the windmill-vscode extension's `processFlowMessage`
|
||||
// (src/extension.ts). Keep them in step — the dev page is the same code in
|
||||
// both contexts, and divergence here means the same flow round-trips
|
||||
// differently between VS Code and the local dev preview.
|
||||
//
|
||||
// Deliberate divergence: the orphan-cleanup pass is restricted to
|
||||
// INLINE_SCRIPT_EXTS so unrelated files (README.md, fixtures, etc.) are
|
||||
// not deleted. The extension's version doesn't filter and would delete
|
||||
// them — that's a known issue tracked separately.
|
||||
async function handleFlowRoundTrip(data: { flow: any; uriPath: string }) {
|
||||
if (!data.uriPath || !data.flow?.value) return;
|
||||
|
||||
let flowDir = data.uriPath;
|
||||
if (!flowDir.endsWith("/")) flowDir += "/";
|
||||
if (flowDir.includes("://")) {
|
||||
flowDir = new URL(flowDir).pathname;
|
||||
}
|
||||
|
||||
// Restore PathScripts BEFORE extracting so we don't write a file for the
|
||||
// inlined body of a `type: 'script'` reference.
|
||||
restorePathScripts(data.flow.value);
|
||||
|
||||
const flowYamlPath = flowDir + "flow.yaml";
|
||||
let currentLoadedFlow: any[] | undefined;
|
||||
let currentLoadedFailureModule: any | undefined;
|
||||
let currentLoadedPreprocessorModule: any | undefined;
|
||||
try {
|
||||
const currentFlow = (await yamlParseFile(flowYamlPath)) as FlowFile;
|
||||
currentLoadedFlow = currentFlow.value?.modules;
|
||||
currentLoadedFailureModule = currentFlow.value?.failure_module;
|
||||
currentLoadedPreprocessorModule = currentFlow.value?.preprocessor_module;
|
||||
} catch {
|
||||
// flow.yaml doesn't exist yet or is invalid
|
||||
}
|
||||
|
||||
const inlineScriptMapping: Record<string, string> = {};
|
||||
extractCurrentMapping(
|
||||
currentLoadedFlow,
|
||||
inlineScriptMapping,
|
||||
currentLoadedFailureModule,
|
||||
currentLoadedPreprocessorModule,
|
||||
);
|
||||
|
||||
// Share one pathAssigner across all extraction calls so failure /
|
||||
// preprocessor modules don't collide on filenames with main modules.
|
||||
const extractOptions = { skipInlineScriptSuffix: getNonDottedPaths() };
|
||||
const pathAssigner = newPathAssigner(opts.defaultTs ?? "bun", extractOptions);
|
||||
|
||||
const allExtracted = extractInlineScripts(
|
||||
data.flow.value.modules ?? [],
|
||||
inlineScriptMapping,
|
||||
"/",
|
||||
opts.defaultTs ?? "bun",
|
||||
pathAssigner,
|
||||
extractOptions,
|
||||
);
|
||||
if (data.flow.value.failure_module?.value?.type === "rawscript") {
|
||||
allExtracted.push(...extractInlineScripts(
|
||||
[data.flow.value.failure_module],
|
||||
inlineScriptMapping,
|
||||
"/",
|
||||
opts.defaultTs ?? "bun",
|
||||
pathAssigner,
|
||||
extractOptions,
|
||||
));
|
||||
}
|
||||
if (data.flow.value.preprocessor_module?.value?.type === "rawscript") {
|
||||
allExtracted.push(...extractInlineScripts(
|
||||
[data.flow.value.preprocessor_module],
|
||||
inlineScriptMapping,
|
||||
"/",
|
||||
opts.defaultTs ?? "bun",
|
||||
pathAssigner,
|
||||
extractOptions,
|
||||
));
|
||||
}
|
||||
|
||||
for (const s of allExtracted) {
|
||||
const filePath = flowDir + s.path;
|
||||
// `!inline foo.ts` is a YAML directive that points at another file —
|
||||
// treat it as a placeholder, not as content to overwrite.
|
||||
if (s.content.startsWith("!inline ")) {
|
||||
try {
|
||||
await stat(filePath);
|
||||
} catch {
|
||||
await writeFile(filePath, "", "utf-8");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let needsWrite = true;
|
||||
try {
|
||||
const existing = await readTextFile(filePath);
|
||||
if (existing === s.content) needsWrite = false;
|
||||
} catch {
|
||||
// File doesn't exist
|
||||
}
|
||||
if (needsWrite) {
|
||||
await writeFile(filePath, s.content, "utf-8");
|
||||
log.info(`Wrote inline script: ${filePath}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Only rewrite flow.yaml when the serialized YAML actually differs from
|
||||
// what's on disk. Avoids noisy mtime updates that re-trigger the watcher.
|
||||
const flowYaml = yamlStringify(data.flow);
|
||||
let currentYaml: string | undefined;
|
||||
try {
|
||||
currentYaml = await readTextFile(flowYamlPath);
|
||||
} catch {
|
||||
// File doesn't exist
|
||||
}
|
||||
if (currentYaml?.trimEnd() !== flowYaml.trimEnd()) {
|
||||
await writeFile(flowYamlPath, flowYaml, "utf-8");
|
||||
log.info(`Wrote flow: ${flowYamlPath}`);
|
||||
}
|
||||
|
||||
// Orphan cleanup: extension does this unconditionally and overshoots,
|
||||
// deleting README.md / fixtures / .env.local. We restrict to known
|
||||
// inline-script extensions.
|
||||
const extractedPaths = new Set(allExtracted.map((s) => s.path));
|
||||
try {
|
||||
const dirFiles = await readdir(flowDir);
|
||||
for (const file of dirFiles) {
|
||||
if (file === "flow.yaml" || file === "flow.json" || file.startsWith(".")) continue;
|
||||
if (!INLINE_SCRIPT_EXTS.has(path.extname(file))) continue;
|
||||
if (!extractedPaths.has(file)) {
|
||||
await unlink(flowDir + file);
|
||||
log.info(`Removed orphaned file: ${flowDir + file}`);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Directory read failed
|
||||
}
|
||||
}
|
||||
|
||||
const connectedClients: Set<WebSocket> = new Set();
|
||||
|
||||
// Function to send a message to all connected clients
|
||||
// Send a message to all connected clients, gated by --path when set so we
|
||||
// don't spam clients (or risk yanking their view) with edits to unrelated files.
|
||||
function broadcastChanges(lastEdit: LastEditScript | LastEditFlow) {
|
||||
if (opts.path && normalizeWmPath(lastEdit.path) !== opts.path) {
|
||||
return;
|
||||
}
|
||||
for (const client of connectedClients.values()) {
|
||||
client.send(JSON.stringify(lastEdit));
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
console.log("New dev client connected");
|
||||
|
||||
ws.on("open", () => {
|
||||
if (currentLastEdit) {
|
||||
broadcastChanges(currentLastEdit);
|
||||
// Push the currently loaded edit so the page renders immediately on
|
||||
// page load, without waiting for a file change to trigger a broadcast.
|
||||
if (currentLastEdit && ws.readyState === WebSocket.OPEN) {
|
||||
try {
|
||||
ws.send(JSON.stringify(currentLastEdit));
|
||||
} catch (e) {
|
||||
console.error("Failed to push initial state to new client:", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ws.on("close", () => {
|
||||
connectedClients.delete(ws);
|
||||
console.log("Client disconnected");
|
||||
console.log("Dev client disconnected");
|
||||
});
|
||||
|
||||
ws.on("message", (message: WebSocket.RawData) => {
|
||||
@@ -199,48 +613,278 @@ async function dev(opts: GlobalOptions & SyncOptions) {
|
||||
|
||||
if (data.type === "load") {
|
||||
loadPaths([data.path]);
|
||||
} else if (data.type === "flow") {
|
||||
handleFlowRoundTrip(data).catch((err) => {
|
||||
log.error(`Failed to write flow changes: ${err}`);
|
||||
});
|
||||
} else if (data.type === "loadWmPath") {
|
||||
loadWmPath(data.path).then((edit) => {
|
||||
if (edit && ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify(edit));
|
||||
}
|
||||
}).catch((err) => {
|
||||
log.error(`Failed to load path ${data.path}: ${err}`);
|
||||
});
|
||||
} else if (data.type === "listPaths") {
|
||||
listWorkspacePaths().then((items) => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ type: "paths", items }));
|
||||
}
|
||||
}).catch((err) => {
|
||||
log.error(`Failed to list paths: ${err}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Start the server
|
||||
const port = await getPort.default({ port: 3001 });
|
||||
const url =
|
||||
`${workspace.remote}dev?workspace=${workspace.workspaceId}&local=true&wm_token=${workspace.token}` +
|
||||
(port === PORT ? "" : `&port=${port}`);
|
||||
|
||||
console.log(`Go to ${url}`);
|
||||
function maybeOpenBrowser(url: string) {
|
||||
if (opts.open === false) return;
|
||||
try {
|
||||
open.openApp(open.apps.browser, { arguments: [url] }).catch((error) => {
|
||||
open.default(url).catch((error) => {
|
||||
console.error(
|
||||
`Failed to open browser, please navigate to ${url}, error: ${error}`
|
||||
);
|
||||
});
|
||||
console.log("Opened browser for you");
|
||||
console.log(`Opened browser at ${url}`);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Failed to open browser, please navigate to ${url}, ${error}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
"Dev server will automatically point to the last script edited locally"
|
||||
);
|
||||
// --- Proxy mode (when --proxy-port is set) ---
|
||||
//
|
||||
// Runs a localhost HTTP server that:
|
||||
// - serves the dev page from `http://localhost:<proxyPort>/` (forwarded
|
||||
// to the remote workspace), so embedders that need a localhost origin
|
||||
// can render it (e.g. Claude Code's port-detection preview), and
|
||||
// - upgrades local /ws connections back to this same process for the
|
||||
// live-reload channel.
|
||||
//
|
||||
// The simpler "direct" mode below works for standalone browser tabs and the
|
||||
// VS Code extension's iframe — only embedders that demand a localhost origin
|
||||
// need this proxy.
|
||||
|
||||
server.listen(port, () => {
|
||||
console.log(`Server listening on port ${port}`);
|
||||
async function startProxyServer(requestedPort: number) {
|
||||
// Probe both IPv4 and IPv6 stacks before binding. If the requested port is
|
||||
// taken on either, walk upward to the next free one so we don't silently
|
||||
// collide with a leftover dev server (see cli/src/utils/port-probe.ts).
|
||||
const proxyPort = await resolveBindPort(requestedPort, "--proxy-port", {
|
||||
info: (m) => console.log(m),
|
||||
warn: (m) => console.warn(m),
|
||||
});
|
||||
|
||||
const remote = new URL(workspace.remote);
|
||||
const isHttps = remote.protocol === "https:";
|
||||
const remoteHost = remote.hostname;
|
||||
const remotePort = remote.port ? parseInt(remote.port) : (isHttps ? 443 : 80);
|
||||
const httpModule = isHttps ? https : http;
|
||||
|
||||
const devWss = new WebSocketServer({ noServer: true });
|
||||
setupDevWs(devWss);
|
||||
|
||||
const proxyWss = new WebSocketServer({ noServer: true });
|
||||
|
||||
const proxyServer = http.createServer((clientReq, clientRes) => {
|
||||
const parsedUrl = new URL(clientReq.url ?? "/", `http://localhost`);
|
||||
if (parsedUrl.pathname === "/" || parsedUrl.pathname === "") {
|
||||
let devUrl = `/dev?workspace=${workspace.workspaceId}&local=true&wm_token=${workspace.token}&port=${proxyPort}`;
|
||||
if (opts.path) {
|
||||
devUrl += `&path=${opts.path}`;
|
||||
}
|
||||
clientRes.writeHead(302, { Location: devUrl });
|
||||
clientRes.end();
|
||||
return;
|
||||
}
|
||||
|
||||
const fwdHeaders: Record<string, string | string[] | undefined> = {
|
||||
...clientReq.headers,
|
||||
host: remote.host,
|
||||
};
|
||||
delete fwdHeaders["connection"];
|
||||
delete fwdHeaders["keep-alive"];
|
||||
delete fwdHeaders["transfer-encoding"];
|
||||
delete fwdHeaders["accept-encoding"];
|
||||
|
||||
const proxyOpts: http.RequestOptions = {
|
||||
hostname: remoteHost,
|
||||
port: remotePort,
|
||||
path: clientReq.url,
|
||||
method: clientReq.method,
|
||||
headers: fwdHeaders,
|
||||
};
|
||||
|
||||
const proxyReq = httpModule.request(proxyOpts, (proxyRes) => {
|
||||
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 });
|
||||
});
|
||||
|
||||
// WebSocket upgrades
|
||||
proxyServer.on("upgrade", (req, socket, head) => {
|
||||
const pathname = req.url?.split("?")[0] ?? "";
|
||||
|
||||
if (pathname === "/ws_dev" || pathname === "/ws") {
|
||||
devWss.handleUpgrade(req, socket, head, (ws) => {
|
||||
devWss.emit("connection", ws, req);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
socket.destroy();
|
||||
});
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
proxyServer.listen(proxyPort, BIND_HOST, () => {
|
||||
console.log(`Dev proxy listening on http://localhost:${proxyPort}`);
|
||||
if (opts.path) {
|
||||
console.log(`Watching ${opts.path} — edits will live-reload in the dev page`);
|
||||
} else {
|
||||
console.log(
|
||||
"Open the dev page and pick a flow or script to preview — edits will live-reload"
|
||||
);
|
||||
console.log("(pass --path <path> to skip the picker)");
|
||||
}
|
||||
maybeOpenBrowser(`http://localhost:${proxyPort}/`);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
await Promise.all([startApp(), watchChanges()]);
|
||||
// --- Direct mode (no localhost HTTP proxy) ---
|
||||
//
|
||||
// The browser loads the dev page from the remote workspace URL and opens a
|
||||
// WebSocket directly to this localhost server. Used when:
|
||||
// - the user runs `wmill dev` and opens a regular browser tab, or
|
||||
// - the VS Code extension iframe loads the dev page (its iframe URL omits
|
||||
// `local=true`, so it never opens this WS, but everything else still
|
||||
// functions through the existing remote workspace connection).
|
||||
//
|
||||
// This is the simplest topology: a bare WebSocket server. The reverse-proxy
|
||||
// mode (above) is only needed when something needs to embed the dev UI on a
|
||||
// localhost origin (Claude Code's port-detection preview).
|
||||
|
||||
async function startDirectServer() {
|
||||
const server = http.createServer((_req, res) => {
|
||||
res.writeHead(200);
|
||||
res.end();
|
||||
});
|
||||
const wss = new WebSocketServer({ server });
|
||||
setupDevWs(wss);
|
||||
|
||||
// Probe both IPv4 and IPv6 stacks before binding. Same dual-stack
|
||||
// collision risk as proxy mode: a leftover wmill dev on [::]:3001 would
|
||||
// silently steer localhost:3001 traffic to the wrong process if we only
|
||||
// probed one stack (which is what getPort does).
|
||||
const port = await resolveBindPort(PORT, "wmill dev", {
|
||||
info: (m) => console.log(m),
|
||||
warn: (m) => console.warn(m),
|
||||
});
|
||||
const url =
|
||||
`${workspace.remote}dev?workspace=${workspace.workspaceId}&local=true&wm_token=${workspace.token}` +
|
||||
(port === PORT ? "" : `&port=${port}`) +
|
||||
(opts.path ? `&path=${opts.path}` : "");
|
||||
|
||||
if (opts.open === false) {
|
||||
console.log(`Go to ${url}`);
|
||||
}
|
||||
maybeOpenBrowser(url);
|
||||
|
||||
if (opts.path) {
|
||||
console.log(`Watching ${opts.path} — edits will live-reload in the dev page`);
|
||||
} else {
|
||||
console.log(
|
||||
"Open the dev page and pick a flow or script to preview — edits will live-reload"
|
||||
);
|
||||
}
|
||||
|
||||
server.listen(port, BIND_HOST, () => {
|
||||
console.log(`Dev WebSocket listening on ws://localhost:${port}/ws`);
|
||||
});
|
||||
}
|
||||
|
||||
// --- Start ---
|
||||
|
||||
// If --path is set, load it immediately
|
||||
if (opts.path) {
|
||||
await loadWmPath(opts.path);
|
||||
}
|
||||
|
||||
const startServer = opts.proxyPort
|
||||
? () => startProxyServer(opts.proxyPort!)
|
||||
: () => startDirectServer();
|
||||
|
||||
await Promise.all([startServer(), watchChanges()]);
|
||||
console.log("Stopped dev mode");
|
||||
}
|
||||
|
||||
const command = new Command()
|
||||
.description("Launch a dev server that watches for local file changes and auto-pushes them to the remote workspace. Provides live reload for scripts and flows during development.")
|
||||
.description("Watch local file changes and live-reload the dev page for preview. Does NOT deploy to the remote workspace — use wmill sync push for that.")
|
||||
.option(
|
||||
"--includes <pattern...:string>",
|
||||
"Filter paths givena glob pattern or path"
|
||||
"Filter paths given a glob pattern or path"
|
||||
)
|
||||
.option(
|
||||
"--proxy-port <port:number>",
|
||||
"Port for a localhost reverse proxy to the remote Windmill server"
|
||||
)
|
||||
.option(
|
||||
"--path <path:string>",
|
||||
"Watch a specific windmill path (e.g., u/admin/my_script or f/my_flow)"
|
||||
)
|
||||
.option(
|
||||
"--no-open",
|
||||
"Do not open the browser automatically"
|
||||
)
|
||||
.action(dev as any);
|
||||
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* Snapshot / tag / restore PathScript modules across the dev-page round-trip.
|
||||
*
|
||||
* **This file is a port of the windmill-vscode extension's
|
||||
* `src/utils/pathscript-restore.ts`.** Keep them in sync — divergence here
|
||||
* means the same flow round-trips differently in VS Code and Claude Code's
|
||||
* local dev preview, which is exactly the kind of bug we worked hard to
|
||||
* make impossible.
|
||||
*
|
||||
* ## Why this exists
|
||||
*
|
||||
* The dev page can't render PathScript modules (`type: 'script'`, `path:` ref)
|
||||
* directly — it has no way to fetch the referenced script's body. Before
|
||||
* sending a flow to the page we inline every PathScript's content via
|
||||
* `replaceAllPathScriptsWithLocal`, turning each into a rawscript shape the
|
||||
* page can render in its editor pane.
|
||||
*
|
||||
* If we did nothing else, the page would round-trip those rawscripts back
|
||||
* verbatim — silently overwriting the user's original `path:` references
|
||||
* with frozen snapshots. Reusability gone.
|
||||
*
|
||||
* The protocol:
|
||||
* 1. `snapshotPathScripts` — stash original PathScript values on each
|
||||
* module (and AI-agent tool) before inlining.
|
||||
* 2. *(Caller runs `replaceAllPathScriptsWithLocal` here.)*
|
||||
* 3. `tagReplacedPathScripts` — move the snapshot inside `value{}` so it
|
||||
* rides along through serialization. The dev page treats it as opaque.
|
||||
* 4. *(Page round-trips the flow back over WS / postMessage.)*
|
||||
* 5. `restorePathScripts` — swap `value` back to the saved snapshot.
|
||||
* The user's edits to the inlined body are deliberately dropped (you
|
||||
* edit a PathScript by opening its file directly, not through the
|
||||
* flow editor).
|
||||
*
|
||||
* The `_originalPathScript` tag key is the contract between this file and
|
||||
* the dev page's serialization. Don't rename it without coordinating.
|
||||
*/
|
||||
|
||||
const TAG_KEY = "_originalPathScript" as const;
|
||||
|
||||
interface ModuleVisitor {
|
||||
onModule(module: any): void;
|
||||
onTool(tool: any): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively walks all modules in a flow value, visiting leaf modules and
|
||||
* AI agent tools. Handles branchone, branchall, forloopflow, whileloopflow,
|
||||
* and aiagent nesting.
|
||||
*/
|
||||
function walkModules(modules: any[], visitor: ModuleVisitor) {
|
||||
for (const module of modules) {
|
||||
if (!module.value) continue;
|
||||
const val = module.value;
|
||||
if (val.type === "forloopflow" || val.type === "whileloopflow") {
|
||||
walkModules(val.modules, visitor);
|
||||
} else if (val.type === "branchall") {
|
||||
for (const branch of val.branches ?? []) {
|
||||
walkModules(branch.modules, visitor);
|
||||
}
|
||||
} else if (val.type === "branchone") {
|
||||
for (const branch of val.branches ?? []) {
|
||||
walkModules(branch.modules, visitor);
|
||||
}
|
||||
if (val.default) {
|
||||
walkModules(val.default, visitor);
|
||||
}
|
||||
} else if (val.type === "aiagent") {
|
||||
for (const tool of val.tools ?? []) {
|
||||
visitor.onTool(tool);
|
||||
}
|
||||
} else {
|
||||
visitor.onModule(module);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function walkFlow(flowValue: any, visitor: ModuleVisitor) {
|
||||
if (flowValue?.modules) walkModules(flowValue.modules, visitor);
|
||||
if (flowValue?.failure_module) walkModules([flowValue.failure_module], visitor);
|
||||
if (flowValue?.preprocessor_module) walkModules([flowValue.preprocessor_module], visitor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Must be called BEFORE `replaceAllPathScriptsWithLocal` to snapshot the
|
||||
* original PathScript values onto each module / AI-agent tool.
|
||||
*/
|
||||
export function snapshotPathScripts(flowValue: any) {
|
||||
walkFlow(flowValue, {
|
||||
onModule(module) {
|
||||
if (module.value.type === "script") {
|
||||
module[TAG_KEY] = JSON.parse(JSON.stringify(module.value));
|
||||
}
|
||||
},
|
||||
onTool(tool) {
|
||||
const tv = tool.value;
|
||||
if (tv && "tool_type" in tv && tv.tool_type === "flowmodule" && tv.type === "script") {
|
||||
tool[TAG_KEY] = JSON.parse(JSON.stringify(tv));
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* After `replaceAllPathScriptsWithLocal` has mutated the flow, call this to
|
||||
* move each snapshot from `module[TAG_KEY]` into `module.value[TAG_KEY]` so
|
||||
* it survives serialization to the dev page (the page only forwards what's
|
||||
* inside `value`).
|
||||
*/
|
||||
export function tagReplacedPathScripts(flowValue: any) {
|
||||
walkFlow(flowValue, {
|
||||
onModule(module) {
|
||||
if (module[TAG_KEY] && module.value.type === "rawscript") {
|
||||
module.value[TAG_KEY] = module[TAG_KEY];
|
||||
delete module[TAG_KEY];
|
||||
} else if (module[TAG_KEY]) {
|
||||
// Snapshotted but not replaced (local file not found) — clean up.
|
||||
delete module[TAG_KEY];
|
||||
}
|
||||
},
|
||||
onTool(tool) {
|
||||
const tv = tool.value;
|
||||
if (tool[TAG_KEY] && tv && "tool_type" in tv && tv.tool_type === "flowmodule" && tv.type === "rawscript") {
|
||||
tv[TAG_KEY] = tool[TAG_KEY];
|
||||
delete tool[TAG_KEY];
|
||||
} else if (tool[TAG_KEY]) {
|
||||
delete tool[TAG_KEY];
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores PathScript modules in a flow returned from the dev page.
|
||||
* Any module/tool with a `_originalPathScript` tag inside its `value` gets
|
||||
* restored unconditionally; the tag is removed after restoration.
|
||||
*/
|
||||
export function restorePathScripts(flowValue: any) {
|
||||
walkFlow(flowValue, {
|
||||
onModule(module) {
|
||||
if (module.value[TAG_KEY]) {
|
||||
module.value = module.value[TAG_KEY];
|
||||
}
|
||||
},
|
||||
onTool(tool) {
|
||||
if (tool.value?.[TAG_KEY]) {
|
||||
tool.value = tool.value[TAG_KEY];
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -733,6 +733,12 @@ export async function bootstrap(
|
||||
const metadataFile = getMetadataFileName("flow", "yaml");
|
||||
const flowYamlPath = `${flowDirFullPath}/${metadataFile}`;
|
||||
writeFileSync(flowYamlPath, newFlowDefinitionYaml, { flag: "wx", encoding: "utf-8" });
|
||||
|
||||
log.info(colors.green(`Created flow at ${flowDirFullPath}`));
|
||||
|
||||
log.info("");
|
||||
log.info(colors.bold("To preview this flow:"));
|
||||
log.info(colors.gray(` wmill dev --path ${flowPath}`));
|
||||
}
|
||||
|
||||
async function history(
|
||||
|
||||
@@ -81,13 +81,20 @@ async function initAction(opts: InitOptions) {
|
||||
: undefined;
|
||||
} else {
|
||||
const activeProfile = await getActiveWorkspace(opts as GlobalOptions);
|
||||
const orderedProfiles = activeProfile
|
||||
? [
|
||||
...profiles.filter((p) => p.name === activeProfile.name),
|
||||
...profiles.filter((p) => p.name !== activeProfile.name),
|
||||
]
|
||||
: profiles;
|
||||
const selectedName = await Select.prompt({
|
||||
message: "Select workspace profile",
|
||||
options: profiles.map((p) => ({
|
||||
name: `${p.name} (${p.workspaceId} on ${p.remote})`,
|
||||
options: orderedProfiles.map((p) => ({
|
||||
name: `${p.name} (${p.workspaceId} on ${p.remote})${
|
||||
activeProfile?.name === p.name ? " — active" : ""
|
||||
}`,
|
||||
value: p.name,
|
||||
})),
|
||||
default: activeProfile?.name,
|
||||
});
|
||||
selectedProfile = profiles.find((p) => p.name === selectedName);
|
||||
}
|
||||
@@ -234,14 +241,14 @@ async function initAction(opts: InitOptions) {
|
||||
}
|
||||
}
|
||||
|
||||
// Read nonDottedPaths from config to specialize generated skills
|
||||
// Read nonDottedPaths from config
|
||||
let nonDottedPaths = true; // default for new inits
|
||||
try {
|
||||
const { readConfigFile } = await import("../../core/conf.ts");
|
||||
const config = await readConfigFile();
|
||||
nonDottedPaths = config.nonDottedPaths ?? true;
|
||||
} catch {
|
||||
// If config can't be read, use default
|
||||
// If config can't be read, use defaults
|
||||
}
|
||||
|
||||
// Create guidance files (AGENTS.md, CLAUDE.md, and agent skills)
|
||||
|
||||
@@ -2559,6 +2559,7 @@ export async function pull(
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
if (tracker.apps.length > 0) {
|
||||
log.info(
|
||||
colors.gray(
|
||||
|
||||
@@ -82,7 +82,7 @@ export async function browserLogin(
|
||||
log.info(`Login by going to ${url}`);
|
||||
|
||||
try {
|
||||
open.openApp(open.apps.browser, { arguments: [url] }).catch((error) => {
|
||||
open.default(url).catch((error) => {
|
||||
console.error(
|
||||
`Failed to open browser, please navigate to ${url}, error: ${error}`
|
||||
);
|
||||
|
||||
@@ -27,11 +27,12 @@ You MUST use the \`write-script-<language>\` skill to write or modify scripts in
|
||||
## Flow Writing Guide
|
||||
|
||||
You MUST use the \`write-flow\` skill to create or modify flows.
|
||||
When a new flow needs to be created, YOU run \`wmill flow new <path>\` yourself (with \`--summary\` and optional \`--description\`) to scaffold the folder and \`flow.yaml\`, then edit \`flow.yaml\` to fill in modules and schema. Do NOT scaffold the folder + yaml by hand and do NOT tell the user to run \`wmill flow new\`. If path or summary are missing from the user's request, ask via \`AskUserQuestion\` (one call, all missing fields) — never invent them. See the \`write-flow\` skill for the procedure.
|
||||
|
||||
## Raw App Development
|
||||
|
||||
You MUST use the \`raw-app\` skill to create or modify raw apps.
|
||||
Whenever a new app needs to be created you MUST ask the user to run \`wmill app new\` in its terminal first.
|
||||
When a new app needs to be created, YOU run \`wmill app new\` yourself with \`--summary\`, \`--path\`, and \`--framework\` flags (and any other relevant flags). Do NOT ask the user to run it. If you don't have the values for those flags, ask the user via \`AskUserQuestion\` (one call, all missing fields) — never invent them. See the \`raw-app\` skill for the full procedure.
|
||||
|
||||
## Triggers
|
||||
|
||||
@@ -45,6 +46,10 @@ You MUST use the \`schedules\` skill to configure cron schedules.
|
||||
|
||||
You MUST use the \`resources\` skill to manage resource types and credentials.
|
||||
|
||||
## Visual Preview
|
||||
|
||||
You MUST use the \`preview\` skill any time the user wants to see/open/visualize/preview a flow, script, or app in the dev page — and after writing one, when offering visual verification. The skill picks between an MCP-embedded proxy (one named \`launch.json\` entry per target) and direct mode (URL handed to the user) based on what tools you have.
|
||||
|
||||
## CLI Reference
|
||||
|
||||
You MUST use the \`cli-commands\` skill to use the CLI.
|
||||
|
||||
+852
-92
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* Port collision detection + fallback for `wmill dev` and `wmill app dev`.
|
||||
*
|
||||
* Why this exists: Node's default listen() has platform-dependent dual-stack
|
||||
* behaviour. If the requested IPv4 binding (0.0.0.0:N) is already taken by
|
||||
* another process, Node may silently fall back to IPv6-only ([::1]:N). The OS
|
||||
* then routes new `localhost` connections to the older IPv4 listener, so the
|
||||
* user opens http://localhost:N and sees the wrong server with no signal that
|
||||
* anything is wrong. Bit us in practice: a leftover `wmill dev --proxy-port 4000`
|
||||
* served traffic for a freshly-started `wmill app dev --port 4000`.
|
||||
*
|
||||
* The fix: probe both stacks before binding. Treat the port as taken if either
|
||||
* 0.0.0.0 or :: refuses the bind. On collision, walk upward to the next free
|
||||
* port and log the shift prominently.
|
||||
*/
|
||||
|
||||
import { createServer } from "node:net";
|
||||
import { execSync } from "node:child_process";
|
||||
|
||||
type Host = "0.0.0.0" | "::";
|
||||
|
||||
/**
|
||||
* Try to bind a fresh server to (port, host) and immediately close it.
|
||||
*
|
||||
* Returns false ONLY when the port is genuinely held by another process
|
||||
* (EADDRINUSE) or denied by permissions (EACCES). Other errors — most
|
||||
* importantly EAFNOSUPPORT / EADDRNOTAVAIL on the IPv6 probe when the host
|
||||
* has no IPv6 stack at all — return true: the stack we're probing simply
|
||||
* isn't reachable, which is functionally indistinguishable from "free" for
|
||||
* the dual-stack collision check.
|
||||
*/
|
||||
function isPortFree(port: number, host: Host): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const s = createServer();
|
||||
s.once("error", (err: NodeJS.ErrnoException) => {
|
||||
const code = err.code ?? "";
|
||||
// Anything that means "another process is holding this port" → not free.
|
||||
// Anything else (no IPv6 stack on this host, etc.) → treat as free so we
|
||||
// don't false-alarm on IPv4-only containers.
|
||||
resolve(code !== "EADDRINUSE" && code !== "EACCES");
|
||||
});
|
||||
s.once("listening", () => s.close(() => resolve(true)));
|
||||
s.listen(port, host);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A port counts as free only if BOTH IPv4 and IPv6 stacks accept the bind.
|
||||
* If either is held by another process, the OS may route `localhost` traffic
|
||||
* to that other process even when our listener succeeds on the free stack.
|
||||
*
|
||||
* Probes sequentially, not in parallel: on Linux the default is
|
||||
* `net.ipv6.bindv6only=0`, which makes a `bind(::, port)` socket also occupy
|
||||
* the IPv4 stack on the same port. Running both probes concurrently then
|
||||
* causes one to lose the race with EADDRINUSE on a port that is actually
|
||||
* free, producing false negatives. Sequential keeps each probe's bind fully
|
||||
* released before the next starts.
|
||||
*/
|
||||
async function isPortFreeOnBothStacks(port: number): Promise<boolean> {
|
||||
if (!(await isPortFree(port, "0.0.0.0"))) return false;
|
||||
if (!(await isPortFree(port, "::"))) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Best-effort lookup of the PID + command currently bound to <port>. Returns
|
||||
* undefined if nothing is found, the lookup fails, or the platform tooling
|
||||
* isn't installed. Never throws.
|
||||
*/
|
||||
function findPortHolder(port: number): { pid: number; command: string } | undefined {
|
||||
// macOS + Linux: lsof. -nP avoids DNS / port-name lookups, -sTCP:LISTEN
|
||||
// narrows to the listening socket.
|
||||
try {
|
||||
const out = execSync(`lsof -nP -iTCP:${port} -sTCP:LISTEN -F pc 2>/dev/null`, {
|
||||
encoding: "utf-8",
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
});
|
||||
// -F pc emits records like:
|
||||
// p91418
|
||||
// cbun
|
||||
let pid: number | undefined;
|
||||
let cmd: string | undefined;
|
||||
for (const line of out.split("\n")) {
|
||||
if (line.startsWith("p")) pid = parseInt(line.slice(1), 10);
|
||||
else if (line.startsWith("c")) cmd = line.slice(1);
|
||||
if (pid && cmd) return { pid, command: cmd };
|
||||
}
|
||||
} catch {
|
||||
// lsof missing or no holder — fall through.
|
||||
}
|
||||
|
||||
// Linux fallback: ss. -ltnp lists listening TCP sockets with PID/command.
|
||||
try {
|
||||
const out = execSync(`ss -ltnp 2>/dev/null | awk '$4 ~ /:${port}$/ { print $NF }'`, {
|
||||
encoding: "utf-8",
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
}).trim();
|
||||
// Format: users:(("bun",pid=91418,fd=23))
|
||||
const m = out.match(/\("([^"]+)",pid=(\d+)/);
|
||||
if (m) return { pid: parseInt(m[2], 10), command: m[1] };
|
||||
} catch {
|
||||
/* fall through */
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the port we should actually bind to.
|
||||
*
|
||||
* Walks upward from `requested` until a port is free on both stacks, capped
|
||||
* at +20 to avoid silently scanning the whole 4xxx range. On shift, logs a
|
||||
* prominent warning naming the holder if we can find it.
|
||||
*
|
||||
* Returns the chosen port (== requested when it was already free).
|
||||
*/
|
||||
export async function resolveBindPort(
|
||||
requested: number,
|
||||
flagLabel: string,
|
||||
log: { info: (msg: string) => void; warn: (msg: string) => void },
|
||||
): Promise<number> {
|
||||
const MAX_SHIFT = 20;
|
||||
for (let port = requested; port < requested + MAX_SHIFT; port++) {
|
||||
if (await isPortFreeOnBothStacks(port)) {
|
||||
if (port !== requested) {
|
||||
const holder = findPortHolder(requested);
|
||||
const holderHint = holder
|
||||
? ` (held by PID ${holder.pid} \`${holder.command}\`)`
|
||||
: "";
|
||||
log.warn(
|
||||
`Port ${requested} is already in use${holderHint}. Using port ${port} instead.`,
|
||||
);
|
||||
log.info(
|
||||
`If you need port ${requested} stable (e.g. a launch.json entry pinned to it), stop the holder and re-run with ${flagLabel} ${requested}.`,
|
||||
);
|
||||
}
|
||||
return port;
|
||||
}
|
||||
}
|
||||
throw new Error(
|
||||
`Could not find a free port in the range ${requested}-${requested + MAX_SHIFT - 1}. Stop a holder or pass ${flagLabel} <other>.`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The host string we bind to. Explicit IPv4 — `localhost` resolves to
|
||||
* 127.0.0.1 first on every platform we care about, and binding both stacks
|
||||
* relies on platform-specific IPV6_V6ONLY behaviour we don't want to debug.
|
||||
*/
|
||||
export const BIND_HOST = "0.0.0.0" as const;
|
||||
@@ -127,7 +127,8 @@ test(
|
||||
let stdoutBuffer = "";
|
||||
let port: number | null = null;
|
||||
|
||||
// Wait for "Server listening on port XXXX" message
|
||||
// Wait for the dev WebSocket startup line — see startDirectServer
|
||||
// in cli/src/commands/dev/dev.ts.
|
||||
const portMatch = await waitFor(
|
||||
async () => {
|
||||
try {
|
||||
@@ -144,7 +145,7 @@ test(
|
||||
// Reader may be exhausted
|
||||
}
|
||||
const match = stdoutBuffer.match(
|
||||
/Server listening on port (\d+)/,
|
||||
/Dev WebSocket listening on ws:\/\/localhost:(\d+)/,
|
||||
);
|
||||
return match;
|
||||
},
|
||||
|
||||
@@ -18,7 +18,13 @@
|
||||
} from '$lib/gen'
|
||||
import { inferArgs } from '$lib/infer'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { emptySchema, readFieldsRecursively, sendUserToast, type StateStore } from '$lib/utils'
|
||||
import {
|
||||
emptySchema,
|
||||
pluralize,
|
||||
readFieldsRecursively,
|
||||
sendUserToast,
|
||||
type StateStore
|
||||
} from '$lib/utils'
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||
import { onDestroy, onMount, setContext, untrack } from 'svelte'
|
||||
import DarkModeToggle from '$lib/components/sidebar/DarkModeToggle.svelte'
|
||||
@@ -38,7 +44,32 @@
|
||||
import { GroupEditor, setGroupEditorContext } from './graph/groupEditor.svelte'
|
||||
import { dfs } from './flows/dfs'
|
||||
import { loadSchemaFromModule } from './flows/flowInfers'
|
||||
import { CornerDownLeft, Play } from 'lucide-svelte'
|
||||
import {
|
||||
CornerDownLeft,
|
||||
Play,
|
||||
Folder,
|
||||
FolderTree,
|
||||
User,
|
||||
Search,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Code2,
|
||||
LayoutDashboard
|
||||
} from 'lucide-svelte'
|
||||
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
|
||||
import FlowIcon from '$lib/components/home/FlowIcon.svelte'
|
||||
import {
|
||||
groupItems,
|
||||
type ItemType,
|
||||
type FolderItem,
|
||||
type UserItem
|
||||
} from '$lib/components/home/treeViewUtils'
|
||||
import SearchItems from '$lib/components/SearchItems.svelte'
|
||||
import TextInput from '$lib/components/text_input/TextInput.svelte'
|
||||
import Row from '$lib/components/common/table/Row.svelte'
|
||||
import Alert from '$lib/components/common/alert/Alert.svelte'
|
||||
import { HOME_SEARCH_PLACEHOLDER } from '$lib/consts'
|
||||
import Toggle from './Toggle.svelte'
|
||||
import { setLicense } from '$lib/enterpriseUtils'
|
||||
import type { FlowCopilotContext } from './copilot/flow'
|
||||
@@ -112,6 +143,9 @@
|
||||
|
||||
let darkModeToggle: DarkModeToggle | undefined = $state()
|
||||
let darkMode: boolean = $state(document.documentElement.classList.contains('dark'))
|
||||
let flowContainerWidth = $state(0)
|
||||
let flowContainerHeight = $state(0)
|
||||
let flowHorizontalSplit = $derived(flowContainerWidth < flowContainerHeight)
|
||||
let modeInitialized = $state(false)
|
||||
let paneWidth = $state(0)
|
||||
let compactPreview = $derived(paneWidth < 800)
|
||||
@@ -160,7 +194,56 @@
|
||||
const href = window.location.href
|
||||
const indexQ = href.indexOf('?')
|
||||
const searchParams = indexQ > -1 ? new URLSearchParams(href.substring(indexQ)) : undefined
|
||||
let relativePaths: any[] = $state([])
|
||||
let relativePaths: (string | [number, string])[] = $state([])
|
||||
|
||||
type WmPathItem = {
|
||||
path: string
|
||||
kind: 'flow' | 'script' | 'raw_app'
|
||||
summary?: string
|
||||
}
|
||||
// watchPath is (re)synced on initial load, on popstate, and on explicit
|
||||
// pickPath assignments. We don't listen for generic pushState events —
|
||||
// the only pushState callsite is pickPath itself, and it updates watchPath
|
||||
// directly. If a third caller starts pushing to history, add a resync there.
|
||||
function parseWatchPath(): string | undefined {
|
||||
const i = window.location.href.indexOf('?')
|
||||
if (i < 0) return undefined
|
||||
return new URLSearchParams(window.location.href.substring(i)).get('path') ?? undefined
|
||||
}
|
||||
const PATH_SUFFIX_RE = /(\.(flow|app|raw_app)|__(flow|app|raw_app))\/?$/
|
||||
let watchPath = $state(parseWatchPath()?.replace(PATH_SUFFIX_RE, ''))
|
||||
let pickerItems: WmPathItem[] = $state([])
|
||||
// Picker only makes sense on the local dev page — that's the only context
|
||||
// with a wmill dev WebSocket capable of returning the workspace listing.
|
||||
// The VS Code extension iframe omits ?local=true and drives the page via
|
||||
// postMessage (replaceScript / replaceFlow), so it must skip the picker.
|
||||
const isLocalDevPage = !!searchParams?.has('local')
|
||||
const pickerMode = $derived(isLocalDevPage && !watchPath)
|
||||
let wsState: 'connecting' | 'open' | 'closed' = $state('connecting')
|
||||
let pickerFilter = $state('')
|
||||
let pickerKind: 'all' | 'flow' | 'script' | 'raw_app' = $state('all')
|
||||
// Shape pickerItems into the homepage's ItemType so we can reuse `groupItems`
|
||||
// for the folder/user tree structure. `kind` ('script'|'flow'|'raw_app') maps 1:1
|
||||
// onto ItemType['type']; missing fields (canWrite, edited_at, etc.) default to safe values.
|
||||
const pickerTreeItems = $derived(
|
||||
pickerItems.map(
|
||||
(item) =>
|
||||
({
|
||||
path: item.path,
|
||||
summary: item.summary ?? '',
|
||||
type: item.kind,
|
||||
canWrite: true,
|
||||
extra_perms: {},
|
||||
starred: false,
|
||||
edited_at: ''
|
||||
}) as unknown as ItemType
|
||||
)
|
||||
)
|
||||
const pickerKindFilteredItems = $derived(
|
||||
pickerKind === 'all' ? pickerTreeItems : pickerTreeItems.filter((i) => i.type === pickerKind)
|
||||
)
|
||||
let pickerFilteredItems: (ItemType & { marked?: string })[] | undefined = $state(undefined)
|
||||
const pickerGroups = $derived(groupItems(pickerFilteredItems ?? pickerKindFilteredItems))
|
||||
|
||||
if (searchParams?.has('local')) {
|
||||
connectWs()
|
||||
@@ -328,13 +411,40 @@
|
||||
)
|
||||
loadingCodebaseButton = false
|
||||
}
|
||||
const onPopState = () => {
|
||||
watchPath = parseWatchPath()?.replace(PATH_SUFFIX_RE, '')
|
||||
if (watchPath && socket && socket.readyState === WebSocket.OPEN) {
|
||||
socket.send(JSON.stringify({ type: 'loadWmPath', path: watchPath }))
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener('popstate', onPopState)
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
window.removeEventListener('message', el)
|
||||
window.removeEventListener('popstate', onPopState)
|
||||
if (socket && socket.readyState === WebSocket.OPEN) {
|
||||
socket?.close()
|
||||
}
|
||||
})
|
||||
|
||||
function pickPath(item: WmPathItem) {
|
||||
if (item.kind === 'raw_app') {
|
||||
sendUserToast(
|
||||
`raw_apps aren't previewable here. Run \`wmill app dev\` from inside the app folder.`,
|
||||
false
|
||||
)
|
||||
return
|
||||
}
|
||||
const url = new URL(window.location.href)
|
||||
url.searchParams.set('path', item.path)
|
||||
window.history.pushState({}, '', url.toString())
|
||||
watchPath = item.path
|
||||
socket?.send(JSON.stringify({ type: 'loadWmPath', path: item.path }))
|
||||
}
|
||||
|
||||
function connectWs() {
|
||||
try {
|
||||
if (socket) {
|
||||
@@ -345,8 +455,28 @@
|
||||
}
|
||||
const port = searchParams?.get('port') || '3001'
|
||||
try {
|
||||
wsState = 'connecting'
|
||||
socket = new WebSocket(`ws://localhost:${port}/ws`)
|
||||
|
||||
// On connect, request the watched path if any, otherwise ask for a list to render the picker
|
||||
socket.addEventListener('open', () => {
|
||||
if (!socket) return
|
||||
wsState = 'open'
|
||||
if (watchPath) {
|
||||
socket.send(JSON.stringify({ type: 'loadWmPath', path: watchPath }))
|
||||
} else {
|
||||
socket.send(JSON.stringify({ type: 'listPaths' }))
|
||||
}
|
||||
})
|
||||
|
||||
socket.addEventListener('error', () => {
|
||||
wsState = 'closed'
|
||||
})
|
||||
|
||||
socket.addEventListener('close', () => {
|
||||
wsState = 'closed'
|
||||
})
|
||||
|
||||
// Listen for messages
|
||||
socket.addEventListener('message', (event) => {
|
||||
replaceData(event.data)
|
||||
@@ -360,10 +490,29 @@
|
||||
console.log('Received invalid JSON: ' + msg)
|
||||
return
|
||||
}
|
||||
if (data.type == 'script') {
|
||||
replaceScript(data)
|
||||
} else if (data.type == 'flow') {
|
||||
replaceFlow(data)
|
||||
if (data.type === 'paths') {
|
||||
pickerItems = data.items ?? []
|
||||
return
|
||||
}
|
||||
// Picker mode (URL has no path) — ignore live broadcasts so a random
|
||||
// file change doesn't yank the page out of the picker. (When watchPath
|
||||
// IS set the server gates broadcasts itself, so no further filter here.)
|
||||
if (!watchPath) return
|
||||
if (data.type == 'script' || data.type == 'flow') {
|
||||
// Guard against the $effect on flowStore.val (re)serializing the
|
||||
// just-received payload back over the same WS to handleFlowRoundTrip
|
||||
// (which would re-run the orphan-file scan with the same content).
|
||||
// Mirrors the postMessage handler above.
|
||||
lockChanges = true
|
||||
if (data.type == 'script') {
|
||||
replaceScript(data)
|
||||
} else {
|
||||
replaceFlow(data)
|
||||
}
|
||||
timeout && clearTimeout(timeout)
|
||||
timeout = window.setTimeout(() => {
|
||||
lockChanges = false
|
||||
}, 500)
|
||||
} else {
|
||||
sendUserToast(`Received invalid message type ${data.type}`, true)
|
||||
}
|
||||
@@ -571,14 +720,28 @@
|
||||
setGroupEditorContext(groupEditor, canCreateGroup)
|
||||
|
||||
let lastSent: OpenFlow | undefined = undefined
|
||||
const isInIframe = window.parent !== window
|
||||
function updateFlow(flow: OpenFlow) {
|
||||
if (lockChanges) {
|
||||
return
|
||||
}
|
||||
if (!deepEqual(flow, lastSent)) {
|
||||
lastSent = $state.snapshot(flow)
|
||||
window?.parent.postMessage({ type: 'flow', flow: lastSent, uriPath: lastUriPath }, '*')
|
||||
if (deepEqual(flow, lastSent)) {
|
||||
return
|
||||
}
|
||||
const snapshot = $state.snapshot(flow)
|
||||
// Prefer the WebSocket whenever a `wmill dev` session is connected — this covers
|
||||
// both standalone browser tabs and Claude Code's iframe preview. The VS Code
|
||||
// extension never opens this socket (its iframe URL omits `local=true`), so it
|
||||
// falls through to the postMessage path it has always used.
|
||||
if (socket && socket.readyState === WebSocket.OPEN) {
|
||||
socket.send(JSON.stringify({ type: 'flow', flow: snapshot, uriPath: lastUriPath }))
|
||||
lastSent = snapshot
|
||||
} else if (isInIframe) {
|
||||
window?.parent.postMessage({ type: 'flow', flow: snapshot, uriPath: lastUriPath }, '*')
|
||||
lastSent = snapshot
|
||||
}
|
||||
// Else: no channel available yet (WS still connecting, not in an iframe).
|
||||
// Don't mark `lastSent` so the next change will retry instead of being silently swallowed.
|
||||
}
|
||||
|
||||
let reload = $state(0)
|
||||
@@ -702,7 +865,7 @@
|
||||
|
||||
const selectedModule = $derived(
|
||||
selectedId && flowStore.val?.value
|
||||
? findModuleInFlow(flowStore.val.value, selectedId) ?? undefined
|
||||
? (findModuleInFlow(flowStore.val.value, selectedId) ?? undefined)
|
||||
: undefined
|
||||
)
|
||||
</script>
|
||||
@@ -712,7 +875,185 @@
|
||||
<JobLoader noCode={true} bind:this={jobLoader} bind:isLoading={testIsLoading} bind:job={testJob} />
|
||||
|
||||
<main class="h-screen w-full">
|
||||
{#if mode == 'script'}
|
||||
{#snippet itemRow(item: ItemType & { marked?: string }, depth: number)}
|
||||
{@const wmItem = pickerItems.find((p) => p.path === item.path)}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => wmItem && pickPath(wmItem)}
|
||||
class="block w-full text-left cursor-pointer border-b last:border-b-0"
|
||||
>
|
||||
<Row
|
||||
marked={item.marked}
|
||||
path={item.path}
|
||||
summary={item.summary}
|
||||
kind={item.type}
|
||||
{depth}
|
||||
workspaceId={$workspaceStore ?? ''}
|
||||
canFavorite={false}
|
||||
/>
|
||||
</button>
|
||||
{/snippet}
|
||||
{#snippet treeNode(node: ItemType | FolderItem | UserItem, depth: number)}
|
||||
{#if 'folderName' in node}
|
||||
<details open class="group border-b last:border-b-0">
|
||||
<summary
|
||||
class="px-4 py-2 w-full flex flex-row items-center justify-between cursor-pointer list-none group-open:border-b"
|
||||
>
|
||||
<div
|
||||
class="flex flex-row items-center gap-4 text-sm font-semibold"
|
||||
style={depth > 0 ? `padding-left: ${depth * 16}px;` : ''}
|
||||
>
|
||||
<div class="flex justify-center items-center">
|
||||
{#if depth === 0}
|
||||
<Folder size={16} class="text-secondary" />
|
||||
{:else}
|
||||
<FolderTree size={16} class="text-secondary" />
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<span class="whitespace-nowrap text-xs text-emphasis font-semibold"
|
||||
>{#if depth === 0}f/{/if}{node.folderName}</span
|
||||
>
|
||||
<div class="text-2xs font-normal text-secondary whitespace-nowrap">
|
||||
({pluralize(node.items.length, 'item')})
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full flex flex-row-reverse">
|
||||
<ChevronUp size={16} class="hidden group-open:block" />
|
||||
<ChevronDown size={16} class="block group-open:hidden" />
|
||||
</div>
|
||||
</summary>
|
||||
{#each node.items as child ('folderName' in child ? `f__${child.folderName}` : 'username' in child ? `u__${child.username}` : `i__${child.type}__${child.path}`)}
|
||||
{@render treeNode(child, depth + 1)}
|
||||
{/each}
|
||||
</details>
|
||||
{:else if 'username' in node}
|
||||
<details open class="group border-b last:border-b-0">
|
||||
<summary
|
||||
class="px-4 py-2 w-full flex flex-row items-center justify-between cursor-pointer list-none group-open:border-b"
|
||||
>
|
||||
<div
|
||||
class="flex flex-row items-center gap-4 text-sm font-semibold"
|
||||
style={depth > 0 ? `padding-left: ${depth * 16}px;` : ''}
|
||||
>
|
||||
<div class="flex justify-center items-center">
|
||||
<User size={16} class="text-secondary" />
|
||||
</div>
|
||||
<div>
|
||||
<span class="whitespace-nowrap text-xs text-emphasis font-semibold"
|
||||
>u/{node.username}</span
|
||||
>
|
||||
<div class="text-2xs font-normal text-secondary whitespace-nowrap">
|
||||
({pluralize(node.items.length, 'item')})
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full flex flex-row-reverse">
|
||||
<ChevronUp size={16} class="hidden group-open:block" />
|
||||
<ChevronDown size={16} class="block group-open:hidden" />
|
||||
</div>
|
||||
</summary>
|
||||
{#each node.items as child ('folderName' in child ? `f__${child.folderName}` : 'username' in child ? `u__${child.username}` : `i__${child.type}__${child.path}`)}
|
||||
{@render treeNode(child, depth + 1)}
|
||||
{/each}
|
||||
</details>
|
||||
{:else}
|
||||
{@render itemRow(node as ItemType & { marked?: string }, depth)}
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{#if pickerMode}
|
||||
<div class="h-full w-full overflow-auto p-6">
|
||||
<div class="absolute top-2 left-2">
|
||||
<DarkModeToggle bind:darkMode bind:this={darkModeToggle} forcedDarkMode={false} />
|
||||
</div>
|
||||
<div class="absolute top-2 right-2 text-xs text-secondary">
|
||||
{#if $userStore}
|
||||
{$userStore?.username} on {$workspaceStore}
|
||||
{:else}
|
||||
<span class="text-red-600">Unable to login on {$workspaceStore}</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="max-w-3xl mx-auto pt-8">
|
||||
<h1 class="text-2xl font-semibold text-primary mb-1">
|
||||
{$workspaceStore}
|
||||
<span class="font-normal text-secondary">(local)</span>
|
||||
</h1>
|
||||
<p class="text-sm text-secondary mb-4"> Click a flow or a script to preview it. </p>
|
||||
|
||||
<SearchItems
|
||||
filter={pickerFilter}
|
||||
items={pickerKindFilteredItems}
|
||||
f={(item: ItemType) => `${item.path} ${item.summary ?? ''}`}
|
||||
bind:filteredItems={pickerFilteredItems}
|
||||
/>
|
||||
|
||||
{#if wsState !== 'closed'}
|
||||
<div class="flex flex-row gap-2 items-center mb-3 w-full">
|
||||
<ToggleButtonGroup bind:selected={pickerKind} class="w-fit">
|
||||
{#snippet children({ item })}
|
||||
<ToggleButton value="all" label="All" size="md" {item} />
|
||||
<ToggleButton value="script" icon={Code2} label="Scripts" size="md" {item} />
|
||||
<ToggleButton
|
||||
value="flow"
|
||||
label="Flows"
|
||||
icon={FlowIcon}
|
||||
selectedColor="#14b8a6"
|
||||
size="md"
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton
|
||||
value="raw_app"
|
||||
label="Apps"
|
||||
icon={LayoutDashboard}
|
||||
selectedColor="#fb923c"
|
||||
size="md"
|
||||
{item}
|
||||
/>
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
|
||||
<div class="relative text-primary flex-1 min-w-[100px]">
|
||||
<!-- svelte-ignore a11y_autofocus -->
|
||||
<TextInput
|
||||
inputProps={{
|
||||
autofocus: true,
|
||||
placeholder: HOME_SEARCH_PLACEHOLDER
|
||||
}}
|
||||
size="md"
|
||||
bind:value={pickerFilter}
|
||||
class="!pr-10"
|
||||
/>
|
||||
<div class="absolute right-0 top-0 mt-2 mr-4 text-secondary" aria-hidden="true">
|
||||
<Search size={16} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if wsState === 'closed'}
|
||||
<Alert type="warning" title="Dev server is not running">
|
||||
Start it from your workspace root with
|
||||
<code class="text-xs px-1 py-0.5 rounded bg-surface-secondary">wmill dev</code>
|
||||
to preview your flows, scripts, and apps.
|
||||
</Alert>
|
||||
{:else if pickerItems.length === 0}
|
||||
<div class="text-sm text-secondary"
|
||||
>No flows, scripts, or apps detected in this workspace.</div
|
||||
>
|
||||
{:else if pickerGroups.length === 0}
|
||||
<div class="text-sm text-secondary">No items match the search.</div>
|
||||
{:else}
|
||||
<div class="border rounded-md bg-surface-tertiary overflow-hidden">
|
||||
{#each pickerGroups as group ('folderName' in group ? `f__${group.folderName}` : 'username' in group ? `u__${group.username}` : `i__${group.type}__${group.path}`)}
|
||||
{@render treeNode(group, 0)}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{:else if mode == 'script'}
|
||||
<div class="flex flex-col min-h-full min-h-screen overflow-auto">
|
||||
<div class="absolute top-0 left-2">
|
||||
<DarkModeToggle bind:darkMode bind:this={darkModeToggle} forcedDarkMode={false} />
|
||||
@@ -855,7 +1196,11 @@
|
||||
</div>
|
||||
{:else}
|
||||
<!-- <div class="h-full w-full grid grid-cols-2"> -->
|
||||
<div class="h-full w-full">
|
||||
<div
|
||||
class="h-full w-full"
|
||||
bind:clientWidth={flowContainerWidth}
|
||||
bind:clientHeight={flowContainerHeight}
|
||||
>
|
||||
<div class="flex flex-col max-h-screen h-full relative" bind:clientWidth={paneWidth}>
|
||||
<div class="absolute top-0 left-2">
|
||||
<DarkModeToggle bind:darkMode bind:this={darkModeToggle} forcedDarkMode={false} />
|
||||
@@ -866,83 +1211,93 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center pt-1 z-50 absolute gap-2 {compactPreview ? 'left-1/2 -translate-x-1/2 top-14' : '-translate-x-[100%] right-2 top-2'}">
|
||||
<FlowPreviewButtons
|
||||
{suspendStatus}
|
||||
bind:this={flowPreviewButtons}
|
||||
{onJobDone}
|
||||
bind:localModuleStates
|
||||
onRunPreview={() => {
|
||||
showJobStatus = true
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Splitpanes horizontal class="max-h-screen grow min-h-0">
|
||||
<Splitpanes horizontal={flowHorizontalSplit} class="min-h-0 max-h-screen grow">
|
||||
<Pane size={67}>
|
||||
{#if flowStore.val?.value?.modules}
|
||||
<div id="flow-editor"></div>
|
||||
<FlowModuleSchemaMap
|
||||
bind:this={flowModuleSchemaMap}
|
||||
disableAi
|
||||
disableTutorials
|
||||
smallErrorHandler={true}
|
||||
disableStaticInputs
|
||||
localModuleStates={showJobStatus ? localModuleStates : {}}
|
||||
onTestUpTo={flowPreviewButtons?.testUpTo}
|
||||
testModuleStates={modulesTestStates}
|
||||
isOwner={flowPreviewContent?.getIsOwner?.()}
|
||||
onTestFlow={flowPreviewButtons?.runPreview}
|
||||
isRunning={flowPreviewContent?.getIsRunning?.()}
|
||||
onCancelTestFlow={flowPreviewContent?.cancelTest}
|
||||
onOpenPreview={flowPreviewButtons?.openPreview}
|
||||
onHideJobStatus={resetModulesStates}
|
||||
flowJob={job}
|
||||
{showJobStatus}
|
||||
onDelete={(id) => {
|
||||
delete localModuleStates[id]
|
||||
delete modulesTestStates.states[id]
|
||||
}}
|
||||
{flowHasChanged}
|
||||
/>
|
||||
{:else}
|
||||
<div class="text-red-400 mt-20">Missing flow modules</div>
|
||||
{/if}
|
||||
</Pane>
|
||||
<div class="relative h-full w-full">
|
||||
{#if flowStore.val?.value?.modules}
|
||||
<div id="flow-editor"></div>
|
||||
<div
|
||||
class="flex justify-center pt-1 z-50 absolute gap-2 {compactPreview
|
||||
? 'left-1/2 -translate-x-1/2 top-14'
|
||||
: 'right-2 top-2'}"
|
||||
>
|
||||
<FlowPreviewButtons
|
||||
{suspendStatus}
|
||||
bind:this={flowPreviewButtons}
|
||||
{onJobDone}
|
||||
bind:localModuleStates
|
||||
onRunPreview={() => {
|
||||
showJobStatus = true
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<FlowModuleSchemaMap
|
||||
bind:this={flowModuleSchemaMap}
|
||||
disableAi
|
||||
disableTutorials
|
||||
smallErrorHandler={true}
|
||||
disableStaticInputs
|
||||
localModuleStates={showJobStatus ? localModuleStates : {}}
|
||||
onTestUpTo={flowPreviewButtons?.testUpTo}
|
||||
testModuleStates={modulesTestStates}
|
||||
isOwner={flowPreviewContent?.getIsOwner?.()}
|
||||
onTestFlow={flowPreviewButtons?.runPreview}
|
||||
isRunning={flowPreviewContent?.getIsRunning?.()}
|
||||
onCancelTestFlow={flowPreviewContent?.cancelTest}
|
||||
onOpenPreview={flowPreviewButtons?.openPreview}
|
||||
onHideJobStatus={resetModulesStates}
|
||||
flowJob={job}
|
||||
{showJobStatus}
|
||||
onDelete={(id) => {
|
||||
delete localModuleStates[id]
|
||||
delete modulesTestStates.states[id]
|
||||
}}
|
||||
{flowHasChanged}
|
||||
controlsPosition="bottom"
|
||||
/>
|
||||
{:else}
|
||||
<div class="text-red-400 mt-20">Missing flow modules</div>
|
||||
{/if}
|
||||
</div></Pane
|
||||
>
|
||||
|
||||
<Pane size={33}>
|
||||
{#key reload}
|
||||
<FlowEditorPanel
|
||||
enableAi
|
||||
noEditor
|
||||
on:applyArgs={(ev) => {
|
||||
if (ev.detail.kind === 'preprocessor') {
|
||||
stepsInputArgs.setStepArgs('preprocessor', ev.detail.args ?? {})
|
||||
selectionManager.selectId('preprocessor')
|
||||
} else {
|
||||
previewArgsStore.val = ev.detail.args ?? {}
|
||||
flowPreviewButtons?.openPreview()
|
||||
}
|
||||
}}
|
||||
onTestFlow={flowPreviewButtons?.runPreview}
|
||||
{job}
|
||||
isOwner={flowPreviewContent?.getIsOwner()}
|
||||
{suspendStatus}
|
||||
onOpenDetails={flowPreviewButtons?.openPreview}
|
||||
previewOpen={flowPreviewButtons?.getPreviewOpen()}
|
||||
/>
|
||||
{/key}
|
||||
<div class="h-full w-full pl-0.5">
|
||||
{#if selectedModule}
|
||||
<div
|
||||
class="flex items-center gap-2 px-3 py-1.5 border-b border-border bg-surface shrink-0"
|
||||
>
|
||||
<span class="text-xs text-secondary shrink-0">{selectedModule.id} summary</span>
|
||||
<TextInput
|
||||
inputProps={{ placeholder: 'Summary' }}
|
||||
bind:value={selectedModule.summary}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{#key reload}
|
||||
<FlowEditorPanel
|
||||
enableAi
|
||||
noEditor
|
||||
on:applyArgs={(ev) => {
|
||||
if (ev.detail.kind === 'preprocessor') {
|
||||
stepsInputArgs.setStepArgs('preprocessor', ev.detail.args ?? {})
|
||||
selectionManager.selectId('preprocessor')
|
||||
} else {
|
||||
previewArgsStore.val = ev.detail.args ?? {}
|
||||
flowPreviewButtons?.openPreview()
|
||||
}
|
||||
}}
|
||||
onTestFlow={flowPreviewButtons?.runPreview}
|
||||
{job}
|
||||
isOwner={flowPreviewContent?.getIsOwner()}
|
||||
{suspendStatus}
|
||||
onOpenDetails={flowPreviewButtons?.openPreview}
|
||||
previewOpen={flowPreviewButtons?.getPreviewOpen()}
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
{#if selectedModule}
|
||||
<div class="flex items-center gap-2 px-3 py-1.5 border-t border-border bg-surface shrink-0">
|
||||
<span class="text-xs text-secondary shrink-0">{selectedModule.id} summary</span>
|
||||
<input
|
||||
type="text"
|
||||
class="text-xs w-full bg-transparent border border-border rounded px-2 py-1 focus:outline-none focus:border-blue-500"
|
||||
placeholder="Summary"
|
||||
bind:value={selectedModule.summary}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -98,6 +98,7 @@
|
||||
suspendStatus?: StateStore<Record<string, { job: Job; nb: number }>>
|
||||
onDelete?: (id: string) => void
|
||||
flowHasChanged?: boolean
|
||||
controlsPosition?: 'top' | 'bottom'
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -127,6 +128,7 @@
|
||||
showJobStatus = false,
|
||||
suspendStatus = $bindable({ val: {} }),
|
||||
onDelete,
|
||||
controlsPosition = 'top',
|
||||
flowHasChanged
|
||||
}: Props = $props()
|
||||
|
||||
@@ -565,7 +567,7 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="z-10 flex-auto grow bg-surface-secondary" bind:clientHeight={minHeight}>
|
||||
<div class="z-10 flex-auto grow min-h-0 bg-surface-secondary" bind:clientHeight={minHeight}>
|
||||
<FlowGraphV2
|
||||
bind:this={graph}
|
||||
earlyStop={flowStore.val.value?.skip_expr !== undefined}
|
||||
@@ -940,6 +942,7 @@
|
||||
{onCancelTestFlow}
|
||||
{onOpenPreview}
|
||||
{onHideJobStatus}
|
||||
{controlsPosition}
|
||||
exitNoteMode={() => (noteMode = false)}
|
||||
onNotePositionUpdate={(noteId, position) => {
|
||||
// Update note position via NoteEditor context in edit mode
|
||||
|
||||
@@ -198,6 +198,7 @@
|
||||
diffBeforeFlow?: OpenFlow
|
||||
currentInputSchema?: Record<string, any>
|
||||
markRemovedAsShadowed?: boolean
|
||||
controlsPosition?: 'top' | 'bottom'
|
||||
outerDivClass?: string
|
||||
}
|
||||
|
||||
@@ -271,6 +272,7 @@
|
||||
onDuplicateMultiple = undefined,
|
||||
onMoveMultiple = undefined,
|
||||
movingIds = undefined,
|
||||
controlsPosition = 'top',
|
||||
outerDivClass = ''
|
||||
}: Props = $props()
|
||||
|
||||
@@ -754,12 +756,16 @@
|
||||
} else {
|
||||
const minY = Math.min(...nodes.map((n) => n.position.y))
|
||||
const maxBottom = Math.max(...nodes.map((n) => n.position.y + NODE.height + 100))
|
||||
height = Math.max(maxBottom - minY, minHeight)
|
||||
const computed = maxBottom - minY
|
||||
height = Math.max(Math.min(computed, maxHeight ?? computed), minHeight)
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
// Track both bounds — updateHeight() reads both, so missing one (as
|
||||
// maxHeight was) leaves height stale when only that bound changes.
|
||||
minHeight
|
||||
maxHeight
|
||||
untrack(() => updateHeight())
|
||||
})
|
||||
|
||||
@@ -1180,7 +1186,7 @@
|
||||
</div>
|
||||
{:else}
|
||||
<Controls
|
||||
position="top-right"
|
||||
position={controlsPosition === 'bottom' ? 'bottom-right' : 'top-right'}
|
||||
orientation="horizontal"
|
||||
showLock={false}
|
||||
fitViewOptions={{ nodes: nodes.filter((n) => n.type !== 'note') }}
|
||||
|
||||
@@ -35,6 +35,13 @@ app related commands
|
||||
- `app lint [app_folder:string]` - Lint a raw app folder to validate structure and buildability
|
||||
- `--fix` - Attempt to fix common issues (not implemented yet)
|
||||
- `app new` - create a new raw app from a template
|
||||
- `--summary <summary:string>` - App summary (short description). Skips the prompt when provided. Triggers non-interactive mode.
|
||||
- `--path <path:string>` - App path (e.g., f/folder/my_app or u/username/my_app). Skips the prompt when provided. Triggers non-interactive mode.
|
||||
- `--framework <framework:string>` - Framework template: react19 | react18 | svelte5 | vue. Skips the prompt when provided. Triggers non-interactive mode.
|
||||
- `--datatable <datatable:string>` - Datatable to wire up. Without this flag in non-interactive mode, no datatable is configured.
|
||||
- `--schema <schema:string>` - Schema to use with --datatable. Created (CREATE SCHEMA IF NOT EXISTS) if it doesn't already exist.
|
||||
- `--overwrite` - Overwrite the target directory if it already exists, without prompting.
|
||||
- `--no-open-in-desktop` - Do not prompt to open the new app in Claude Desktop.
|
||||
- `app generate-agents [app_folder:string]` - regenerate AGENTS.md and DATATABLES.md from remote workspace
|
||||
- `app set-permissioned-as <path:string> <email:string>` - Set the on_behalf_of_email for an app (requires admin or wm_deployers group)
|
||||
|
||||
@@ -71,10 +78,13 @@ workspace dependencies related commands
|
||||
|
||||
### dev
|
||||
|
||||
Launch a dev server that watches for local file changes and auto-pushes them to the remote workspace. Provides live reload for scripts and flows during development.
|
||||
Watch local file changes and live-reload the dev page for preview. Does NOT deploy to the remote workspace — use wmill sync push for that.
|
||||
|
||||
**Options:**
|
||||
- `--includes <pattern...:string>` - Filter paths givena glob pattern or path
|
||||
- `--includes <pattern...:string>` - Filter paths given a glob pattern or path
|
||||
- `--proxy-port <port:number>` - Port for a localhost reverse proxy to the remote Windmill server
|
||||
- `--path <path:string>` - Watch a specific windmill path (e.g., u/admin/my_script or f/my_flow)
|
||||
- `--no-open` - Do not open the browser automatically
|
||||
|
||||
### docs
|
||||
|
||||
|
||||
@@ -1,14 +1,76 @@
|
||||
# Windmill Flow Building Guide
|
||||
|
||||
## CLI Commands
|
||||
## Creating a Flow
|
||||
|
||||
**You — the AI agent — scaffold the flow yourself by running `wmill flow new <path>` with the right flags. Do NOT hand-create the folder + `flow.yaml`, and do NOT tell the user to "run `wmill flow new` and follow the prompts".**
|
||||
|
||||
`wmill flow new` creates the folder with the correct suffix (`__flow` or `.flow` depending on the workspace's `nonDottedPaths` setting), writes a minimal `flow.yaml` shell, and prints Claude-specific next-step hints. Scaffolding by hand skips all of that and often picks the wrong suffix.
|
||||
|
||||
### Step 1 — Gather path + summary by asking the user
|
||||
|
||||
You need two things:
|
||||
|
||||
1. **path** — the windmill path, e.g. `f/folder/my_flow` or `u/username/my_flow`.
|
||||
2. **summary** — a short description of the flow.
|
||||
|
||||
If the user's request didn't supply both, ask for both in a single round-trip. Use whichever interactive question facility your runtime provides — a structured multi-choice tool if available, otherwise plain chat — and provide one or two example values for each (with an "Other" / free-form fallback). Do not guess paths or summaries.
|
||||
|
||||
### Step 2 — Run the command yourself
|
||||
|
||||
```bash
|
||||
wmill flow new f/folder/my_flow --summary "Short description"
|
||||
```
|
||||
|
||||
Add `--description "..."` when the user provided a longer explanation worth preserving separately from the summary.
|
||||
|
||||
### Step 3 — Fill in `flow.yaml`
|
||||
|
||||
Open the generated `flow.yaml` (under the folder the command just created) and replace the empty `value.modules` + `schema` with the real flow definition.
|
||||
|
||||
Create a folder ending with `__flow` and add a `flow.yaml` file with the flow definition.
|
||||
For rawscript modules, use `!inline path/to/script.ts` for the content key. Inline script files should NOT include `.inline_script.` in their names (e.g. use `a.ts`, not `a.inline_script.ts`).
|
||||
After writing, tell the user they can run:
|
||||
- `wmill generate-metadata` - Generate lock files for the flow you modified
|
||||
- `wmill sync push` - Deploy to Windmill
|
||||
|
||||
Do NOT run these commands yourself. Instead, inform the user that they should run them.
|
||||
Once the flow has real content, **offer** to open the visual preview as a one-sentence next step (e.g. "Want me to open the visual preview?"). Don't auto-open — opening the dev page has side effects (browser window, possibly a `launch.json` entry) and the user should consent.
|
||||
|
||||
### Anti-patterns to avoid
|
||||
|
||||
- ❌ Hand-creating the `__flow` folder + `flow.yaml` instead of running `wmill flow new`. You'll miss the suffix-setting resolution, the default shape, and the Claude hints.
|
||||
- ❌ Telling the user to "run `wmill flow new <path>`" — you can and should run it yourself.
|
||||
- ❌ Inventing a path/summary instead of asking the user.
|
||||
|
||||
## CLI Commands — running, previewing, deploying
|
||||
|
||||
After writing, tell the user which command fits what they want to do:
|
||||
|
||||
- `wmill flow preview <flow_path>` — **default when iterating on a local flow.** Runs the local `flow.yaml` against local inline scripts without deploying. Add `--remote` to use deployed workspace scripts for PathScript steps instead of local files.
|
||||
- `wmill flow run <path>` — runs the flow **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
|
||||
- `wmill generate-metadata` — regenerate stale `.lock` and `.script.yaml` files. By default it scans **scripts, flows, and apps** across the workspace; pass `--skip-flows --skip-apps` (or run from a subdirectory) to limit the scope when you only care about the flow you edited.
|
||||
- `wmill sync push` — deploy local changes to the workspace. Only suggest/run this when the user explicitly asks to deploy/publish/push — not when they say "run", "try", or "test".
|
||||
|
||||
### Preview vs run — choose by intent, not habit
|
||||
|
||||
If the user says "run the flow", "try it", "test it", "does it work" while there are **local edits to a `flow.yaml`**, use `flow preview`. Do NOT push the flow to then `flow run` it — pushing is a deploy, and deploying just to test overwrites the workspace version with untested changes.
|
||||
|
||||
Only use `flow run` when:
|
||||
- The user explicitly says "run the deployed version" / "run what's on the server".
|
||||
- There is no local `flow.yaml` being edited (you're just invoking an existing flow).
|
||||
|
||||
Only use `sync push` when:
|
||||
- The user explicitly asks to deploy, publish, push, or ship.
|
||||
- The preview has already validated the change and the user wants it in the workspace.
|
||||
|
||||
### After writing — offer to run, don't wait passively
|
||||
|
||||
This is about **programmatic execution** (`wmill flow preview -d '<args>'`), which actually runs the flow and has side effects. Visual preview (the `preview` skill) is offered separately — see "Visual preview" below.
|
||||
|
||||
If the user hasn't already told you to run/test the flow, offer it as a one-sentence next step (e.g. "Want me to run `wmill flow preview` with sample args?"). Do not present a multi-option menu.
|
||||
|
||||
If the user already asked to test/run/try the flow in their original request, skip the offer and just execute `wmill flow preview <path> -d '<args>'` directly — pick plausible args from the flow's input schema.
|
||||
|
||||
`wmill flow preview` is safe to run yourself (it does not deploy). `wmill sync push` and `wmill generate-metadata` modify workspace state or local files — only run these when the user explicitly asks; otherwise tell them which to run.
|
||||
|
||||
### Visual preview
|
||||
|
||||
To open the flow visually in the dev page (graph + live reload), use the `preview` skill. Always **offer** it as a one-sentence next step (e.g. "Want me to open the visual preview?") rather than opening it automatically — opening the dev page has side effects (browser window, possibly a `launch.json` entry under MCP-preview branches) the user should consent to. If the user already asked to see/preview/visualize the flow in their original request, skip the offer and just invoke the skill.
|
||||
|
||||
## OpenFlow Schema
|
||||
|
||||
|
||||
@@ -31,15 +31,77 @@ The preprocessor receives a single parameter called \`event\`.
|
||||
|
||||
export const FLOW_BASE = `# Windmill Flow Building Guide
|
||||
|
||||
## CLI Commands
|
||||
## Creating a Flow
|
||||
|
||||
**You — the AI agent — scaffold the flow yourself by running \`wmill flow new <path>\` with the right flags. Do NOT hand-create the folder + \`flow.yaml\`, and do NOT tell the user to "run \`wmill flow new\` and follow the prompts".**
|
||||
|
||||
\`wmill flow new\` creates the folder with the correct suffix (\`__flow\` or \`.flow\` depending on the workspace's \`nonDottedPaths\` setting), writes a minimal \`flow.yaml\` shell, and prints Claude-specific next-step hints. Scaffolding by hand skips all of that and often picks the wrong suffix.
|
||||
|
||||
### Step 1 — Gather path + summary by asking the user
|
||||
|
||||
You need two things:
|
||||
|
||||
1. **path** — the windmill path, e.g. \`f/folder/my_flow\` or \`u/username/my_flow\`.
|
||||
2. **summary** — a short description of the flow.
|
||||
|
||||
If the user's request didn't supply both, ask for both in a single round-trip. Use whichever interactive question facility your runtime provides — a structured multi-choice tool if available, otherwise plain chat — and provide one or two example values for each (with an "Other" / free-form fallback). Do not guess paths or summaries.
|
||||
|
||||
### Step 2 — Run the command yourself
|
||||
|
||||
\`\`\`bash
|
||||
wmill flow new f/folder/my_flow --summary "Short description"
|
||||
\`\`\`
|
||||
|
||||
Add \`--description "..."\` when the user provided a longer explanation worth preserving separately from the summary.
|
||||
|
||||
### Step 3 — Fill in \`flow.yaml\`
|
||||
|
||||
Open the generated \`flow.yaml\` (under the folder the command just created) and replace the empty \`value.modules\` + \`schema\` with the real flow definition.
|
||||
|
||||
Create a folder ending with \`__flow\` and add a \`flow.yaml\` file with the flow definition.
|
||||
For rawscript modules, use \`!inline path/to/script.ts\` for the content key. Inline script files should NOT include \`.inline_script.\` in their names (e.g. use \`a.ts\`, not \`a.inline_script.ts\`).
|
||||
After writing, tell the user they can run:
|
||||
- \`wmill generate-metadata\` - Generate lock files for the flow you modified
|
||||
- \`wmill sync push\` - Deploy to Windmill
|
||||
|
||||
Do NOT run these commands yourself. Instead, inform the user that they should run them.
|
||||
Once the flow has real content, **offer** to open the visual preview as a one-sentence next step (e.g. "Want me to open the visual preview?"). Don't auto-open — opening the dev page has side effects (browser window, possibly a \`launch.json\` entry) and the user should consent.
|
||||
|
||||
### Anti-patterns to avoid
|
||||
|
||||
- ❌ Hand-creating the \`__flow\` folder + \`flow.yaml\` instead of running \`wmill flow new\`. You'll miss the suffix-setting resolution, the default shape, and the Claude hints.
|
||||
- ❌ Telling the user to "run \`wmill flow new <path>\`" — you can and should run it yourself.
|
||||
- ❌ Inventing a path/summary instead of asking the user.
|
||||
|
||||
## CLI Commands — running, previewing, deploying
|
||||
|
||||
After writing, tell the user which command fits what they want to do:
|
||||
|
||||
- \`wmill flow preview <flow_path>\` — **default when iterating on a local flow.** Runs the local \`flow.yaml\` against local inline scripts without deploying. Add \`--remote\` to use deployed workspace scripts for PathScript steps instead of local files.
|
||||
- \`wmill flow run <path>\` — runs the flow **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
|
||||
- \`wmill generate-metadata\` — regenerate stale \`.lock\` and \`.script.yaml\` files. By default it scans **scripts, flows, and apps** across the workspace; pass \`--skip-flows --skip-apps\` (or run from a subdirectory) to limit the scope when you only care about the flow you edited.
|
||||
- \`wmill sync push\` — deploy local changes to the workspace. Only suggest/run this when the user explicitly asks to deploy/publish/push — not when they say "run", "try", or "test".
|
||||
|
||||
### Preview vs run — choose by intent, not habit
|
||||
|
||||
If the user says "run the flow", "try it", "test it", "does it work" while there are **local edits to a \`flow.yaml\`**, use \`flow preview\`. Do NOT push the flow to then \`flow run\` it — pushing is a deploy, and deploying just to test overwrites the workspace version with untested changes.
|
||||
|
||||
Only use \`flow run\` when:
|
||||
- The user explicitly says "run the deployed version" / "run what's on the server".
|
||||
- There is no local \`flow.yaml\` being edited (you're just invoking an existing flow).
|
||||
|
||||
Only use \`sync push\` when:
|
||||
- The user explicitly asks to deploy, publish, push, or ship.
|
||||
- The preview has already validated the change and the user wants it in the workspace.
|
||||
|
||||
### After writing — offer to run, don't wait passively
|
||||
|
||||
This is about **programmatic execution** (\`wmill flow preview -d '<args>'\`), which actually runs the flow and has side effects. Visual preview (the \`preview\` skill) is offered separately — see "Visual preview" below.
|
||||
|
||||
If the user hasn't already told you to run/test the flow, offer it as a one-sentence next step (e.g. "Want me to run \`wmill flow preview\` with sample args?"). Do not present a multi-option menu.
|
||||
|
||||
If the user already asked to test/run/try the flow in their original request, skip the offer and just execute \`wmill flow preview <path> -d '<args>'\` directly — pick plausible args from the flow's input schema.
|
||||
|
||||
\`wmill flow preview\` is safe to run yourself (it does not deploy). \`wmill sync push\` and \`wmill generate-metadata\` modify workspace state or local files — only run these when the user explicitly asks; otherwise tell them which to run.
|
||||
|
||||
### Visual preview
|
||||
|
||||
To open the flow visually in the dev page (graph + live reload), use the \`preview\` skill. Always **offer** it as a one-sentence next step (e.g. "Want me to open the visual preview?") rather than opening it automatically — opening the dev page has side effects (browser window, possibly a \`launch.json\` entry under MCP-preview branches) the user should consent to. If the user already asked to see/preview/visualize the flow in their original request, skip the offer and just invoke the skill.
|
||||
|
||||
## OpenFlow Schema
|
||||
|
||||
@@ -1794,6 +1856,13 @@ app related commands
|
||||
- \`app lint [app_folder:string]\` - Lint a raw app folder to validate structure and buildability
|
||||
- \`--fix\` - Attempt to fix common issues (not implemented yet)
|
||||
- \`app new\` - create a new raw app from a template
|
||||
- \`--summary <summary:string>\` - App summary (short description). Skips the prompt when provided. Triggers non-interactive mode.
|
||||
- \`--path <path:string>\` - App path (e.g., f/folder/my_app or u/username/my_app). Skips the prompt when provided. Triggers non-interactive mode.
|
||||
- \`--framework <framework:string>\` - Framework template: react19 | react18 | svelte5 | vue. Skips the prompt when provided. Triggers non-interactive mode.
|
||||
- \`--datatable <datatable:string>\` - Datatable to wire up. Without this flag in non-interactive mode, no datatable is configured.
|
||||
- \`--schema <schema:string>\` - Schema to use with --datatable. Created (CREATE SCHEMA IF NOT EXISTS) if it doesn't already exist.
|
||||
- \`--overwrite\` - Overwrite the target directory if it already exists, without prompting.
|
||||
- \`--no-open-in-desktop\` - Do not prompt to open the new app in Claude Desktop.
|
||||
- \`app generate-agents [app_folder:string]\` - regenerate AGENTS.md and DATATABLES.md from remote workspace
|
||||
- \`app set-permissioned-as <path:string> <email:string>\` - Set the on_behalf_of_email for an app (requires admin or wm_deployers group)
|
||||
|
||||
@@ -1830,10 +1899,13 @@ workspace dependencies related commands
|
||||
|
||||
### dev
|
||||
|
||||
Launch a dev server that watches for local file changes and auto-pushes them to the remote workspace. Provides live reload for scripts and flows during development.
|
||||
Watch local file changes and live-reload the dev page for preview. Does NOT deploy to the remote workspace — use wmill sync push for that.
|
||||
|
||||
**Options:**
|
||||
- \`--includes <pattern...:string>\` - Filter paths givena glob pattern or path
|
||||
- \`--includes <pattern...:string>\` - Filter paths given a glob pattern or path
|
||||
- \`--proxy-port <port:number>\` - Port for a localhost reverse proxy to the remote Windmill server
|
||||
- \`--path <path:string>\` - Watch a specific windmill path (e.g., u/admin/my_script or f/my_flow)
|
||||
- \`--no-open\` - Do not open the browser automatically
|
||||
|
||||
### docs
|
||||
|
||||
|
||||
@@ -40,6 +40,13 @@ app related commands
|
||||
- `app lint [app_folder:string]` - Lint a raw app folder to validate structure and buildability
|
||||
- `--fix` - Attempt to fix common issues (not implemented yet)
|
||||
- `app new` - create a new raw app from a template
|
||||
- `--summary <summary:string>` - App summary (short description). Skips the prompt when provided. Triggers non-interactive mode.
|
||||
- `--path <path:string>` - App path (e.g., f/folder/my_app or u/username/my_app). Skips the prompt when provided. Triggers non-interactive mode.
|
||||
- `--framework <framework:string>` - Framework template: react19 | react18 | svelte5 | vue. Skips the prompt when provided. Triggers non-interactive mode.
|
||||
- `--datatable <datatable:string>` - Datatable to wire up. Without this flag in non-interactive mode, no datatable is configured.
|
||||
- `--schema <schema:string>` - Schema to use with --datatable. Created (CREATE SCHEMA IF NOT EXISTS) if it doesn't already exist.
|
||||
- `--overwrite` - Overwrite the target directory if it already exists, without prompting.
|
||||
- `--no-open-in-desktop` - Do not prompt to open the new app in Claude Desktop.
|
||||
- `app generate-agents [app_folder:string]` - regenerate AGENTS.md and DATATABLES.md from remote workspace
|
||||
- `app set-permissioned-as <path:string> <email:string>` - Set the on_behalf_of_email for an app (requires admin or wm_deployers group)
|
||||
|
||||
@@ -76,10 +83,13 @@ workspace dependencies related commands
|
||||
|
||||
### dev
|
||||
|
||||
Launch a dev server that watches for local file changes and auto-pushes them to the remote workspace. Provides live reload for scripts and flows during development.
|
||||
Watch local file changes and live-reload the dev page for preview. Does NOT deploy to the remote workspace — use wmill sync push for that.
|
||||
|
||||
**Options:**
|
||||
- `--includes <pattern...:string>` - Filter paths givena glob pattern or path
|
||||
- `--includes <pattern...:string>` - Filter paths given a glob pattern or path
|
||||
- `--proxy-port <port:number>` - Port for a localhost reverse proxy to the remote Windmill server
|
||||
- `--path <path:string>` - Watch a specific windmill path (e.g., u/admin/my_script or f/my_flow)
|
||||
- `--no-open` - Do not open the browser automatically
|
||||
|
||||
### docs
|
||||
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
---
|
||||
name: preview
|
||||
description: MUST use when opening the Windmill dev page / visual preview of a flow, script, or app. Triggers on words like preview, open, navigate to, visualize, see the flow/app/script, and after writing a flow/script/app for visual verification.
|
||||
---
|
||||
|
||||
# Windmill Preview Workflow
|
||||
|
||||
Use this skill any time the user wants to **see**, **open**, **navigate to**, **visualize**, or **preview** a flow, script, or app — and any time you've just finished writing one and want to offer visual verification.
|
||||
|
||||
The Windmill dev page renders the flow graph / script editor, lets the user step through steps, and live-reloads on every save. It runs locally via `wmill dev` and is reached on a localhost port.
|
||||
|
||||
## Two independent decisions
|
||||
|
||||
### 1. Mode: proxy or direct?
|
||||
|
||||
`wmill dev` runs in two modes; pick by asking what kind of URL whatever will display the preview needs.
|
||||
|
||||
- **Proxy** (`--proxy-port <port>`) — exposes the dev page on `http://localhost:<port>/`. Use it when the embedder you'll hand the URL to **only accepts localhost URLs** (most in-IDE / in-chat preview embedders do, because they sandbox cross-origin loads).
|
||||
- **Direct** (default) — the user's browser loads the dev page from the remote workspace's HTTPS URL; the local `wmill dev` only runs the WebSocket back-channel for live reload. Use it when the URL will be opened in a regular browser tab.
|
||||
|
||||
Default to **direct** unless you have a specific embedder that needs localhost.
|
||||
|
||||
### 2. Who starts the server?
|
||||
|
||||
- **You start it** in the background. Spawn `wmill dev …` (or `wmill app dev …`) yourself, capture the URL it prints, do whatever's next (open a tab, hand the URL to an embedder).
|
||||
- **The runtime starts it from `.claude/launch.json`.** Some runtimes (currently the Claude Desktop / Claude Code MCP preview integration — tools prefixed with `mcp__Claude_Preview__`) can read a `launch.json` configuration and launch the dev server on demand when you invoke their preview tool. **Only take this path if you actually have such a tool** — otherwise nothing reads the file and `wmill dev` never starts.
|
||||
|
||||
The two decisions compose. The common cases:
|
||||
|
||||
| Embedder | Needs localhost? | launch.json runtime? | What to do |
|
||||
|---|---|---|---|
|
||||
| Regular browser tab | No | n/a | Direct mode, you start it, give URL to user |
|
||||
| IDE / chat preview pane that takes any URL | No | No | Direct mode, you start it, point the embedder at the printed URL |
|
||||
| IDE / chat preview pane that only accepts localhost | Yes | No | Proxy mode, you start it, point the embedder at `http://localhost:<port>/` |
|
||||
| Claude Desktop / Code MCP preview | Yes | Yes | Proxy mode, write a `launch.json` entry, invoke the MCP tool |
|
||||
|
||||
Never start the proxy "just in case" — it adds the localhost hop for no benefit when no embedder needs it.
|
||||
|
||||
## Starting the server yourself
|
||||
|
||||
Use this when no `launch.json`-aware runtime is available, regardless of mode.
|
||||
|
||||
For flows / scripts:
|
||||
```bash
|
||||
# Direct mode — gives you the remote dev-page URL
|
||||
wmill dev --path <wmill_path> --no-open
|
||||
|
||||
# Proxy mode — gives you a localhost URL that 302s to the remote dev page
|
||||
wmill dev --proxy-port 4000 --path <wmill_path> --no-open
|
||||
```
|
||||
|
||||
For apps:
|
||||
```bash
|
||||
cd <app_path>__raw_app && wmill app dev --no-open --port 4000
|
||||
```
|
||||
|
||||
Each command prints the URL on stdout. Line shapes differ:
|
||||
|
||||
- `wmill dev --no-open` (direct) prints `Go to <url>` with the full remote URL (workspace, token, path baked in).
|
||||
- `wmill dev --proxy-port` prints `Dev proxy listening on http://localhost:<port>` — the URL to hand to an embedder is `http://localhost:<port>/`.
|
||||
- `wmill app dev --no-open` prints `🚀 Dev server running at <url>` — the local app server.
|
||||
|
||||
Capture the URL with a loose match (the first `https?://…` token after startup) and either hand it to your embedder or relay it to the user: *"Preview is running — open `<url>` in your browser."* Don't construct the URL yourself; you don't have the workspace ID or auth token.
|
||||
|
||||
These commands are long-running — start them in the background, don't block waiting.
|
||||
|
||||
## Letting `launch.json` start the server (Claude Desktop / Code MCP only)
|
||||
|
||||
Take this path when **and only when** an `mcp__Claude_Preview__*` MCP tool is exposed in your tool list. Skip it otherwise — without an MCP tool reading the file, `wmill dev` never starts.
|
||||
|
||||
**Each flow / script / app gets its own named entry** in the user's `.claude/launch.json` so multiple previews coexist without colliding — each entry pins a different port + path. Never reuse a generic "windmill" entry for different targets.
|
||||
|
||||
### Step 1 — Reuse or add a per-target entry in `.claude/launch.json`
|
||||
|
||||
Convention: name the entry `windmill: <wmill_path>` (e.g. `windmill: f/test/my_flow`).
|
||||
|
||||
- **Entry already exists** → reuse it; note its `port` for the next step.
|
||||
- **Not there** → add one. Pick a port not already taken by another entry (start at 4000 and bump). Shape:
|
||||
|
||||
For flows / scripts:
|
||||
```json
|
||||
{
|
||||
"name": "windmill: f/test/my_flow",
|
||||
"runtimeExecutable": "bash",
|
||||
"runtimeArgs": ["-c", "wmill dev --proxy-port ${PORT:-4000} --path f/test/my_flow --no-open"],
|
||||
"port": 4000,
|
||||
"autoPort": true
|
||||
}
|
||||
```
|
||||
|
||||
For apps (`*__raw_app/`), `wmill app dev` is the equivalent — runs from the app folder, no `--path`:
|
||||
```json
|
||||
{
|
||||
"name": "windmill: f/test/my_app",
|
||||
"runtimeExecutable": "bash",
|
||||
"runtimeArgs": ["-c", "cd f/test/my_app__raw_app && wmill app dev --no-open --port ${PORT:-4001}"],
|
||||
"port": 4001,
|
||||
"autoPort": true
|
||||
}
|
||||
```
|
||||
|
||||
If `.claude/launch.json` doesn't exist yet, create it with the standard shell `{ "version": "0.0.1", "configurations": [...] }`.
|
||||
|
||||
### Step 2 — Invoke the MCP preview tool
|
||||
|
||||
Point it at the entry you just added/found. Use `http://localhost:<port>/` as the URL — the proxy's redirect at `/` is what appends the workspace ID, the auth token, and the path. Do **NOT** construct a `/dev?...` URL yourself.
|
||||
|
||||
The MCP tool launches the configuration on demand, so you don't need to start the `wmill dev` process manually.
|
||||
|
||||
## Non-visual alternative
|
||||
|
||||
If the user wants a programmatic test rather than a visual one:
|
||||
- Flow: `wmill flow preview <path> -d '<args>'`
|
||||
- Script: `wmill script preview <path> -d '<args>'`
|
||||
|
||||
Both print the job result, are safe to run yourself, and don't deploy.
|
||||
|
||||
## Anti-patterns to avoid
|
||||
|
||||
- ❌ Writing a `.claude/launch.json` entry when no `mcp__Claude_Preview__*` tool is in your tool list. Nothing will read the file; the server never starts. Spawn `wmill dev` yourself instead.
|
||||
- ❌ Starting the proxy when no embedder needs a localhost URL. Direct mode is the right choice — the proxy is overhead with no purpose.
|
||||
- ❌ Reusing a single generic `launch.json` entry for every preview target. Each flow/script/app gets its own named entry on its own port — that's how multiple sessions coexist without one preview clobbering another.
|
||||
- ❌ Mutating an existing entry's `--path` to retarget it. Add a new entry instead.
|
||||
- ❌ Constructing `http://localhost:<port>/dev?path=<X>` yourself. The proxy's `/` redirect is what appends the workspace ID and auth token; bypassing it gives a broken page. Always use `http://localhost:<port>/`.
|
||||
- ❌ Starting `wmill dev` in the foreground (you'll hang). Always background.
|
||||
- ❌ Listing both "open in IDE pane" and "open in browser" as a menu — pick one based on context.
|
||||
@@ -9,11 +9,70 @@ Raw apps let you build custom frontends with React, Svelte, or Vue that connect
|
||||
|
||||
## Creating a Raw App
|
||||
|
||||
**You — the AI agent — create the app yourself by running `wmill app new` with the right flags. Do NOT tell the user to "run `wmill app new` and follow the prompts" or wait for them to do it.** The bare `wmill app new` is an interactive wizard that hangs waiting for stdin in any non-TTY context (which includes you). Always pass flags.
|
||||
|
||||
### Step 1 — Gather the three required values by asking the user
|
||||
|
||||
You need three things to run the command:
|
||||
|
||||
1. **summary** — a short description of the app
|
||||
2. **path** — the windmill path, e.g. `f/folder/my_app` or `u/username/my_app`
|
||||
3. **framework** — one of `react19` (recommended), `react18`, `svelte5`, `vue`
|
||||
|
||||
If the user's request did not supply *every* one of these explicitly, ask. Do not guess values, do not invent paths, do not pick a framework on the user's behalf, do not "just use react19 because it's the default".
|
||||
|
||||
Use whichever interactive question facility your runtime provides — a structured multi-choice tool if available, otherwise plain chat — and group all missing fields into a single round-trip so the user answers them at once:
|
||||
|
||||
- For `framework` — multiple-choice with the four allowed values; mark `react19` as `(Recommended)` and put it first.
|
||||
- For `summary` and `path` — provide one or two example values as multiple-choice options (the user can pick "Other" to type a free-form answer).
|
||||
|
||||
Only proceed once you have concrete values for all three. If the user replies with something ambiguous, ask again rather than guessing.
|
||||
|
||||
### Step 2 — Run the command yourself
|
||||
|
||||
Once you have summary + path + framework, run it:
|
||||
|
||||
```bash
|
||||
wmill app new \
|
||||
--summary "Customer dashboard" \
|
||||
--path f/sales/dashboard \
|
||||
--framework react19
|
||||
```
|
||||
|
||||
That's the minimum. The datatable wizard and the "Open in Claude Desktop?" prompt are skipped silently because passing any of `--summary`/`--path`/`--framework` puts the command in non-interactive mode.
|
||||
|
||||
### Optional flags
|
||||
|
||||
Layer these in only when the user asked for them:
|
||||
|
||||
| Flag | When to add it |
|
||||
|---|---|
|
||||
| `--datatable <name>` | The user wants this app wired to a specific Windmill datatable. Without it, the app is created with no datatable. |
|
||||
| `--schema <name>` | Together with `--datatable`. Creates the schema with `CREATE SCHEMA IF NOT EXISTS` if it doesn't already exist. |
|
||||
| `--overwrite` | The target directory already exists and the user said it's OK to replace. Without it, non-interactive mode aborts with an error so you don't clobber existing work. |
|
||||
| `--no-open-in-desktop` | Already implied in non-interactive mode; only needed if you're somehow running interactively. |
|
||||
|
||||
### Step 3 — Offer the visual preview
|
||||
|
||||
After `wmill app new` and any initial edits to `App.tsx` / `index.tsx`, **offer** to open the visual preview as a one-sentence next step (e.g. "Want me to open the visual preview?"). Don't auto-open — opening the dev page has side effects (browser window, possibly a `launch.json` entry when an embedded preview tool is in play) the user should consent to.
|
||||
|
||||
For apps the preview command runs from the app folder (`cd <app_path>__raw_app && wmill app dev …`); the `preview` skill picks the proxy vs direct branch based on whether the runtime exposes a tool that can embed a localhost URL. If the user already asked to see/preview/visualize the app in their original request, skip the offer and just invoke the skill.
|
||||
|
||||
### Anti-patterns to avoid
|
||||
|
||||
- ❌ Running `wmill app new` with no flags (the prompt will hang).
|
||||
- ❌ Telling the user to "run `wmill app new` and follow the prompts" — that's a step backwards from what you can do directly.
|
||||
- ❌ Inventing a path/summary/framework instead of asking the user.
|
||||
- ❌ Defaulting to `react19` because the user didn't say — even sensible defaults must be confirmed.
|
||||
- ❌ Passing `--overwrite` automatically when the directory exists — confirm with the user first.
|
||||
|
||||
### Interactive (only when a human is at the terminal)
|
||||
|
||||
```bash
|
||||
wmill app new
|
||||
```
|
||||
|
||||
This interactive command creates a complete app structure with your choice of frontend framework (React, Svelte, or Vue).
|
||||
This is the wizard. It only works when run by a human in a real terminal. Don't call it this way from an agent.
|
||||
|
||||
## App Structure
|
||||
|
||||
@@ -237,12 +296,13 @@ data:
|
||||
|
||||
## CLI Commands
|
||||
|
||||
Tell the user they can run these commands (do NOT run them yourself):
|
||||
`wmill app new` is the exception: you run it yourself, with flags, per the "Creating a Raw App" section above.
|
||||
|
||||
For everything else, tell the user which command fits their intent and let them run it — these touch the workspace or local lock files, and the user should consent each time:
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `wmill app new` | Create a new raw app interactively |
|
||||
| `wmill app dev` | Start dev server with live reload |
|
||||
| `wmill app dev` | Start dev server with live reload (see the `preview` skill for the full open-the-app-in-the-IDE-pane procedure). |
|
||||
| `wmill app generate-agents` | Refresh AGENTS.md and DATATABLES.md |
|
||||
| `wmill generate-metadata` | Generate lock files for backend runnables |
|
||||
| `wmill sync push` | Deploy app to Windmill |
|
||||
|
||||
@@ -5,15 +5,77 @@ description: MUST use when creating flows.
|
||||
|
||||
# Windmill Flow Building Guide
|
||||
|
||||
## CLI Commands
|
||||
## Creating a Flow
|
||||
|
||||
**You — the AI agent — scaffold the flow yourself by running `wmill flow new <path>` with the right flags. Do NOT hand-create the folder + `flow.yaml`, and do NOT tell the user to "run `wmill flow new` and follow the prompts".**
|
||||
|
||||
`wmill flow new` creates the folder with the correct suffix (`__flow` or `.flow` depending on the workspace's `nonDottedPaths` setting), writes a minimal `flow.yaml` shell, and prints Claude-specific next-step hints. Scaffolding by hand skips all of that and often picks the wrong suffix.
|
||||
|
||||
### Step 1 — Gather path + summary by asking the user
|
||||
|
||||
You need two things:
|
||||
|
||||
1. **path** — the windmill path, e.g. `f/folder/my_flow` or `u/username/my_flow`.
|
||||
2. **summary** — a short description of the flow.
|
||||
|
||||
If the user's request didn't supply both, ask for both in a single round-trip. Use whichever interactive question facility your runtime provides — a structured multi-choice tool if available, otherwise plain chat — and provide one or two example values for each (with an "Other" / free-form fallback). Do not guess paths or summaries.
|
||||
|
||||
### Step 2 — Run the command yourself
|
||||
|
||||
```bash
|
||||
wmill flow new f/folder/my_flow --summary "Short description"
|
||||
```
|
||||
|
||||
Add `--description "..."` when the user provided a longer explanation worth preserving separately from the summary.
|
||||
|
||||
### Step 3 — Fill in `flow.yaml`
|
||||
|
||||
Open the generated `flow.yaml` (under the folder the command just created) and replace the empty `value.modules` + `schema` with the real flow definition.
|
||||
|
||||
Create a folder ending with `__flow` and add a `flow.yaml` file with the flow definition.
|
||||
For rawscript modules, use `!inline path/to/script.ts` for the content key. Inline script files should NOT include `.inline_script.` in their names (e.g. use `a.ts`, not `a.inline_script.ts`).
|
||||
After writing, tell the user they can run:
|
||||
- `wmill generate-metadata` - Generate lock files for the flow you modified
|
||||
- `wmill sync push` - Deploy to Windmill
|
||||
|
||||
Do NOT run these commands yourself. Instead, inform the user that they should run them.
|
||||
Once the flow has real content, **offer** to open the visual preview as a one-sentence next step (e.g. "Want me to open the visual preview?"). Don't auto-open — opening the dev page has side effects (browser window, possibly a `launch.json` entry) and the user should consent.
|
||||
|
||||
### Anti-patterns to avoid
|
||||
|
||||
- ❌ Hand-creating the `__flow` folder + `flow.yaml` instead of running `wmill flow new`. You'll miss the suffix-setting resolution, the default shape, and the Claude hints.
|
||||
- ❌ Telling the user to "run `wmill flow new <path>`" — you can and should run it yourself.
|
||||
- ❌ Inventing a path/summary instead of asking the user.
|
||||
|
||||
## CLI Commands — running, previewing, deploying
|
||||
|
||||
After writing, tell the user which command fits what they want to do:
|
||||
|
||||
- `wmill flow preview <flow_path>` — **default when iterating on a local flow.** Runs the local `flow.yaml` against local inline scripts without deploying. Add `--remote` to use deployed workspace scripts for PathScript steps instead of local files.
|
||||
- `wmill flow run <path>` — runs the flow **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
|
||||
- `wmill generate-metadata` — regenerate stale `.lock` and `.script.yaml` files. By default it scans **scripts, flows, and apps** across the workspace; pass `--skip-flows --skip-apps` (or run from a subdirectory) to limit the scope when you only care about the flow you edited.
|
||||
- `wmill sync push` — deploy local changes to the workspace. Only suggest/run this when the user explicitly asks to deploy/publish/push — not when they say "run", "try", or "test".
|
||||
|
||||
### Preview vs run — choose by intent, not habit
|
||||
|
||||
If the user says "run the flow", "try it", "test it", "does it work" while there are **local edits to a `flow.yaml`**, use `flow preview`. Do NOT push the flow to then `flow run` it — pushing is a deploy, and deploying just to test overwrites the workspace version with untested changes.
|
||||
|
||||
Only use `flow run` when:
|
||||
- The user explicitly says "run the deployed version" / "run what's on the server".
|
||||
- There is no local `flow.yaml` being edited (you're just invoking an existing flow).
|
||||
|
||||
Only use `sync push` when:
|
||||
- The user explicitly asks to deploy, publish, push, or ship.
|
||||
- The preview has already validated the change and the user wants it in the workspace.
|
||||
|
||||
### After writing — offer to run, don't wait passively
|
||||
|
||||
This is about **programmatic execution** (`wmill flow preview -d '<args>'`), which actually runs the flow and has side effects. Visual preview (the `preview` skill) is offered separately — see "Visual preview" below.
|
||||
|
||||
If the user hasn't already told you to run/test the flow, offer it as a one-sentence next step (e.g. "Want me to run `wmill flow preview` with sample args?"). Do not present a multi-option menu.
|
||||
|
||||
If the user already asked to test/run/try the flow in their original request, skip the offer and just execute `wmill flow preview <path> -d '<args>'` directly — pick plausible args from the flow's input schema.
|
||||
|
||||
`wmill flow preview` is safe to run yourself (it does not deploy). `wmill sync push` and `wmill generate-metadata` modify workspace state or local files — only run these when the user explicitly asks; otherwise tell them which to run.
|
||||
|
||||
### Visual preview
|
||||
|
||||
To open the flow visually in the dev page (graph + live reload), use the `preview` skill. Always **offer** it as a one-sentence next step (e.g. "Want me to open the visual preview?") rather than opening it automatically — opening the dev page has side effects (browser window, possibly a `launch.json` entry under MCP-preview branches) the user should consent to. If the user already asked to see/preview/visualize the flow in their original request, skip the offer and just invoke the skill.
|
||||
|
||||
## OpenFlow Schema
|
||||
|
||||
|
||||
@@ -5,11 +5,36 @@ description: MUST use when writing Bash scripts.
|
||||
|
||||
## CLI Commands
|
||||
|
||||
Place scripts in a folder. After writing, tell the user they can run:
|
||||
- `wmill generate-metadata` - Generate .script.yaml and .lock files
|
||||
- `wmill sync push` - Deploy to Windmill
|
||||
Place scripts in a folder.
|
||||
|
||||
Do NOT run these commands yourself. Instead, inform the user that they should run them.
|
||||
After writing, tell the user which command fits what they want to do:
|
||||
|
||||
- `wmill script preview <script_path>` — **default when iterating on a local script.** Runs the local file without deploying.
|
||||
- `wmill script run <path>` — runs the script **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
|
||||
- `wmill generate-metadata` — generate `.script.yaml` and `.lock` files for the script you modified.
|
||||
- `wmill sync push` — deploy local changes to the workspace. Only suggest/run this when the user explicitly asks to deploy/publish/push — not when they say "run", "try", or "test".
|
||||
|
||||
### Preview vs run — choose by intent, not habit
|
||||
|
||||
If the user says "run the script", "try it", "test it", "does it work" while there are **local edits to the script file**, use `script preview`. Do NOT push the script to then `script run` it — pushing is a deploy, and deploying just to test overwrites the workspace version with untested changes.
|
||||
|
||||
Only use `script run` when:
|
||||
- The user explicitly says "run the deployed version" / "run what's on the server".
|
||||
- There is no local script being edited (you're just invoking an existing script).
|
||||
|
||||
Only use `sync push` when:
|
||||
- The user explicitly asks to deploy, publish, push, or ship.
|
||||
- The preview has already validated the change and the user wants it in the workspace.
|
||||
|
||||
### After writing — offer to test, don't wait passively
|
||||
|
||||
If the user hasn't already told you to run/test/preview the script, offer it as a one-sentence next step (e.g. "Want me to run `wmill script preview` with sample args?"). Do not present a multi-option menu.
|
||||
|
||||
If the user already asked to test/run/try the script in their original request, skip the offer and just execute `wmill script preview <path> -d '<args>'` directly — pick plausible args from the script's declared parameters. The shape varies by language: `main(...)` for code languages, the SQL dialect's own placeholder syntax (`$1` for PostgreSQL, `?` for MySQL/Snowflake, `@P1` for MSSQL, `@name` for BigQuery, etc.), positional `$1`, `$2`, … for Bash, `param(...)` for PowerShell.
|
||||
|
||||
`wmill script preview` does not deploy, but it still executes script code and may cause side effects; run it yourself when the user asked to test/preview (or after confirming that execution is intended). `wmill sync push` and `wmill generate-metadata` modify workspace state or local files — only run these when the user explicitly asks; otherwise tell them which to run.
|
||||
|
||||
For a **visual** open-the-script-in-the-dev-page preview (rather than `script preview`'s run-and-print-result), use the `preview` skill.
|
||||
|
||||
Use `wmill resource-type list --schema` to discover available resource types.
|
||||
|
||||
|
||||
@@ -5,11 +5,36 @@ description: MUST use when writing BigQuery queries.
|
||||
|
||||
## CLI Commands
|
||||
|
||||
Place scripts in a folder. After writing, tell the user they can run:
|
||||
- `wmill generate-metadata` - Generate .script.yaml and .lock files
|
||||
- `wmill sync push` - Deploy to Windmill
|
||||
Place scripts in a folder.
|
||||
|
||||
Do NOT run these commands yourself. Instead, inform the user that they should run them.
|
||||
After writing, tell the user which command fits what they want to do:
|
||||
|
||||
- `wmill script preview <script_path>` — **default when iterating on a local script.** Runs the local file without deploying.
|
||||
- `wmill script run <path>` — runs the script **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
|
||||
- `wmill generate-metadata` — generate `.script.yaml` and `.lock` files for the script you modified.
|
||||
- `wmill sync push` — deploy local changes to the workspace. Only suggest/run this when the user explicitly asks to deploy/publish/push — not when they say "run", "try", or "test".
|
||||
|
||||
### Preview vs run — choose by intent, not habit
|
||||
|
||||
If the user says "run the script", "try it", "test it", "does it work" while there are **local edits to the script file**, use `script preview`. Do NOT push the script to then `script run` it — pushing is a deploy, and deploying just to test overwrites the workspace version with untested changes.
|
||||
|
||||
Only use `script run` when:
|
||||
- The user explicitly says "run the deployed version" / "run what's on the server".
|
||||
- There is no local script being edited (you're just invoking an existing script).
|
||||
|
||||
Only use `sync push` when:
|
||||
- The user explicitly asks to deploy, publish, push, or ship.
|
||||
- The preview has already validated the change and the user wants it in the workspace.
|
||||
|
||||
### After writing — offer to test, don't wait passively
|
||||
|
||||
If the user hasn't already told you to run/test/preview the script, offer it as a one-sentence next step (e.g. "Want me to run `wmill script preview` with sample args?"). Do not present a multi-option menu.
|
||||
|
||||
If the user already asked to test/run/try the script in their original request, skip the offer and just execute `wmill script preview <path> -d '<args>'` directly — pick plausible args from the script's declared parameters. The shape varies by language: `main(...)` for code languages, the SQL dialect's own placeholder syntax (`$1` for PostgreSQL, `?` for MySQL/Snowflake, `@P1` for MSSQL, `@name` for BigQuery, etc.), positional `$1`, `$2`, … for Bash, `param(...)` for PowerShell.
|
||||
|
||||
`wmill script preview` does not deploy, but it still executes script code and may cause side effects; run it yourself when the user asked to test/preview (or after confirming that execution is intended). `wmill sync push` and `wmill generate-metadata` modify workspace state or local files — only run these when the user explicitly asks; otherwise tell them which to run.
|
||||
|
||||
For a **visual** open-the-script-in-the-dev-page preview (rather than `script preview`'s run-and-print-result), use the `preview` skill.
|
||||
|
||||
Use `wmill resource-type list --schema` to discover available resource types.
|
||||
|
||||
|
||||
@@ -5,11 +5,36 @@ description: MUST use when writing Bun/TypeScript scripts.
|
||||
|
||||
## CLI Commands
|
||||
|
||||
Place scripts in a folder. After writing, tell the user they can run:
|
||||
- `wmill generate-metadata` - Generate .script.yaml and .lock files
|
||||
- `wmill sync push` - Deploy to Windmill
|
||||
Place scripts in a folder.
|
||||
|
||||
Do NOT run these commands yourself. Instead, inform the user that they should run them.
|
||||
After writing, tell the user which command fits what they want to do:
|
||||
|
||||
- `wmill script preview <script_path>` — **default when iterating on a local script.** Runs the local file without deploying.
|
||||
- `wmill script run <path>` — runs the script **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
|
||||
- `wmill generate-metadata` — generate `.script.yaml` and `.lock` files for the script you modified.
|
||||
- `wmill sync push` — deploy local changes to the workspace. Only suggest/run this when the user explicitly asks to deploy/publish/push — not when they say "run", "try", or "test".
|
||||
|
||||
### Preview vs run — choose by intent, not habit
|
||||
|
||||
If the user says "run the script", "try it", "test it", "does it work" while there are **local edits to the script file**, use `script preview`. Do NOT push the script to then `script run` it — pushing is a deploy, and deploying just to test overwrites the workspace version with untested changes.
|
||||
|
||||
Only use `script run` when:
|
||||
- The user explicitly says "run the deployed version" / "run what's on the server".
|
||||
- There is no local script being edited (you're just invoking an existing script).
|
||||
|
||||
Only use `sync push` when:
|
||||
- The user explicitly asks to deploy, publish, push, or ship.
|
||||
- The preview has already validated the change and the user wants it in the workspace.
|
||||
|
||||
### After writing — offer to test, don't wait passively
|
||||
|
||||
If the user hasn't already told you to run/test/preview the script, offer it as a one-sentence next step (e.g. "Want me to run `wmill script preview` with sample args?"). Do not present a multi-option menu.
|
||||
|
||||
If the user already asked to test/run/try the script in their original request, skip the offer and just execute `wmill script preview <path> -d '<args>'` directly — pick plausible args from the script's declared parameters. The shape varies by language: `main(...)` for code languages, the SQL dialect's own placeholder syntax (`$1` for PostgreSQL, `?` for MySQL/Snowflake, `@P1` for MSSQL, `@name` for BigQuery, etc.), positional `$1`, `$2`, … for Bash, `param(...)` for PowerShell.
|
||||
|
||||
`wmill script preview` does not deploy, but it still executes script code and may cause side effects; run it yourself when the user asked to test/preview (or after confirming that execution is intended). `wmill sync push` and `wmill generate-metadata` modify workspace state or local files — only run these when the user explicitly asks; otherwise tell them which to run.
|
||||
|
||||
For a **visual** open-the-script-in-the-dev-page preview (rather than `script preview`'s run-and-print-result), use the `preview` skill.
|
||||
|
||||
Use `wmill resource-type list --schema` to discover available resource types.
|
||||
|
||||
|
||||
@@ -5,11 +5,36 @@ description: MUST use when writing Bun Native scripts.
|
||||
|
||||
## CLI Commands
|
||||
|
||||
Place scripts in a folder. After writing, tell the user they can run:
|
||||
- `wmill generate-metadata` - Generate .script.yaml and .lock files
|
||||
- `wmill sync push` - Deploy to Windmill
|
||||
Place scripts in a folder.
|
||||
|
||||
Do NOT run these commands yourself. Instead, inform the user that they should run them.
|
||||
After writing, tell the user which command fits what they want to do:
|
||||
|
||||
- `wmill script preview <script_path>` — **default when iterating on a local script.** Runs the local file without deploying.
|
||||
- `wmill script run <path>` — runs the script **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
|
||||
- `wmill generate-metadata` — generate `.script.yaml` and `.lock` files for the script you modified.
|
||||
- `wmill sync push` — deploy local changes to the workspace. Only suggest/run this when the user explicitly asks to deploy/publish/push — not when they say "run", "try", or "test".
|
||||
|
||||
### Preview vs run — choose by intent, not habit
|
||||
|
||||
If the user says "run the script", "try it", "test it", "does it work" while there are **local edits to the script file**, use `script preview`. Do NOT push the script to then `script run` it — pushing is a deploy, and deploying just to test overwrites the workspace version with untested changes.
|
||||
|
||||
Only use `script run` when:
|
||||
- The user explicitly says "run the deployed version" / "run what's on the server".
|
||||
- There is no local script being edited (you're just invoking an existing script).
|
||||
|
||||
Only use `sync push` when:
|
||||
- The user explicitly asks to deploy, publish, push, or ship.
|
||||
- The preview has already validated the change and the user wants it in the workspace.
|
||||
|
||||
### After writing — offer to test, don't wait passively
|
||||
|
||||
If the user hasn't already told you to run/test/preview the script, offer it as a one-sentence next step (e.g. "Want me to run `wmill script preview` with sample args?"). Do not present a multi-option menu.
|
||||
|
||||
If the user already asked to test/run/try the script in their original request, skip the offer and just execute `wmill script preview <path> -d '<args>'` directly — pick plausible args from the script's declared parameters. The shape varies by language: `main(...)` for code languages, the SQL dialect's own placeholder syntax (`$1` for PostgreSQL, `?` for MySQL/Snowflake, `@P1` for MSSQL, `@name` for BigQuery, etc.), positional `$1`, `$2`, … for Bash, `param(...)` for PowerShell.
|
||||
|
||||
`wmill script preview` does not deploy, but it still executes script code and may cause side effects; run it yourself when the user asked to test/preview (or after confirming that execution is intended). `wmill sync push` and `wmill generate-metadata` modify workspace state or local files — only run these when the user explicitly asks; otherwise tell them which to run.
|
||||
|
||||
For a **visual** open-the-script-in-the-dev-page preview (rather than `script preview`'s run-and-print-result), use the `preview` skill.
|
||||
|
||||
Use `wmill resource-type list --schema` to discover available resource types.
|
||||
|
||||
|
||||
@@ -5,11 +5,36 @@ description: MUST use when writing C# scripts.
|
||||
|
||||
## CLI Commands
|
||||
|
||||
Place scripts in a folder. After writing, tell the user they can run:
|
||||
- `wmill generate-metadata` - Generate .script.yaml and .lock files
|
||||
- `wmill sync push` - Deploy to Windmill
|
||||
Place scripts in a folder.
|
||||
|
||||
Do NOT run these commands yourself. Instead, inform the user that they should run them.
|
||||
After writing, tell the user which command fits what they want to do:
|
||||
|
||||
- `wmill script preview <script_path>` — **default when iterating on a local script.** Runs the local file without deploying.
|
||||
- `wmill script run <path>` — runs the script **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
|
||||
- `wmill generate-metadata` — generate `.script.yaml` and `.lock` files for the script you modified.
|
||||
- `wmill sync push` — deploy local changes to the workspace. Only suggest/run this when the user explicitly asks to deploy/publish/push — not when they say "run", "try", or "test".
|
||||
|
||||
### Preview vs run — choose by intent, not habit
|
||||
|
||||
If the user says "run the script", "try it", "test it", "does it work" while there are **local edits to the script file**, use `script preview`. Do NOT push the script to then `script run` it — pushing is a deploy, and deploying just to test overwrites the workspace version with untested changes.
|
||||
|
||||
Only use `script run` when:
|
||||
- The user explicitly says "run the deployed version" / "run what's on the server".
|
||||
- There is no local script being edited (you're just invoking an existing script).
|
||||
|
||||
Only use `sync push` when:
|
||||
- The user explicitly asks to deploy, publish, push, or ship.
|
||||
- The preview has already validated the change and the user wants it in the workspace.
|
||||
|
||||
### After writing — offer to test, don't wait passively
|
||||
|
||||
If the user hasn't already told you to run/test/preview the script, offer it as a one-sentence next step (e.g. "Want me to run `wmill script preview` with sample args?"). Do not present a multi-option menu.
|
||||
|
||||
If the user already asked to test/run/try the script in their original request, skip the offer and just execute `wmill script preview <path> -d '<args>'` directly — pick plausible args from the script's declared parameters. The shape varies by language: `main(...)` for code languages, the SQL dialect's own placeholder syntax (`$1` for PostgreSQL, `?` for MySQL/Snowflake, `@P1` for MSSQL, `@name` for BigQuery, etc.), positional `$1`, `$2`, … for Bash, `param(...)` for PowerShell.
|
||||
|
||||
`wmill script preview` does not deploy, but it still executes script code and may cause side effects; run it yourself when the user asked to test/preview (or after confirming that execution is intended). `wmill sync push` and `wmill generate-metadata` modify workspace state or local files — only run these when the user explicitly asks; otherwise tell them which to run.
|
||||
|
||||
For a **visual** open-the-script-in-the-dev-page preview (rather than `script preview`'s run-and-print-result), use the `preview` skill.
|
||||
|
||||
Use `wmill resource-type list --schema` to discover available resource types.
|
||||
|
||||
|
||||
@@ -5,11 +5,36 @@ description: MUST use when writing Deno/TypeScript scripts.
|
||||
|
||||
## CLI Commands
|
||||
|
||||
Place scripts in a folder. After writing, tell the user they can run:
|
||||
- `wmill generate-metadata` - Generate .script.yaml and .lock files
|
||||
- `wmill sync push` - Deploy to Windmill
|
||||
Place scripts in a folder.
|
||||
|
||||
Do NOT run these commands yourself. Instead, inform the user that they should run them.
|
||||
After writing, tell the user which command fits what they want to do:
|
||||
|
||||
- `wmill script preview <script_path>` — **default when iterating on a local script.** Runs the local file without deploying.
|
||||
- `wmill script run <path>` — runs the script **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
|
||||
- `wmill generate-metadata` — generate `.script.yaml` and `.lock` files for the script you modified.
|
||||
- `wmill sync push` — deploy local changes to the workspace. Only suggest/run this when the user explicitly asks to deploy/publish/push — not when they say "run", "try", or "test".
|
||||
|
||||
### Preview vs run — choose by intent, not habit
|
||||
|
||||
If the user says "run the script", "try it", "test it", "does it work" while there are **local edits to the script file**, use `script preview`. Do NOT push the script to then `script run` it — pushing is a deploy, and deploying just to test overwrites the workspace version with untested changes.
|
||||
|
||||
Only use `script run` when:
|
||||
- The user explicitly says "run the deployed version" / "run what's on the server".
|
||||
- There is no local script being edited (you're just invoking an existing script).
|
||||
|
||||
Only use `sync push` when:
|
||||
- The user explicitly asks to deploy, publish, push, or ship.
|
||||
- The preview has already validated the change and the user wants it in the workspace.
|
||||
|
||||
### After writing — offer to test, don't wait passively
|
||||
|
||||
If the user hasn't already told you to run/test/preview the script, offer it as a one-sentence next step (e.g. "Want me to run `wmill script preview` with sample args?"). Do not present a multi-option menu.
|
||||
|
||||
If the user already asked to test/run/try the script in their original request, skip the offer and just execute `wmill script preview <path> -d '<args>'` directly — pick plausible args from the script's declared parameters. The shape varies by language: `main(...)` for code languages, the SQL dialect's own placeholder syntax (`$1` for PostgreSQL, `?` for MySQL/Snowflake, `@P1` for MSSQL, `@name` for BigQuery, etc.), positional `$1`, `$2`, … for Bash, `param(...)` for PowerShell.
|
||||
|
||||
`wmill script preview` does not deploy, but it still executes script code and may cause side effects; run it yourself when the user asked to test/preview (or after confirming that execution is intended). `wmill sync push` and `wmill generate-metadata` modify workspace state or local files — only run these when the user explicitly asks; otherwise tell them which to run.
|
||||
|
||||
For a **visual** open-the-script-in-the-dev-page preview (rather than `script preview`'s run-and-print-result), use the `preview` skill.
|
||||
|
||||
Use `wmill resource-type list --schema` to discover available resource types.
|
||||
|
||||
|
||||
@@ -5,11 +5,36 @@ description: MUST use when writing DuckDB queries.
|
||||
|
||||
## CLI Commands
|
||||
|
||||
Place scripts in a folder. After writing, tell the user they can run:
|
||||
- `wmill generate-metadata` - Generate .script.yaml and .lock files
|
||||
- `wmill sync push` - Deploy to Windmill
|
||||
Place scripts in a folder.
|
||||
|
||||
Do NOT run these commands yourself. Instead, inform the user that they should run them.
|
||||
After writing, tell the user which command fits what they want to do:
|
||||
|
||||
- `wmill script preview <script_path>` — **default when iterating on a local script.** Runs the local file without deploying.
|
||||
- `wmill script run <path>` — runs the script **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
|
||||
- `wmill generate-metadata` — generate `.script.yaml` and `.lock` files for the script you modified.
|
||||
- `wmill sync push` — deploy local changes to the workspace. Only suggest/run this when the user explicitly asks to deploy/publish/push — not when they say "run", "try", or "test".
|
||||
|
||||
### Preview vs run — choose by intent, not habit
|
||||
|
||||
If the user says "run the script", "try it", "test it", "does it work" while there are **local edits to the script file**, use `script preview`. Do NOT push the script to then `script run` it — pushing is a deploy, and deploying just to test overwrites the workspace version with untested changes.
|
||||
|
||||
Only use `script run` when:
|
||||
- The user explicitly says "run the deployed version" / "run what's on the server".
|
||||
- There is no local script being edited (you're just invoking an existing script).
|
||||
|
||||
Only use `sync push` when:
|
||||
- The user explicitly asks to deploy, publish, push, or ship.
|
||||
- The preview has already validated the change and the user wants it in the workspace.
|
||||
|
||||
### After writing — offer to test, don't wait passively
|
||||
|
||||
If the user hasn't already told you to run/test/preview the script, offer it as a one-sentence next step (e.g. "Want me to run `wmill script preview` with sample args?"). Do not present a multi-option menu.
|
||||
|
||||
If the user already asked to test/run/try the script in their original request, skip the offer and just execute `wmill script preview <path> -d '<args>'` directly — pick plausible args from the script's declared parameters. The shape varies by language: `main(...)` for code languages, the SQL dialect's own placeholder syntax (`$1` for PostgreSQL, `?` for MySQL/Snowflake, `@P1` for MSSQL, `@name` for BigQuery, etc.), positional `$1`, `$2`, … for Bash, `param(...)` for PowerShell.
|
||||
|
||||
`wmill script preview` does not deploy, but it still executes script code and may cause side effects; run it yourself when the user asked to test/preview (or after confirming that execution is intended). `wmill sync push` and `wmill generate-metadata` modify workspace state or local files — only run these when the user explicitly asks; otherwise tell them which to run.
|
||||
|
||||
For a **visual** open-the-script-in-the-dev-page preview (rather than `script preview`'s run-and-print-result), use the `preview` skill.
|
||||
|
||||
Use `wmill resource-type list --schema` to discover available resource types.
|
||||
|
||||
|
||||
@@ -5,11 +5,36 @@ description: MUST use when writing Go scripts.
|
||||
|
||||
## CLI Commands
|
||||
|
||||
Place scripts in a folder. After writing, tell the user they can run:
|
||||
- `wmill generate-metadata` - Generate .script.yaml and .lock files
|
||||
- `wmill sync push` - Deploy to Windmill
|
||||
Place scripts in a folder.
|
||||
|
||||
Do NOT run these commands yourself. Instead, inform the user that they should run them.
|
||||
After writing, tell the user which command fits what they want to do:
|
||||
|
||||
- `wmill script preview <script_path>` — **default when iterating on a local script.** Runs the local file without deploying.
|
||||
- `wmill script run <path>` — runs the script **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
|
||||
- `wmill generate-metadata` — generate `.script.yaml` and `.lock` files for the script you modified.
|
||||
- `wmill sync push` — deploy local changes to the workspace. Only suggest/run this when the user explicitly asks to deploy/publish/push — not when they say "run", "try", or "test".
|
||||
|
||||
### Preview vs run — choose by intent, not habit
|
||||
|
||||
If the user says "run the script", "try it", "test it", "does it work" while there are **local edits to the script file**, use `script preview`. Do NOT push the script to then `script run` it — pushing is a deploy, and deploying just to test overwrites the workspace version with untested changes.
|
||||
|
||||
Only use `script run` when:
|
||||
- The user explicitly says "run the deployed version" / "run what's on the server".
|
||||
- There is no local script being edited (you're just invoking an existing script).
|
||||
|
||||
Only use `sync push` when:
|
||||
- The user explicitly asks to deploy, publish, push, or ship.
|
||||
- The preview has already validated the change and the user wants it in the workspace.
|
||||
|
||||
### After writing — offer to test, don't wait passively
|
||||
|
||||
If the user hasn't already told you to run/test/preview the script, offer it as a one-sentence next step (e.g. "Want me to run `wmill script preview` with sample args?"). Do not present a multi-option menu.
|
||||
|
||||
If the user already asked to test/run/try the script in their original request, skip the offer and just execute `wmill script preview <path> -d '<args>'` directly — pick plausible args from the script's declared parameters. The shape varies by language: `main(...)` for code languages, the SQL dialect's own placeholder syntax (`$1` for PostgreSQL, `?` for MySQL/Snowflake, `@P1` for MSSQL, `@name` for BigQuery, etc.), positional `$1`, `$2`, … for Bash, `param(...)` for PowerShell.
|
||||
|
||||
`wmill script preview` does not deploy, but it still executes script code and may cause side effects; run it yourself when the user asked to test/preview (or after confirming that execution is intended). `wmill sync push` and `wmill generate-metadata` modify workspace state or local files — only run these when the user explicitly asks; otherwise tell them which to run.
|
||||
|
||||
For a **visual** open-the-script-in-the-dev-page preview (rather than `script preview`'s run-and-print-result), use the `preview` skill.
|
||||
|
||||
Use `wmill resource-type list --schema` to discover available resource types.
|
||||
|
||||
|
||||
@@ -5,11 +5,36 @@ description: MUST use when writing GraphQL queries.
|
||||
|
||||
## CLI Commands
|
||||
|
||||
Place scripts in a folder. After writing, tell the user they can run:
|
||||
- `wmill generate-metadata` - Generate .script.yaml and .lock files
|
||||
- `wmill sync push` - Deploy to Windmill
|
||||
Place scripts in a folder.
|
||||
|
||||
Do NOT run these commands yourself. Instead, inform the user that they should run them.
|
||||
After writing, tell the user which command fits what they want to do:
|
||||
|
||||
- `wmill script preview <script_path>` — **default when iterating on a local script.** Runs the local file without deploying.
|
||||
- `wmill script run <path>` — runs the script **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
|
||||
- `wmill generate-metadata` — generate `.script.yaml` and `.lock` files for the script you modified.
|
||||
- `wmill sync push` — deploy local changes to the workspace. Only suggest/run this when the user explicitly asks to deploy/publish/push — not when they say "run", "try", or "test".
|
||||
|
||||
### Preview vs run — choose by intent, not habit
|
||||
|
||||
If the user says "run the script", "try it", "test it", "does it work" while there are **local edits to the script file**, use `script preview`. Do NOT push the script to then `script run` it — pushing is a deploy, and deploying just to test overwrites the workspace version with untested changes.
|
||||
|
||||
Only use `script run` when:
|
||||
- The user explicitly says "run the deployed version" / "run what's on the server".
|
||||
- There is no local script being edited (you're just invoking an existing script).
|
||||
|
||||
Only use `sync push` when:
|
||||
- The user explicitly asks to deploy, publish, push, or ship.
|
||||
- The preview has already validated the change and the user wants it in the workspace.
|
||||
|
||||
### After writing — offer to test, don't wait passively
|
||||
|
||||
If the user hasn't already told you to run/test/preview the script, offer it as a one-sentence next step (e.g. "Want me to run `wmill script preview` with sample args?"). Do not present a multi-option menu.
|
||||
|
||||
If the user already asked to test/run/try the script in their original request, skip the offer and just execute `wmill script preview <path> -d '<args>'` directly — pick plausible args from the script's declared parameters. The shape varies by language: `main(...)` for code languages, the SQL dialect's own placeholder syntax (`$1` for PostgreSQL, `?` for MySQL/Snowflake, `@P1` for MSSQL, `@name` for BigQuery, etc.), positional `$1`, `$2`, … for Bash, `param(...)` for PowerShell.
|
||||
|
||||
`wmill script preview` does not deploy, but it still executes script code and may cause side effects; run it yourself when the user asked to test/preview (or after confirming that execution is intended). `wmill sync push` and `wmill generate-metadata` modify workspace state or local files — only run these when the user explicitly asks; otherwise tell them which to run.
|
||||
|
||||
For a **visual** open-the-script-in-the-dev-page preview (rather than `script preview`'s run-and-print-result), use the `preview` skill.
|
||||
|
||||
Use `wmill resource-type list --schema` to discover available resource types.
|
||||
|
||||
|
||||
@@ -5,11 +5,36 @@ description: MUST use when writing Java scripts.
|
||||
|
||||
## CLI Commands
|
||||
|
||||
Place scripts in a folder. After writing, tell the user they can run:
|
||||
- `wmill generate-metadata` - Generate .script.yaml and .lock files
|
||||
- `wmill sync push` - Deploy to Windmill
|
||||
Place scripts in a folder.
|
||||
|
||||
Do NOT run these commands yourself. Instead, inform the user that they should run them.
|
||||
After writing, tell the user which command fits what they want to do:
|
||||
|
||||
- `wmill script preview <script_path>` — **default when iterating on a local script.** Runs the local file without deploying.
|
||||
- `wmill script run <path>` — runs the script **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
|
||||
- `wmill generate-metadata` — generate `.script.yaml` and `.lock` files for the script you modified.
|
||||
- `wmill sync push` — deploy local changes to the workspace. Only suggest/run this when the user explicitly asks to deploy/publish/push — not when they say "run", "try", or "test".
|
||||
|
||||
### Preview vs run — choose by intent, not habit
|
||||
|
||||
If the user says "run the script", "try it", "test it", "does it work" while there are **local edits to the script file**, use `script preview`. Do NOT push the script to then `script run` it — pushing is a deploy, and deploying just to test overwrites the workspace version with untested changes.
|
||||
|
||||
Only use `script run` when:
|
||||
- The user explicitly says "run the deployed version" / "run what's on the server".
|
||||
- There is no local script being edited (you're just invoking an existing script).
|
||||
|
||||
Only use `sync push` when:
|
||||
- The user explicitly asks to deploy, publish, push, or ship.
|
||||
- The preview has already validated the change and the user wants it in the workspace.
|
||||
|
||||
### After writing — offer to test, don't wait passively
|
||||
|
||||
If the user hasn't already told you to run/test/preview the script, offer it as a one-sentence next step (e.g. "Want me to run `wmill script preview` with sample args?"). Do not present a multi-option menu.
|
||||
|
||||
If the user already asked to test/run/try the script in their original request, skip the offer and just execute `wmill script preview <path> -d '<args>'` directly — pick plausible args from the script's declared parameters. The shape varies by language: `main(...)` for code languages, the SQL dialect's own placeholder syntax (`$1` for PostgreSQL, `?` for MySQL/Snowflake, `@P1` for MSSQL, `@name` for BigQuery, etc.), positional `$1`, `$2`, … for Bash, `param(...)` for PowerShell.
|
||||
|
||||
`wmill script preview` does not deploy, but it still executes script code and may cause side effects; run it yourself when the user asked to test/preview (or after confirming that execution is intended). `wmill sync push` and `wmill generate-metadata` modify workspace state or local files — only run these when the user explicitly asks; otherwise tell them which to run.
|
||||
|
||||
For a **visual** open-the-script-in-the-dev-page preview (rather than `script preview`'s run-and-print-result), use the `preview` skill.
|
||||
|
||||
Use `wmill resource-type list --schema` to discover available resource types.
|
||||
|
||||
|
||||
@@ -5,11 +5,36 @@ description: MUST use when writing MS SQL Server queries.
|
||||
|
||||
## CLI Commands
|
||||
|
||||
Place scripts in a folder. After writing, tell the user they can run:
|
||||
- `wmill generate-metadata` - Generate .script.yaml and .lock files
|
||||
- `wmill sync push` - Deploy to Windmill
|
||||
Place scripts in a folder.
|
||||
|
||||
Do NOT run these commands yourself. Instead, inform the user that they should run them.
|
||||
After writing, tell the user which command fits what they want to do:
|
||||
|
||||
- `wmill script preview <script_path>` — **default when iterating on a local script.** Runs the local file without deploying.
|
||||
- `wmill script run <path>` — runs the script **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
|
||||
- `wmill generate-metadata` — generate `.script.yaml` and `.lock` files for the script you modified.
|
||||
- `wmill sync push` — deploy local changes to the workspace. Only suggest/run this when the user explicitly asks to deploy/publish/push — not when they say "run", "try", or "test".
|
||||
|
||||
### Preview vs run — choose by intent, not habit
|
||||
|
||||
If the user says "run the script", "try it", "test it", "does it work" while there are **local edits to the script file**, use `script preview`. Do NOT push the script to then `script run` it — pushing is a deploy, and deploying just to test overwrites the workspace version with untested changes.
|
||||
|
||||
Only use `script run` when:
|
||||
- The user explicitly says "run the deployed version" / "run what's on the server".
|
||||
- There is no local script being edited (you're just invoking an existing script).
|
||||
|
||||
Only use `sync push` when:
|
||||
- The user explicitly asks to deploy, publish, push, or ship.
|
||||
- The preview has already validated the change and the user wants it in the workspace.
|
||||
|
||||
### After writing — offer to test, don't wait passively
|
||||
|
||||
If the user hasn't already told you to run/test/preview the script, offer it as a one-sentence next step (e.g. "Want me to run `wmill script preview` with sample args?"). Do not present a multi-option menu.
|
||||
|
||||
If the user already asked to test/run/try the script in their original request, skip the offer and just execute `wmill script preview <path> -d '<args>'` directly — pick plausible args from the script's declared parameters. The shape varies by language: `main(...)` for code languages, the SQL dialect's own placeholder syntax (`$1` for PostgreSQL, `?` for MySQL/Snowflake, `@P1` for MSSQL, `@name` for BigQuery, etc.), positional `$1`, `$2`, … for Bash, `param(...)` for PowerShell.
|
||||
|
||||
`wmill script preview` does not deploy, but it still executes script code and may cause side effects; run it yourself when the user asked to test/preview (or after confirming that execution is intended). `wmill sync push` and `wmill generate-metadata` modify workspace state or local files — only run these when the user explicitly asks; otherwise tell them which to run.
|
||||
|
||||
For a **visual** open-the-script-in-the-dev-page preview (rather than `script preview`'s run-and-print-result), use the `preview` skill.
|
||||
|
||||
Use `wmill resource-type list --schema` to discover available resource types.
|
||||
|
||||
|
||||
@@ -5,11 +5,36 @@ description: MUST use when writing MySQL queries.
|
||||
|
||||
## CLI Commands
|
||||
|
||||
Place scripts in a folder. After writing, tell the user they can run:
|
||||
- `wmill generate-metadata` - Generate .script.yaml and .lock files
|
||||
- `wmill sync push` - Deploy to Windmill
|
||||
Place scripts in a folder.
|
||||
|
||||
Do NOT run these commands yourself. Instead, inform the user that they should run them.
|
||||
After writing, tell the user which command fits what they want to do:
|
||||
|
||||
- `wmill script preview <script_path>` — **default when iterating on a local script.** Runs the local file without deploying.
|
||||
- `wmill script run <path>` — runs the script **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
|
||||
- `wmill generate-metadata` — generate `.script.yaml` and `.lock` files for the script you modified.
|
||||
- `wmill sync push` — deploy local changes to the workspace. Only suggest/run this when the user explicitly asks to deploy/publish/push — not when they say "run", "try", or "test".
|
||||
|
||||
### Preview vs run — choose by intent, not habit
|
||||
|
||||
If the user says "run the script", "try it", "test it", "does it work" while there are **local edits to the script file**, use `script preview`. Do NOT push the script to then `script run` it — pushing is a deploy, and deploying just to test overwrites the workspace version with untested changes.
|
||||
|
||||
Only use `script run` when:
|
||||
- The user explicitly says "run the deployed version" / "run what's on the server".
|
||||
- There is no local script being edited (you're just invoking an existing script).
|
||||
|
||||
Only use `sync push` when:
|
||||
- The user explicitly asks to deploy, publish, push, or ship.
|
||||
- The preview has already validated the change and the user wants it in the workspace.
|
||||
|
||||
### After writing — offer to test, don't wait passively
|
||||
|
||||
If the user hasn't already told you to run/test/preview the script, offer it as a one-sentence next step (e.g. "Want me to run `wmill script preview` with sample args?"). Do not present a multi-option menu.
|
||||
|
||||
If the user already asked to test/run/try the script in their original request, skip the offer and just execute `wmill script preview <path> -d '<args>'` directly — pick plausible args from the script's declared parameters. The shape varies by language: `main(...)` for code languages, the SQL dialect's own placeholder syntax (`$1` for PostgreSQL, `?` for MySQL/Snowflake, `@P1` for MSSQL, `@name` for BigQuery, etc.), positional `$1`, `$2`, … for Bash, `param(...)` for PowerShell.
|
||||
|
||||
`wmill script preview` does not deploy, but it still executes script code and may cause side effects; run it yourself when the user asked to test/preview (or after confirming that execution is intended). `wmill sync push` and `wmill generate-metadata` modify workspace state or local files — only run these when the user explicitly asks; otherwise tell them which to run.
|
||||
|
||||
For a **visual** open-the-script-in-the-dev-page preview (rather than `script preview`'s run-and-print-result), use the `preview` skill.
|
||||
|
||||
Use `wmill resource-type list --schema` to discover available resource types.
|
||||
|
||||
|
||||
@@ -5,11 +5,36 @@ description: MUST use when writing Native TypeScript scripts.
|
||||
|
||||
## CLI Commands
|
||||
|
||||
Place scripts in a folder. After writing, tell the user they can run:
|
||||
- `wmill generate-metadata` - Generate .script.yaml and .lock files
|
||||
- `wmill sync push` - Deploy to Windmill
|
||||
Place scripts in a folder.
|
||||
|
||||
Do NOT run these commands yourself. Instead, inform the user that they should run them.
|
||||
After writing, tell the user which command fits what they want to do:
|
||||
|
||||
- `wmill script preview <script_path>` — **default when iterating on a local script.** Runs the local file without deploying.
|
||||
- `wmill script run <path>` — runs the script **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
|
||||
- `wmill generate-metadata` — generate `.script.yaml` and `.lock` files for the script you modified.
|
||||
- `wmill sync push` — deploy local changes to the workspace. Only suggest/run this when the user explicitly asks to deploy/publish/push — not when they say "run", "try", or "test".
|
||||
|
||||
### Preview vs run — choose by intent, not habit
|
||||
|
||||
If the user says "run the script", "try it", "test it", "does it work" while there are **local edits to the script file**, use `script preview`. Do NOT push the script to then `script run` it — pushing is a deploy, and deploying just to test overwrites the workspace version with untested changes.
|
||||
|
||||
Only use `script run` when:
|
||||
- The user explicitly says "run the deployed version" / "run what's on the server".
|
||||
- There is no local script being edited (you're just invoking an existing script).
|
||||
|
||||
Only use `sync push` when:
|
||||
- The user explicitly asks to deploy, publish, push, or ship.
|
||||
- The preview has already validated the change and the user wants it in the workspace.
|
||||
|
||||
### After writing — offer to test, don't wait passively
|
||||
|
||||
If the user hasn't already told you to run/test/preview the script, offer it as a one-sentence next step (e.g. "Want me to run `wmill script preview` with sample args?"). Do not present a multi-option menu.
|
||||
|
||||
If the user already asked to test/run/try the script in their original request, skip the offer and just execute `wmill script preview <path> -d '<args>'` directly — pick plausible args from the script's declared parameters. The shape varies by language: `main(...)` for code languages, the SQL dialect's own placeholder syntax (`$1` for PostgreSQL, `?` for MySQL/Snowflake, `@P1` for MSSQL, `@name` for BigQuery, etc.), positional `$1`, `$2`, … for Bash, `param(...)` for PowerShell.
|
||||
|
||||
`wmill script preview` does not deploy, but it still executes script code and may cause side effects; run it yourself when the user asked to test/preview (or after confirming that execution is intended). `wmill sync push` and `wmill generate-metadata` modify workspace state or local files — only run these when the user explicitly asks; otherwise tell them which to run.
|
||||
|
||||
For a **visual** open-the-script-in-the-dev-page preview (rather than `script preview`'s run-and-print-result), use the `preview` skill.
|
||||
|
||||
Use `wmill resource-type list --schema` to discover available resource types.
|
||||
|
||||
|
||||
@@ -5,11 +5,36 @@ description: MUST use when writing PHP scripts.
|
||||
|
||||
## CLI Commands
|
||||
|
||||
Place scripts in a folder. After writing, tell the user they can run:
|
||||
- `wmill generate-metadata` - Generate .script.yaml and .lock files
|
||||
- `wmill sync push` - Deploy to Windmill
|
||||
Place scripts in a folder.
|
||||
|
||||
Do NOT run these commands yourself. Instead, inform the user that they should run them.
|
||||
After writing, tell the user which command fits what they want to do:
|
||||
|
||||
- `wmill script preview <script_path>` — **default when iterating on a local script.** Runs the local file without deploying.
|
||||
- `wmill script run <path>` — runs the script **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
|
||||
- `wmill generate-metadata` — generate `.script.yaml` and `.lock` files for the script you modified.
|
||||
- `wmill sync push` — deploy local changes to the workspace. Only suggest/run this when the user explicitly asks to deploy/publish/push — not when they say "run", "try", or "test".
|
||||
|
||||
### Preview vs run — choose by intent, not habit
|
||||
|
||||
If the user says "run the script", "try it", "test it", "does it work" while there are **local edits to the script file**, use `script preview`. Do NOT push the script to then `script run` it — pushing is a deploy, and deploying just to test overwrites the workspace version with untested changes.
|
||||
|
||||
Only use `script run` when:
|
||||
- The user explicitly says "run the deployed version" / "run what's on the server".
|
||||
- There is no local script being edited (you're just invoking an existing script).
|
||||
|
||||
Only use `sync push` when:
|
||||
- The user explicitly asks to deploy, publish, push, or ship.
|
||||
- The preview has already validated the change and the user wants it in the workspace.
|
||||
|
||||
### After writing — offer to test, don't wait passively
|
||||
|
||||
If the user hasn't already told you to run/test/preview the script, offer it as a one-sentence next step (e.g. "Want me to run `wmill script preview` with sample args?"). Do not present a multi-option menu.
|
||||
|
||||
If the user already asked to test/run/try the script in their original request, skip the offer and just execute `wmill script preview <path> -d '<args>'` directly — pick plausible args from the script's declared parameters. The shape varies by language: `main(...)` for code languages, the SQL dialect's own placeholder syntax (`$1` for PostgreSQL, `?` for MySQL/Snowflake, `@P1` for MSSQL, `@name` for BigQuery, etc.), positional `$1`, `$2`, … for Bash, `param(...)` for PowerShell.
|
||||
|
||||
`wmill script preview` does not deploy, but it still executes script code and may cause side effects; run it yourself when the user asked to test/preview (or after confirming that execution is intended). `wmill sync push` and `wmill generate-metadata` modify workspace state or local files — only run these when the user explicitly asks; otherwise tell them which to run.
|
||||
|
||||
For a **visual** open-the-script-in-the-dev-page preview (rather than `script preview`'s run-and-print-result), use the `preview` skill.
|
||||
|
||||
Use `wmill resource-type list --schema` to discover available resource types.
|
||||
|
||||
|
||||
@@ -5,11 +5,36 @@ description: MUST use when writing PostgreSQL queries.
|
||||
|
||||
## CLI Commands
|
||||
|
||||
Place scripts in a folder. After writing, tell the user they can run:
|
||||
- `wmill generate-metadata` - Generate .script.yaml and .lock files
|
||||
- `wmill sync push` - Deploy to Windmill
|
||||
Place scripts in a folder.
|
||||
|
||||
Do NOT run these commands yourself. Instead, inform the user that they should run them.
|
||||
After writing, tell the user which command fits what they want to do:
|
||||
|
||||
- `wmill script preview <script_path>` — **default when iterating on a local script.** Runs the local file without deploying.
|
||||
- `wmill script run <path>` — runs the script **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
|
||||
- `wmill generate-metadata` — generate `.script.yaml` and `.lock` files for the script you modified.
|
||||
- `wmill sync push` — deploy local changes to the workspace. Only suggest/run this when the user explicitly asks to deploy/publish/push — not when they say "run", "try", or "test".
|
||||
|
||||
### Preview vs run — choose by intent, not habit
|
||||
|
||||
If the user says "run the script", "try it", "test it", "does it work" while there are **local edits to the script file**, use `script preview`. Do NOT push the script to then `script run` it — pushing is a deploy, and deploying just to test overwrites the workspace version with untested changes.
|
||||
|
||||
Only use `script run` when:
|
||||
- The user explicitly says "run the deployed version" / "run what's on the server".
|
||||
- There is no local script being edited (you're just invoking an existing script).
|
||||
|
||||
Only use `sync push` when:
|
||||
- The user explicitly asks to deploy, publish, push, or ship.
|
||||
- The preview has already validated the change and the user wants it in the workspace.
|
||||
|
||||
### After writing — offer to test, don't wait passively
|
||||
|
||||
If the user hasn't already told you to run/test/preview the script, offer it as a one-sentence next step (e.g. "Want me to run `wmill script preview` with sample args?"). Do not present a multi-option menu.
|
||||
|
||||
If the user already asked to test/run/try the script in their original request, skip the offer and just execute `wmill script preview <path> -d '<args>'` directly — pick plausible args from the script's declared parameters. The shape varies by language: `main(...)` for code languages, the SQL dialect's own placeholder syntax (`$1` for PostgreSQL, `?` for MySQL/Snowflake, `@P1` for MSSQL, `@name` for BigQuery, etc.), positional `$1`, `$2`, … for Bash, `param(...)` for PowerShell.
|
||||
|
||||
`wmill script preview` does not deploy, but it still executes script code and may cause side effects; run it yourself when the user asked to test/preview (or after confirming that execution is intended). `wmill sync push` and `wmill generate-metadata` modify workspace state or local files — only run these when the user explicitly asks; otherwise tell them which to run.
|
||||
|
||||
For a **visual** open-the-script-in-the-dev-page preview (rather than `script preview`'s run-and-print-result), use the `preview` skill.
|
||||
|
||||
Use `wmill resource-type list --schema` to discover available resource types.
|
||||
|
||||
|
||||
@@ -5,11 +5,36 @@ description: MUST use when writing PowerShell scripts.
|
||||
|
||||
## CLI Commands
|
||||
|
||||
Place scripts in a folder. After writing, tell the user they can run:
|
||||
- `wmill generate-metadata` - Generate .script.yaml and .lock files
|
||||
- `wmill sync push` - Deploy to Windmill
|
||||
Place scripts in a folder.
|
||||
|
||||
Do NOT run these commands yourself. Instead, inform the user that they should run them.
|
||||
After writing, tell the user which command fits what they want to do:
|
||||
|
||||
- `wmill script preview <script_path>` — **default when iterating on a local script.** Runs the local file without deploying.
|
||||
- `wmill script run <path>` — runs the script **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
|
||||
- `wmill generate-metadata` — generate `.script.yaml` and `.lock` files for the script you modified.
|
||||
- `wmill sync push` — deploy local changes to the workspace. Only suggest/run this when the user explicitly asks to deploy/publish/push — not when they say "run", "try", or "test".
|
||||
|
||||
### Preview vs run — choose by intent, not habit
|
||||
|
||||
If the user says "run the script", "try it", "test it", "does it work" while there are **local edits to the script file**, use `script preview`. Do NOT push the script to then `script run` it — pushing is a deploy, and deploying just to test overwrites the workspace version with untested changes.
|
||||
|
||||
Only use `script run` when:
|
||||
- The user explicitly says "run the deployed version" / "run what's on the server".
|
||||
- There is no local script being edited (you're just invoking an existing script).
|
||||
|
||||
Only use `sync push` when:
|
||||
- The user explicitly asks to deploy, publish, push, or ship.
|
||||
- The preview has already validated the change and the user wants it in the workspace.
|
||||
|
||||
### After writing — offer to test, don't wait passively
|
||||
|
||||
If the user hasn't already told you to run/test/preview the script, offer it as a one-sentence next step (e.g. "Want me to run `wmill script preview` with sample args?"). Do not present a multi-option menu.
|
||||
|
||||
If the user already asked to test/run/try the script in their original request, skip the offer and just execute `wmill script preview <path> -d '<args>'` directly — pick plausible args from the script's declared parameters. The shape varies by language: `main(...)` for code languages, the SQL dialect's own placeholder syntax (`$1` for PostgreSQL, `?` for MySQL/Snowflake, `@P1` for MSSQL, `@name` for BigQuery, etc.), positional `$1`, `$2`, … for Bash, `param(...)` for PowerShell.
|
||||
|
||||
`wmill script preview` does not deploy, but it still executes script code and may cause side effects; run it yourself when the user asked to test/preview (or after confirming that execution is intended). `wmill sync push` and `wmill generate-metadata` modify workspace state or local files — only run these when the user explicitly asks; otherwise tell them which to run.
|
||||
|
||||
For a **visual** open-the-script-in-the-dev-page preview (rather than `script preview`'s run-and-print-result), use the `preview` skill.
|
||||
|
||||
Use `wmill resource-type list --schema` to discover available resource types.
|
||||
|
||||
|
||||
@@ -5,11 +5,36 @@ description: MUST use when writing Python scripts.
|
||||
|
||||
## CLI Commands
|
||||
|
||||
Place scripts in a folder. After writing, tell the user they can run:
|
||||
- `wmill generate-metadata` - Generate .script.yaml and .lock files
|
||||
- `wmill sync push` - Deploy to Windmill
|
||||
Place scripts in a folder.
|
||||
|
||||
Do NOT run these commands yourself. Instead, inform the user that they should run them.
|
||||
After writing, tell the user which command fits what they want to do:
|
||||
|
||||
- `wmill script preview <script_path>` — **default when iterating on a local script.** Runs the local file without deploying.
|
||||
- `wmill script run <path>` — runs the script **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
|
||||
- `wmill generate-metadata` — generate `.script.yaml` and `.lock` files for the script you modified.
|
||||
- `wmill sync push` — deploy local changes to the workspace. Only suggest/run this when the user explicitly asks to deploy/publish/push — not when they say "run", "try", or "test".
|
||||
|
||||
### Preview vs run — choose by intent, not habit
|
||||
|
||||
If the user says "run the script", "try it", "test it", "does it work" while there are **local edits to the script file**, use `script preview`. Do NOT push the script to then `script run` it — pushing is a deploy, and deploying just to test overwrites the workspace version with untested changes.
|
||||
|
||||
Only use `script run` when:
|
||||
- The user explicitly says "run the deployed version" / "run what's on the server".
|
||||
- There is no local script being edited (you're just invoking an existing script).
|
||||
|
||||
Only use `sync push` when:
|
||||
- The user explicitly asks to deploy, publish, push, or ship.
|
||||
- The preview has already validated the change and the user wants it in the workspace.
|
||||
|
||||
### After writing — offer to test, don't wait passively
|
||||
|
||||
If the user hasn't already told you to run/test/preview the script, offer it as a one-sentence next step (e.g. "Want me to run `wmill script preview` with sample args?"). Do not present a multi-option menu.
|
||||
|
||||
If the user already asked to test/run/try the script in their original request, skip the offer and just execute `wmill script preview <path> -d '<args>'` directly — pick plausible args from the script's declared parameters. The shape varies by language: `main(...)` for code languages, the SQL dialect's own placeholder syntax (`$1` for PostgreSQL, `?` for MySQL/Snowflake, `@P1` for MSSQL, `@name` for BigQuery, etc.), positional `$1`, `$2`, … for Bash, `param(...)` for PowerShell.
|
||||
|
||||
`wmill script preview` does not deploy, but it still executes script code and may cause side effects; run it yourself when the user asked to test/preview (or after confirming that execution is intended). `wmill sync push` and `wmill generate-metadata` modify workspace state or local files — only run these when the user explicitly asks; otherwise tell them which to run.
|
||||
|
||||
For a **visual** open-the-script-in-the-dev-page preview (rather than `script preview`'s run-and-print-result), use the `preview` skill.
|
||||
|
||||
Use `wmill resource-type list --schema` to discover available resource types.
|
||||
|
||||
|
||||
@@ -5,11 +5,36 @@ description: MUST use when writing R scripts.
|
||||
|
||||
## CLI Commands
|
||||
|
||||
Place scripts in a folder. After writing, tell the user they can run:
|
||||
- `wmill generate-metadata` - Generate .script.yaml and .lock files
|
||||
- `wmill sync push` - Deploy to Windmill
|
||||
Place scripts in a folder.
|
||||
|
||||
Do NOT run these commands yourself. Instead, inform the user that they should run them.
|
||||
After writing, tell the user which command fits what they want to do:
|
||||
|
||||
- `wmill script preview <script_path>` — **default when iterating on a local script.** Runs the local file without deploying.
|
||||
- `wmill script run <path>` — runs the script **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
|
||||
- `wmill generate-metadata` — generate `.script.yaml` and `.lock` files for the script you modified.
|
||||
- `wmill sync push` — deploy local changes to the workspace. Only suggest/run this when the user explicitly asks to deploy/publish/push — not when they say "run", "try", or "test".
|
||||
|
||||
### Preview vs run — choose by intent, not habit
|
||||
|
||||
If the user says "run the script", "try it", "test it", "does it work" while there are **local edits to the script file**, use `script preview`. Do NOT push the script to then `script run` it — pushing is a deploy, and deploying just to test overwrites the workspace version with untested changes.
|
||||
|
||||
Only use `script run` when:
|
||||
- The user explicitly says "run the deployed version" / "run what's on the server".
|
||||
- There is no local script being edited (you're just invoking an existing script).
|
||||
|
||||
Only use `sync push` when:
|
||||
- The user explicitly asks to deploy, publish, push, or ship.
|
||||
- The preview has already validated the change and the user wants it in the workspace.
|
||||
|
||||
### After writing — offer to test, don't wait passively
|
||||
|
||||
If the user hasn't already told you to run/test/preview the script, offer it as a one-sentence next step (e.g. "Want me to run `wmill script preview` with sample args?"). Do not present a multi-option menu.
|
||||
|
||||
If the user already asked to test/run/try the script in their original request, skip the offer and just execute `wmill script preview <path> -d '<args>'` directly — pick plausible args from the script's declared parameters. The shape varies by language: `main(...)` for code languages, the SQL dialect's own placeholder syntax (`$1` for PostgreSQL, `?` for MySQL/Snowflake, `@P1` for MSSQL, `@name` for BigQuery, etc.), positional `$1`, `$2`, … for Bash, `param(...)` for PowerShell.
|
||||
|
||||
`wmill script preview` does not deploy, but it still executes script code and may cause side effects; run it yourself when the user asked to test/preview (or after confirming that execution is intended). `wmill sync push` and `wmill generate-metadata` modify workspace state or local files — only run these when the user explicitly asks; otherwise tell them which to run.
|
||||
|
||||
For a **visual** open-the-script-in-the-dev-page preview (rather than `script preview`'s run-and-print-result), use the `preview` skill.
|
||||
|
||||
Use `wmill resource-type list --schema` to discover available resource types.
|
||||
|
||||
|
||||
@@ -5,11 +5,36 @@ description: MUST use when writing Rust scripts.
|
||||
|
||||
## CLI Commands
|
||||
|
||||
Place scripts in a folder. After writing, tell the user they can run:
|
||||
- `wmill generate-metadata` - Generate .script.yaml and .lock files
|
||||
- `wmill sync push` - Deploy to Windmill
|
||||
Place scripts in a folder.
|
||||
|
||||
Do NOT run these commands yourself. Instead, inform the user that they should run them.
|
||||
After writing, tell the user which command fits what they want to do:
|
||||
|
||||
- `wmill script preview <script_path>` — **default when iterating on a local script.** Runs the local file without deploying.
|
||||
- `wmill script run <path>` — runs the script **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
|
||||
- `wmill generate-metadata` — generate `.script.yaml` and `.lock` files for the script you modified.
|
||||
- `wmill sync push` — deploy local changes to the workspace. Only suggest/run this when the user explicitly asks to deploy/publish/push — not when they say "run", "try", or "test".
|
||||
|
||||
### Preview vs run — choose by intent, not habit
|
||||
|
||||
If the user says "run the script", "try it", "test it", "does it work" while there are **local edits to the script file**, use `script preview`. Do NOT push the script to then `script run` it — pushing is a deploy, and deploying just to test overwrites the workspace version with untested changes.
|
||||
|
||||
Only use `script run` when:
|
||||
- The user explicitly says "run the deployed version" / "run what's on the server".
|
||||
- There is no local script being edited (you're just invoking an existing script).
|
||||
|
||||
Only use `sync push` when:
|
||||
- The user explicitly asks to deploy, publish, push, or ship.
|
||||
- The preview has already validated the change and the user wants it in the workspace.
|
||||
|
||||
### After writing — offer to test, don't wait passively
|
||||
|
||||
If the user hasn't already told you to run/test/preview the script, offer it as a one-sentence next step (e.g. "Want me to run `wmill script preview` with sample args?"). Do not present a multi-option menu.
|
||||
|
||||
If the user already asked to test/run/try the script in their original request, skip the offer and just execute `wmill script preview <path> -d '<args>'` directly — pick plausible args from the script's declared parameters. The shape varies by language: `main(...)` for code languages, the SQL dialect's own placeholder syntax (`$1` for PostgreSQL, `?` for MySQL/Snowflake, `@P1` for MSSQL, `@name` for BigQuery, etc.), positional `$1`, `$2`, … for Bash, `param(...)` for PowerShell.
|
||||
|
||||
`wmill script preview` does not deploy, but it still executes script code and may cause side effects; run it yourself when the user asked to test/preview (or after confirming that execution is intended). `wmill sync push` and `wmill generate-metadata` modify workspace state or local files — only run these when the user explicitly asks; otherwise tell them which to run.
|
||||
|
||||
For a **visual** open-the-script-in-the-dev-page preview (rather than `script preview`'s run-and-print-result), use the `preview` skill.
|
||||
|
||||
Use `wmill resource-type list --schema` to discover available resource types.
|
||||
|
||||
|
||||
@@ -5,11 +5,36 @@ description: MUST use when writing Snowflake queries.
|
||||
|
||||
## CLI Commands
|
||||
|
||||
Place scripts in a folder. After writing, tell the user they can run:
|
||||
- `wmill generate-metadata` - Generate .script.yaml and .lock files
|
||||
- `wmill sync push` - Deploy to Windmill
|
||||
Place scripts in a folder.
|
||||
|
||||
Do NOT run these commands yourself. Instead, inform the user that they should run them.
|
||||
After writing, tell the user which command fits what they want to do:
|
||||
|
||||
- `wmill script preview <script_path>` — **default when iterating on a local script.** Runs the local file without deploying.
|
||||
- `wmill script run <path>` — runs the script **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
|
||||
- `wmill generate-metadata` — generate `.script.yaml` and `.lock` files for the script you modified.
|
||||
- `wmill sync push` — deploy local changes to the workspace. Only suggest/run this when the user explicitly asks to deploy/publish/push — not when they say "run", "try", or "test".
|
||||
|
||||
### Preview vs run — choose by intent, not habit
|
||||
|
||||
If the user says "run the script", "try it", "test it", "does it work" while there are **local edits to the script file**, use `script preview`. Do NOT push the script to then `script run` it — pushing is a deploy, and deploying just to test overwrites the workspace version with untested changes.
|
||||
|
||||
Only use `script run` when:
|
||||
- The user explicitly says "run the deployed version" / "run what's on the server".
|
||||
- There is no local script being edited (you're just invoking an existing script).
|
||||
|
||||
Only use `sync push` when:
|
||||
- The user explicitly asks to deploy, publish, push, or ship.
|
||||
- The preview has already validated the change and the user wants it in the workspace.
|
||||
|
||||
### After writing — offer to test, don't wait passively
|
||||
|
||||
If the user hasn't already told you to run/test/preview the script, offer it as a one-sentence next step (e.g. "Want me to run `wmill script preview` with sample args?"). Do not present a multi-option menu.
|
||||
|
||||
If the user already asked to test/run/try the script in their original request, skip the offer and just execute `wmill script preview <path> -d '<args>'` directly — pick plausible args from the script's declared parameters. The shape varies by language: `main(...)` for code languages, the SQL dialect's own placeholder syntax (`$1` for PostgreSQL, `?` for MySQL/Snowflake, `@P1` for MSSQL, `@name` for BigQuery, etc.), positional `$1`, `$2`, … for Bash, `param(...)` for PowerShell.
|
||||
|
||||
`wmill script preview` does not deploy, but it still executes script code and may cause side effects; run it yourself when the user asked to test/preview (or after confirming that execution is intended). `wmill sync push` and `wmill generate-metadata` modify workspace state or local files — only run these when the user explicitly asks; otherwise tell them which to run.
|
||||
|
||||
For a **visual** open-the-script-in-the-dev-page preview (rather than `script preview`'s run-and-print-result), use the `preview` skill.
|
||||
|
||||
Use `wmill resource-type list --schema` to discover available resource types.
|
||||
|
||||
|
||||
@@ -1,14 +1,76 @@
|
||||
# Windmill Flow Building Guide
|
||||
|
||||
## CLI Commands
|
||||
## Creating a Flow
|
||||
|
||||
**You — the AI agent — scaffold the flow yourself by running `wmill flow new <path>` with the right flags. Do NOT hand-create the folder + `flow.yaml`, and do NOT tell the user to "run `wmill flow new` and follow the prompts".**
|
||||
|
||||
`wmill flow new` creates the folder with the correct suffix (`__flow` or `.flow` depending on the workspace's `nonDottedPaths` setting), writes a minimal `flow.yaml` shell, and prints Claude-specific next-step hints. Scaffolding by hand skips all of that and often picks the wrong suffix.
|
||||
|
||||
### Step 1 — Gather path + summary by asking the user
|
||||
|
||||
You need two things:
|
||||
|
||||
1. **path** — the windmill path, e.g. `f/folder/my_flow` or `u/username/my_flow`.
|
||||
2. **summary** — a short description of the flow.
|
||||
|
||||
If the user's request didn't supply both, ask for both in a single round-trip. Use whichever interactive question facility your runtime provides — a structured multi-choice tool if available, otherwise plain chat — and provide one or two example values for each (with an "Other" / free-form fallback). Do not guess paths or summaries.
|
||||
|
||||
### Step 2 — Run the command yourself
|
||||
|
||||
```bash
|
||||
wmill flow new f/folder/my_flow --summary "Short description"
|
||||
```
|
||||
|
||||
Add `--description "..."` when the user provided a longer explanation worth preserving separately from the summary.
|
||||
|
||||
### Step 3 — Fill in `flow.yaml`
|
||||
|
||||
Open the generated `flow.yaml` (under the folder the command just created) and replace the empty `value.modules` + `schema` with the real flow definition.
|
||||
|
||||
Create a folder ending with `__flow` and add a `flow.yaml` file with the flow definition.
|
||||
For rawscript modules, use `!inline path/to/script.ts` for the content key. Inline script files should NOT include `.inline_script.` in their names (e.g. use `a.ts`, not `a.inline_script.ts`).
|
||||
After writing, tell the user they can run:
|
||||
- `wmill generate-metadata` - Generate lock files for the flow you modified
|
||||
- `wmill sync push` - Deploy to Windmill
|
||||
|
||||
Do NOT run these commands yourself. Instead, inform the user that they should run them.
|
||||
Once the flow has real content, **offer** to open the visual preview as a one-sentence next step (e.g. "Want me to open the visual preview?"). Don't auto-open — opening the dev page has side effects (browser window, possibly a `launch.json` entry) and the user should consent.
|
||||
|
||||
### Anti-patterns to avoid
|
||||
|
||||
- ❌ Hand-creating the `__flow` folder + `flow.yaml` instead of running `wmill flow new`. You'll miss the suffix-setting resolution, the default shape, and the Claude hints.
|
||||
- ❌ Telling the user to "run `wmill flow new <path>`" — you can and should run it yourself.
|
||||
- ❌ Inventing a path/summary instead of asking the user.
|
||||
|
||||
## CLI Commands — running, previewing, deploying
|
||||
|
||||
After writing, tell the user which command fits what they want to do:
|
||||
|
||||
- `wmill flow preview <flow_path>` — **default when iterating on a local flow.** Runs the local `flow.yaml` against local inline scripts without deploying. Add `--remote` to use deployed workspace scripts for PathScript steps instead of local files.
|
||||
- `wmill flow run <path>` — runs the flow **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
|
||||
- `wmill generate-metadata` — regenerate stale `.lock` and `.script.yaml` files. By default it scans **scripts, flows, and apps** across the workspace; pass `--skip-flows --skip-apps` (or run from a subdirectory) to limit the scope when you only care about the flow you edited.
|
||||
- `wmill sync push` — deploy local changes to the workspace. Only suggest/run this when the user explicitly asks to deploy/publish/push — not when they say "run", "try", or "test".
|
||||
|
||||
### Preview vs run — choose by intent, not habit
|
||||
|
||||
If the user says "run the flow", "try it", "test it", "does it work" while there are **local edits to a `flow.yaml`**, use `flow preview`. Do NOT push the flow to then `flow run` it — pushing is a deploy, and deploying just to test overwrites the workspace version with untested changes.
|
||||
|
||||
Only use `flow run` when:
|
||||
- The user explicitly says "run the deployed version" / "run what's on the server".
|
||||
- There is no local `flow.yaml` being edited (you're just invoking an existing flow).
|
||||
|
||||
Only use `sync push` when:
|
||||
- The user explicitly asks to deploy, publish, push, or ship.
|
||||
- The preview has already validated the change and the user wants it in the workspace.
|
||||
|
||||
### After writing — offer to run, don't wait passively
|
||||
|
||||
This is about **programmatic execution** (`wmill flow preview -d '<args>'`), which actually runs the flow and has side effects. Visual preview (the `preview` skill) is offered separately — see "Visual preview" below.
|
||||
|
||||
If the user hasn't already told you to run/test the flow, offer it as a one-sentence next step (e.g. "Want me to run `wmill flow preview` with sample args?"). Do not present a multi-option menu.
|
||||
|
||||
If the user already asked to test/run/try the flow in their original request, skip the offer and just execute `wmill flow preview <path> -d '<args>'` directly — pick plausible args from the flow's input schema.
|
||||
|
||||
`wmill flow preview` is safe to run yourself (it does not deploy). `wmill sync push` and `wmill generate-metadata` modify workspace state or local files — only run these when the user explicitly asks; otherwise tell them which to run.
|
||||
|
||||
### Visual preview
|
||||
|
||||
To open the flow visually in the dev page (graph + live reload), use the `preview` skill. Always **offer** it as a one-sentence next step (e.g. "Want me to open the visual preview?") rather than opening it automatically — opening the dev page has side effects (browser window, possibly a `launch.json` entry under MCP-preview branches) the user should consent to. If the user already asked to see/preview/visualize the flow in their original request, skip the offer and just invoke the skill.
|
||||
|
||||
## OpenFlow Schema
|
||||
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
# Windmill Preview Workflow
|
||||
|
||||
Use this skill any time the user wants to **see**, **open**, **navigate to**, **visualize**, or **preview** a flow, script, or app — and any time you've just finished writing one and want to offer visual verification.
|
||||
|
||||
The Windmill dev page renders the flow graph / script editor, lets the user step through steps, and live-reloads on every save. It runs locally via `wmill dev` and is reached on a localhost port.
|
||||
|
||||
## Two independent decisions
|
||||
|
||||
### 1. Mode: proxy or direct?
|
||||
|
||||
`wmill dev` runs in two modes; pick by asking what kind of URL whatever will display the preview needs.
|
||||
|
||||
- **Proxy** (`--proxy-port <port>`) — exposes the dev page on `http://localhost:<port>/`. Use it when the embedder you'll hand the URL to **only accepts localhost URLs** (most in-IDE / in-chat preview embedders do, because they sandbox cross-origin loads).
|
||||
- **Direct** (default) — the user's browser loads the dev page from the remote workspace's HTTPS URL; the local `wmill dev` only runs the WebSocket back-channel for live reload. Use it when the URL will be opened in a regular browser tab.
|
||||
|
||||
Default to **direct** unless you have a specific embedder that needs localhost.
|
||||
|
||||
### 2. Who starts the server?
|
||||
|
||||
- **You start it** in the background. Spawn `wmill dev …` (or `wmill app dev …`) yourself, capture the URL it prints, do whatever's next (open a tab, hand the URL to an embedder).
|
||||
- **The runtime starts it from `.claude/launch.json`.** Some runtimes (currently the Claude Desktop / Claude Code MCP preview integration — tools prefixed with `mcp__Claude_Preview__`) can read a `launch.json` configuration and launch the dev server on demand when you invoke their preview tool. **Only take this path if you actually have such a tool** — otherwise nothing reads the file and `wmill dev` never starts.
|
||||
|
||||
The two decisions compose. The common cases:
|
||||
|
||||
| Embedder | Needs localhost? | launch.json runtime? | What to do |
|
||||
|---|---|---|---|
|
||||
| Regular browser tab | No | n/a | Direct mode, you start it, give URL to user |
|
||||
| IDE / chat preview pane that takes any URL | No | No | Direct mode, you start it, point the embedder at the printed URL |
|
||||
| IDE / chat preview pane that only accepts localhost | Yes | No | Proxy mode, you start it, point the embedder at `http://localhost:<port>/` |
|
||||
| Claude Desktop / Code MCP preview | Yes | Yes | Proxy mode, write a `launch.json` entry, invoke the MCP tool |
|
||||
|
||||
Never start the proxy "just in case" — it adds the localhost hop for no benefit when no embedder needs it.
|
||||
|
||||
## Starting the server yourself
|
||||
|
||||
Use this when no `launch.json`-aware runtime is available, regardless of mode.
|
||||
|
||||
For flows / scripts:
|
||||
```bash
|
||||
# Direct mode — gives you the remote dev-page URL
|
||||
wmill dev --path <wmill_path> --no-open
|
||||
|
||||
# Proxy mode — gives you a localhost URL that 302s to the remote dev page
|
||||
wmill dev --proxy-port 4000 --path <wmill_path> --no-open
|
||||
```
|
||||
|
||||
For apps:
|
||||
```bash
|
||||
cd <app_path>__raw_app && wmill app dev --no-open --port 4000
|
||||
```
|
||||
|
||||
Each command prints the URL on stdout. Line shapes differ:
|
||||
|
||||
- `wmill dev --no-open` (direct) prints `Go to <url>` with the full remote URL (workspace, token, path baked in).
|
||||
- `wmill dev --proxy-port` prints `Dev proxy listening on http://localhost:<port>` — the URL to hand to an embedder is `http://localhost:<port>/`.
|
||||
- `wmill app dev --no-open` prints `🚀 Dev server running at <url>` — the local app server.
|
||||
|
||||
Capture the URL with a loose match (the first `https?://…` token after startup) and either hand it to your embedder or relay it to the user: *"Preview is running — open `<url>` in your browser."* Don't construct the URL yourself; you don't have the workspace ID or auth token.
|
||||
|
||||
These commands are long-running — start them in the background, don't block waiting.
|
||||
|
||||
## Letting `launch.json` start the server (Claude Desktop / Code MCP only)
|
||||
|
||||
Take this path when **and only when** an `mcp__Claude_Preview__*` MCP tool is exposed in your tool list. Skip it otherwise — without an MCP tool reading the file, `wmill dev` never starts.
|
||||
|
||||
**Each flow / script / app gets its own named entry** in the user's `.claude/launch.json` so multiple previews coexist without colliding — each entry pins a different port + path. Never reuse a generic "windmill" entry for different targets.
|
||||
|
||||
### Step 1 — Reuse or add a per-target entry in `.claude/launch.json`
|
||||
|
||||
Convention: name the entry `windmill: <wmill_path>` (e.g. `windmill: f/test/my_flow`).
|
||||
|
||||
- **Entry already exists** → reuse it; note its `port` for the next step.
|
||||
- **Not there** → add one. Pick a port not already taken by another entry (start at 4000 and bump). Shape:
|
||||
|
||||
For flows / scripts:
|
||||
```json
|
||||
{
|
||||
"name": "windmill: f/test/my_flow",
|
||||
"runtimeExecutable": "bash",
|
||||
"runtimeArgs": ["-c", "wmill dev --proxy-port ${PORT:-4000} --path f/test/my_flow --no-open"],
|
||||
"port": 4000,
|
||||
"autoPort": true
|
||||
}
|
||||
```
|
||||
|
||||
For apps (`*__raw_app/`), `wmill app dev` is the equivalent — runs from the app folder, no `--path`:
|
||||
```json
|
||||
{
|
||||
"name": "windmill: f/test/my_app",
|
||||
"runtimeExecutable": "bash",
|
||||
"runtimeArgs": ["-c", "cd f/test/my_app__raw_app && wmill app dev --no-open --port ${PORT:-4001}"],
|
||||
"port": 4001,
|
||||
"autoPort": true
|
||||
}
|
||||
```
|
||||
|
||||
If `.claude/launch.json` doesn't exist yet, create it with the standard shell `{ "version": "0.0.1", "configurations": [...] }`.
|
||||
|
||||
### Step 2 — Invoke the MCP preview tool
|
||||
|
||||
Point it at the entry you just added/found. Use `http://localhost:<port>/` as the URL — the proxy's redirect at `/` is what appends the workspace ID, the auth token, and the path. Do **NOT** construct a `/dev?...` URL yourself.
|
||||
|
||||
The MCP tool launches the configuration on demand, so you don't need to start the `wmill dev` process manually.
|
||||
|
||||
## Non-visual alternative
|
||||
|
||||
If the user wants a programmatic test rather than a visual one:
|
||||
- Flow: `wmill flow preview <path> -d '<args>'`
|
||||
- Script: `wmill script preview <path> -d '<args>'`
|
||||
|
||||
Both print the job result, are safe to run yourself, and don't deploy.
|
||||
|
||||
## Anti-patterns to avoid
|
||||
|
||||
- ❌ Writing a `.claude/launch.json` entry when no `mcp__Claude_Preview__*` tool is in your tool list. Nothing will read the file; the server never starts. Spawn `wmill dev` yourself instead.
|
||||
- ❌ Starting the proxy when no embedder needs a localhost URL. Direct mode is the right choice — the proxy is overhead with no purpose.
|
||||
- ❌ Reusing a single generic `launch.json` entry for every preview target. Each flow/script/app gets its own named entry on its own port — that's how multiple sessions coexist without one preview clobbering another.
|
||||
- ❌ Mutating an existing entry's `--path` to retarget it. Add a new entry instead.
|
||||
- ❌ Constructing `http://localhost:<port>/dev?path=<X>` yourself. The proxy's `/` redirect is what appends the workspace ID and auth token; bypassing it gives a broken page. Always use `http://localhost:<port>/`.
|
||||
- ❌ Starting `wmill dev` in the foreground (you'll hang). Always background.
|
||||
- ❌ Listing both "open in IDE pane" and "open in browser" as a menu — pick one based on context.
|
||||
@@ -4,11 +4,70 @@ Raw apps let you build custom frontends with React, Svelte, or Vue that connect
|
||||
|
||||
## Creating a Raw App
|
||||
|
||||
**You — the AI agent — create the app yourself by running `wmill app new` with the right flags. Do NOT tell the user to "run `wmill app new` and follow the prompts" or wait for them to do it.** The bare `wmill app new` is an interactive wizard that hangs waiting for stdin in any non-TTY context (which includes you). Always pass flags.
|
||||
|
||||
### Step 1 — Gather the three required values by asking the user
|
||||
|
||||
You need three things to run the command:
|
||||
|
||||
1. **summary** — a short description of the app
|
||||
2. **path** — the windmill path, e.g. `f/folder/my_app` or `u/username/my_app`
|
||||
3. **framework** — one of `react19` (recommended), `react18`, `svelte5`, `vue`
|
||||
|
||||
If the user's request did not supply *every* one of these explicitly, ask. Do not guess values, do not invent paths, do not pick a framework on the user's behalf, do not "just use react19 because it's the default".
|
||||
|
||||
Use whichever interactive question facility your runtime provides — a structured multi-choice tool if available, otherwise plain chat — and group all missing fields into a single round-trip so the user answers them at once:
|
||||
|
||||
- For `framework` — multiple-choice with the four allowed values; mark `react19` as `(Recommended)` and put it first.
|
||||
- For `summary` and `path` — provide one or two example values as multiple-choice options (the user can pick "Other" to type a free-form answer).
|
||||
|
||||
Only proceed once you have concrete values for all three. If the user replies with something ambiguous, ask again rather than guessing.
|
||||
|
||||
### Step 2 — Run the command yourself
|
||||
|
||||
Once you have summary + path + framework, run it:
|
||||
|
||||
```bash
|
||||
wmill app new \
|
||||
--summary "Customer dashboard" \
|
||||
--path f/sales/dashboard \
|
||||
--framework react19
|
||||
```
|
||||
|
||||
That's the minimum. The datatable wizard and the "Open in Claude Desktop?" prompt are skipped silently because passing any of `--summary`/`--path`/`--framework` puts the command in non-interactive mode.
|
||||
|
||||
### Optional flags
|
||||
|
||||
Layer these in only when the user asked for them:
|
||||
|
||||
| Flag | When to add it |
|
||||
|---|---|
|
||||
| `--datatable <name>` | The user wants this app wired to a specific Windmill datatable. Without it, the app is created with no datatable. |
|
||||
| `--schema <name>` | Together with `--datatable`. Creates the schema with `CREATE SCHEMA IF NOT EXISTS` if it doesn't already exist. |
|
||||
| `--overwrite` | The target directory already exists and the user said it's OK to replace. Without it, non-interactive mode aborts with an error so you don't clobber existing work. |
|
||||
| `--no-open-in-desktop` | Already implied in non-interactive mode; only needed if you're somehow running interactively. |
|
||||
|
||||
### Step 3 — Offer the visual preview
|
||||
|
||||
After `wmill app new` and any initial edits to `App.tsx` / `index.tsx`, **offer** to open the visual preview as a one-sentence next step (e.g. "Want me to open the visual preview?"). Don't auto-open — opening the dev page has side effects (browser window, possibly a `launch.json` entry when an embedded preview tool is in play) the user should consent to.
|
||||
|
||||
For apps the preview command runs from the app folder (`cd <app_path>__raw_app && wmill app dev …`); the `preview` skill picks the proxy vs direct branch based on whether the runtime exposes a tool that can embed a localhost URL. If the user already asked to see/preview/visualize the app in their original request, skip the offer and just invoke the skill.
|
||||
|
||||
### Anti-patterns to avoid
|
||||
|
||||
- ❌ Running `wmill app new` with no flags (the prompt will hang).
|
||||
- ❌ Telling the user to "run `wmill app new` and follow the prompts" — that's a step backwards from what you can do directly.
|
||||
- ❌ Inventing a path/summary/framework instead of asking the user.
|
||||
- ❌ Defaulting to `react19` because the user didn't say — even sensible defaults must be confirmed.
|
||||
- ❌ Passing `--overwrite` automatically when the directory exists — confirm with the user first.
|
||||
|
||||
### Interactive (only when a human is at the terminal)
|
||||
|
||||
```bash
|
||||
wmill app new
|
||||
```
|
||||
|
||||
This interactive command creates a complete app structure with your choice of frontend framework (React, Svelte, or Vue).
|
||||
This is the wizard. It only works when run by a human in a real terminal. Don't call it this way from an agent.
|
||||
|
||||
## App Structure
|
||||
|
||||
@@ -232,12 +291,13 @@ data:
|
||||
|
||||
## CLI Commands
|
||||
|
||||
Tell the user they can run these commands (do NOT run them yourself):
|
||||
`wmill app new` is the exception: you run it yourself, with flags, per the "Creating a Raw App" section above.
|
||||
|
||||
For everything else, tell the user which command fits their intent and let them run it — these touch the workspace or local lock files, and the user should consent each time:
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `wmill app new` | Create a new raw app interactively |
|
||||
| `wmill app dev` | Start dev server with live reload |
|
||||
| `wmill app dev` | Start dev server with live reload (see the `preview` skill for the full open-the-app-in-the-IDE-pane procedure). |
|
||||
| `wmill app generate-agents` | Refresh AGENTS.md and DATATABLES.md |
|
||||
| `wmill generate-metadata` | Generate lock files for backend runnables |
|
||||
| `wmill sync push` | Deploy app to Windmill |
|
||||
|
||||
@@ -931,6 +931,11 @@ SKILL_DEFINITIONS = [
|
||||
'description': 'MUST use when using the CLI, including debugging job failures and inspecting run history via `wmill job`.',
|
||||
'content_key': 'cli_commands',
|
||||
},
|
||||
{
|
||||
'name': 'preview',
|
||||
'description': 'MUST use when opening the Windmill dev page / visual preview of a flow, script, or app. Triggers on words like preview, open, navigate to, visualize, see the flow/app/script, and after writing a flow/script/app for visual verification.',
|
||||
'content_key': 'preview',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -960,16 +965,42 @@ def generate_skills(
|
||||
'schedules': read_markdown_file(base_dir / "schedules.md"),
|
||||
'resources': read_markdown_file(base_dir / "resources.md"),
|
||||
'cli_commands': cli_commands,
|
||||
'preview': read_markdown_file(base_dir / "preview.md"),
|
||||
}
|
||||
|
||||
# CLI intro for script skills
|
||||
script_cli_intro = """## CLI Commands
|
||||
|
||||
Place scripts in a folder. After writing, tell the user they can run:
|
||||
- `wmill generate-metadata` - Generate .script.yaml and .lock files
|
||||
- `wmill sync push` - Deploy to Windmill
|
||||
Place scripts in a folder.
|
||||
|
||||
Do NOT run these commands yourself. Instead, inform the user that they should run them.
|
||||
After writing, tell the user which command fits what they want to do:
|
||||
|
||||
- `wmill script preview <script_path>` — **default when iterating on a local script.** Runs the local file without deploying.
|
||||
- `wmill script run <path>` — runs the script **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
|
||||
- `wmill generate-metadata` — generate `.script.yaml` and `.lock` files for the script you modified.
|
||||
- `wmill sync push` — deploy local changes to the workspace. Only suggest/run this when the user explicitly asks to deploy/publish/push — not when they say "run", "try", or "test".
|
||||
|
||||
### Preview vs run — choose by intent, not habit
|
||||
|
||||
If the user says "run the script", "try it", "test it", "does it work" while there are **local edits to the script file**, use `script preview`. Do NOT push the script to then `script run` it — pushing is a deploy, and deploying just to test overwrites the workspace version with untested changes.
|
||||
|
||||
Only use `script run` when:
|
||||
- The user explicitly says "run the deployed version" / "run what's on the server".
|
||||
- There is no local script being edited (you're just invoking an existing script).
|
||||
|
||||
Only use `sync push` when:
|
||||
- The user explicitly asks to deploy, publish, push, or ship.
|
||||
- The preview has already validated the change and the user wants it in the workspace.
|
||||
|
||||
### After writing — offer to test, don't wait passively
|
||||
|
||||
If the user hasn't already told you to run/test/preview the script, offer it as a one-sentence next step (e.g. "Want me to run `wmill script preview` with sample args?"). Do not present a multi-option menu.
|
||||
|
||||
If the user already asked to test/run/try the script in their original request, skip the offer and just execute `wmill script preview <path> -d '<args>'` directly — pick plausible args from the script's declared parameters. The shape varies by language: `main(...)` for code languages, the SQL dialect's own placeholder syntax (`$1` for PostgreSQL, `?` for MySQL/Snowflake, `@P1` for MSSQL, `@name` for BigQuery, etc.), positional `$1`, `$2`, … for Bash, `param(...)` for PowerShell.
|
||||
|
||||
`wmill script preview` does not deploy, but it still executes script code and may cause side effects; run it yourself when the user asked to test/preview (or after confirming that execution is intended). `wmill sync push` and `wmill generate-metadata` modify workspace state or local files — only run these when the user explicitly asks; otherwise tell them which to run.
|
||||
|
||||
For a **visual** open-the-script-in-the-dev-page preview (rather than `script preview`'s run-and-print-result), use the `preview` skill.
|
||||
|
||||
Use `wmill resource-type list --schema` to discover available resource types."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user