-
Notifications
You must be signed in to change notification settings - Fork 42
feat(freestyle): add Freestyle VM integration for Virtual MCP repos #3079
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
tlgimenes
wants to merge
36
commits into
main
Choose a base branch
from
tlgimenes/check-freestyle-docs
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
36 commits
Select commit
Hold shift + click to select a range
ddcceaa
feat(freestyle): add Freestyle VM integration for Virtual MCP repos
tlgimenes 95e8bd1
feat(freestyle): add repo URL input in settings + entity refetch afte…
tlgimenes 1949690
fix(freestyle): add step-by-step error context to setup flow
tlgimenes a23294a
fix(freestyle): make GitHub sync optional in setup flow
tlgimenes 48b7430
fix(freestyle): use VmBun integration install() instead of systemd fo…
tlgimenes cb91e85
feat(freestyle): add GitHub settings tab + Preview button in tasks panel
tlgimenes db1868d
fix(freestyle): move GitHub tab to last position in settings
tlgimenes 893f8d2
fix(freestyle): separate GitHub tab to the right with GitHub logo icon
tlgimenes e0613b2
fix(freestyle): use hosted GitHub logo image instead of icon component
tlgimenes b93bbe7
fix(freestyle): debounce preview port input — save on blur instead of…
tlgimenes 14cdd33
feat(freestyle): add Deno runtime support
tlgimenes cde369c
fix(freestyle): add deno to VirtualMCPEntitySchema runtime enum
tlgimenes 05c9d8a
feat(freestyle): add scripts/tasks editor to GitHub tab
tlgimenes 17a9230
fix(freestyle): remove script editor, show scripts as read-only tags
tlgimenes 165cc6a
fix(freestyle): save detection results (scripts, runtime) before Free…
tlgimenes 8ab695e
debug(freestyle): add logs to detection, add-repo, and play button
tlgimenes 34529a9
fix(freestyle): remove scripts/tasks list from GitHub tab
tlgimenes c34aa59
refactor(freestyle): move play button from header to preview empty state
tlgimenes 9bc65b0
refactor(freestyle): unified browser inspector with persistent toolbar
tlgimenes 6854c2a
fix(freestyle): pin browser inspector toolbar to top of content area
tlgimenes 6b21dc5
fix(freestyle): allow run-script when status is stale running without…
tlgimenes 7e43b0e
debug(freestyle): add logs to browser inspector view
tlgimenes fb09ec9
fix(freestyle): handle empty string vm_domain as null
tlgimenes 2153ab0
debug(freestyle): log run-script error content in browser console
tlgimenes f70f2a4
fix(freestyle): use snapshot key instead of spec in vms.create()
tlgimenes 7e8f903
fix(freestyle): async VM creation to prevent MCP timeout
tlgimenes 6bf2948
fix(freestyle): use flat create() options instead of VmSpec for domai…
tlgimenes 4a5e4b0
debug(freestyle): extensive logging for VM domain resolution
tlgimenes 1aac780
debug(freestyle): dump full VM info and construct domain from vmId as…
tlgimenes 227a128
debug(freestyle): add VM diagnostics — check listening ports and app …
tlgimenes afd2416
fix(freestyle): use correct integration key for deno (deno not js)
tlgimenes e713b38
fix: remove unused freestyle-play-button component
tlgimenes a42fadf
feat(preview): add terminal log viewer during VM install
tlgimenes 15d490e
fix: resolve rebase merge errors in shell-layout and browser-inspector
tlgimenes ad0be05
fix(freestyle): update emptyFreestyleMetadata test for terminal_domai…
tlgimenes 7f4bc6b
fix(preview): update metadata with terminal_domain before health-check
tlgimenes File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import { Freestyle } from "freestyle-sandboxes"; | ||
|
|
||
| export function createFreestyleClient(apiKey: string): Freestyle { | ||
| return new Freestyle({ apiKey }); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,277 @@ | ||
| import { describe, test, expect } from "bun:test"; | ||
| import { detectRepo, type RepoFileReader } from "./detect"; | ||
|
|
||
| function mockReader(files: Record<string, string | null>): RepoFileReader { | ||
| return { | ||
| readFile: async (_owner: string, _repo: string, path: string) => | ||
| files[path] ?? null, | ||
| }; | ||
| } | ||
|
|
||
| describe("detectRepo", () => { | ||
| test("detects bun project with scripts", async () => { | ||
| const result = await detectRepo( | ||
| "owner/repo", | ||
| mockReader({ | ||
| "package.json": JSON.stringify({ | ||
| scripts: { dev: "bun run dev", build: "bun run build" }, | ||
| }), | ||
| "bun.lock": "lockfile content", | ||
| }), | ||
| ); | ||
|
|
||
| expect(result.runtime).toBe("bun"); | ||
| expect(result.scripts).toEqual({ | ||
| dev: "bun run dev", | ||
| build: "bun run build", | ||
| }); | ||
| expect(result.instructions).toBeNull(); | ||
| expect(result.autorun).toBeNull(); | ||
| expect(result.preview_port).toBeNull(); | ||
| }); | ||
|
|
||
| test("reads AGENTS.md as instructions", async () => { | ||
| const result = await detectRepo( | ||
| "owner/repo", | ||
| mockReader({ | ||
| "package.json": JSON.stringify({ scripts: {} }), | ||
| "bun.lock": "lockfile", | ||
| "AGENTS.md": "You are a helpful agent.", | ||
| }), | ||
| ); | ||
|
|
||
| expect(result.instructions).toBe("You are a helpful agent."); | ||
| }); | ||
|
|
||
| test("throws when no JS project files found", async () => { | ||
| await expect(detectRepo("owner/repo", mockReader({}))).rejects.toThrow( | ||
| "does not appear to be a JavaScript project", | ||
| ); | ||
| }); | ||
|
|
||
| // deno detection tests | ||
|
|
||
| test("detects deno project from deno.json", async () => { | ||
| const result = await detectRepo( | ||
| "owner/repo", | ||
| mockReader({ | ||
| "deno.json": JSON.stringify({ | ||
| tasks: { | ||
| dev: "deno run --allow-net main.ts", | ||
| start: "deno run main.ts", | ||
| }, | ||
| }), | ||
| }), | ||
| ); | ||
|
|
||
| expect(result.runtime).toBe("deno"); | ||
| expect(result.scripts).toEqual({ | ||
| dev: "deno run --allow-net main.ts", | ||
| start: "deno run main.ts", | ||
| }); | ||
| }); | ||
|
|
||
| test("detects deno project from deno.jsonc", async () => { | ||
| const result = await detectRepo( | ||
| "owner/repo", | ||
| mockReader({ | ||
| "deno.jsonc": JSON.stringify({ tasks: { dev: "deno run dev.ts" } }), | ||
| }), | ||
| ); | ||
|
|
||
| expect(result.runtime).toBe("deno"); | ||
| expect(result.scripts).toEqual({ dev: "deno run dev.ts" }); | ||
| }); | ||
|
|
||
| test("detects deno project from deno.lock only", async () => { | ||
| const result = await detectRepo( | ||
| "owner/repo", | ||
| mockReader({ | ||
| "deno.lock": "lockfile", | ||
| "package.json": JSON.stringify({ scripts: { dev: "npm run dev" } }), | ||
| }), | ||
| ); | ||
|
|
||
| expect(result.runtime).toBe("deno"); | ||
| // Falls back to package.json scripts when no deno.json tasks | ||
| expect(result.scripts).toEqual({ dev: "npm run dev" }); | ||
| }); | ||
|
|
||
| test("prefers deno.json tasks over package.json scripts for deno runtime", async () => { | ||
| const result = await detectRepo( | ||
| "owner/repo", | ||
| mockReader({ | ||
| "deno.json": JSON.stringify({ tasks: { dev: "deno task dev" } }), | ||
| "package.json": JSON.stringify({ scripts: { dev: "npm run dev" } }), | ||
| "deno.lock": "lockfile", | ||
| }), | ||
| ); | ||
|
|
||
| expect(result.runtime).toBe("deno"); | ||
| expect(result.scripts).toEqual({ dev: "deno task dev" }); | ||
| }); | ||
|
|
||
| test("deco.json runtime override from bun to deno", async () => { | ||
| const result = await detectRepo( | ||
| "owner/repo", | ||
| mockReader({ | ||
| "package.json": JSON.stringify({ scripts: { dev: "bun dev" } }), | ||
| "bun.lock": "lockfile", | ||
| "deco.json": JSON.stringify({ runtime: "deno" }), | ||
| }), | ||
| ); | ||
|
|
||
| expect(result.runtime).toBe("deno"); | ||
| }); | ||
|
|
||
| test("deco.json runtime override from deno to bun", async () => { | ||
| const result = await detectRepo( | ||
| "owner/repo", | ||
| mockReader({ | ||
| "deno.json": JSON.stringify({ tasks: { dev: "deno dev" } }), | ||
| "deco.json": JSON.stringify({ runtime: "bun" }), | ||
| }), | ||
| ); | ||
|
|
||
| expect(result.runtime).toBe("bun"); | ||
| }); | ||
|
|
||
| // deco.json tests | ||
|
|
||
| test("reads deco.json with all fields", async () => { | ||
| const result = await detectRepo( | ||
| "owner/repo", | ||
| mockReader({ | ||
| "package.json": JSON.stringify({ scripts: { dev: "bun dev" } }), | ||
| "bun.lock": "lockfile", | ||
| "deco.json": JSON.stringify({ | ||
| autorun: "dev", | ||
| runtime: "bun", | ||
| previewPort: 3000, | ||
| }), | ||
| }), | ||
| ); | ||
|
|
||
| expect(result.autorun).toBe("dev"); | ||
| expect(result.preview_port).toBe(3000); | ||
| }); | ||
|
|
||
| test("reads deco.json with partial fields", async () => { | ||
| const result = await detectRepo( | ||
| "owner/repo", | ||
| mockReader({ | ||
| "package.json": JSON.stringify({ scripts: {} }), | ||
| "bun.lock": "lockfile", | ||
| "deco.json": JSON.stringify({ autorun: "start" }), | ||
| }), | ||
| ); | ||
|
|
||
| expect(result.autorun).toBe("start"); | ||
| expect(result.preview_port).toBeNull(); | ||
| }); | ||
|
|
||
| test("silently skips malformed deco.json", async () => { | ||
| const result = await detectRepo( | ||
| "owner/repo", | ||
| mockReader({ | ||
| "package.json": JSON.stringify({ scripts: {} }), | ||
| "bun.lock": "lockfile", | ||
| "deco.json": "not json", | ||
| }), | ||
| ); | ||
|
|
||
| expect(result.autorun).toBeNull(); | ||
| expect(result.preview_port).toBeNull(); | ||
| }); | ||
|
|
||
| test("silently skips deco.json with wrong types", async () => { | ||
| const result = await detectRepo( | ||
| "owner/repo", | ||
| mockReader({ | ||
| "package.json": JSON.stringify({ scripts: {} }), | ||
| "bun.lock": "lockfile", | ||
| "deco.json": JSON.stringify({ | ||
| autorun: 123, | ||
| previewPort: "abc", | ||
| }), | ||
| }), | ||
| ); | ||
|
|
||
| expect(result.autorun).toBeNull(); | ||
| expect(result.preview_port).toBeNull(); | ||
| }); | ||
|
|
||
| test("rejects deco.json previewPort out of range", async () => { | ||
| const result = await detectRepo( | ||
| "owner/repo", | ||
| mockReader({ | ||
| "package.json": JSON.stringify({ scripts: {} }), | ||
| "bun.lock": "lockfile", | ||
| "deco.json": JSON.stringify({ previewPort: 99999 }), | ||
| }), | ||
| ); | ||
|
|
||
| expect(result.preview_port).toBeNull(); | ||
| }); | ||
|
|
||
| test("rejects deco.json previewPort of 0", async () => { | ||
| const result = await detectRepo( | ||
| "owner/repo", | ||
| mockReader({ | ||
| "package.json": JSON.stringify({ scripts: {} }), | ||
| "bun.lock": "lockfile", | ||
| "deco.json": JSON.stringify({ previewPort: 0 }), | ||
| }), | ||
| ); | ||
|
|
||
| expect(result.preview_port).toBeNull(); | ||
| }); | ||
|
|
||
| test("rejects deco.json previewPort negative", async () => { | ||
| const result = await detectRepo( | ||
| "owner/repo", | ||
| mockReader({ | ||
| "package.json": JSON.stringify({ scripts: {} }), | ||
| "bun.lock": "lockfile", | ||
| "deco.json": JSON.stringify({ previewPort: -1 }), | ||
| }), | ||
| ); | ||
|
|
||
| expect(result.preview_port).toBeNull(); | ||
| }); | ||
|
|
||
| test("accepts deco.json previewPort at boundaries", async () => { | ||
| const result1 = await detectRepo( | ||
| "owner/repo", | ||
| mockReader({ | ||
| "package.json": JSON.stringify({ scripts: {} }), | ||
| "bun.lock": "lockfile", | ||
| "deco.json": JSON.stringify({ previewPort: 1 }), | ||
| }), | ||
| ); | ||
| expect(result1.preview_port).toBe(1); | ||
|
|
||
| const result2 = await detectRepo( | ||
| "owner/repo", | ||
| mockReader({ | ||
| "package.json": JSON.stringify({ scripts: {} }), | ||
| "bun.lock": "lockfile", | ||
| "deco.json": JSON.stringify({ previewPort: 65535 }), | ||
| }), | ||
| ); | ||
| expect(result2.preview_port).toBe(65535); | ||
| }); | ||
|
|
||
| test("absent deco.json returns null fields", async () => { | ||
| const result = await detectRepo( | ||
| "owner/repo", | ||
| mockReader({ | ||
| "package.json": JSON.stringify({ scripts: {} }), | ||
| "bun.lock": "lockfile", | ||
| }), | ||
| ); | ||
|
|
||
| expect(result.autorun).toBeNull(); | ||
| expect(result.preview_port).toBeNull(); | ||
| }); | ||
| }); | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: The
deno.jsonctest uses JSON instead of JSONC, so it doesn’t actually validate JSONC parsing.Prompt for AI agents