Skip to content
Open
Show file tree
Hide file tree
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 Apr 9, 2026
95e8bd1
feat(freestyle): add repo URL input in settings + entity refetch afte…
tlgimenes Apr 9, 2026
1949690
fix(freestyle): add step-by-step error context to setup flow
tlgimenes Apr 9, 2026
a23294a
fix(freestyle): make GitHub sync optional in setup flow
tlgimenes Apr 9, 2026
48b7430
fix(freestyle): use VmBun integration install() instead of systemd fo…
tlgimenes Apr 9, 2026
cb91e85
feat(freestyle): add GitHub settings tab + Preview button in tasks panel
tlgimenes Apr 9, 2026
db1868d
fix(freestyle): move GitHub tab to last position in settings
tlgimenes Apr 9, 2026
893f8d2
fix(freestyle): separate GitHub tab to the right with GitHub logo icon
tlgimenes Apr 9, 2026
e0613b2
fix(freestyle): use hosted GitHub logo image instead of icon component
tlgimenes Apr 9, 2026
b93bbe7
fix(freestyle): debounce preview port input — save on blur instead of…
tlgimenes Apr 9, 2026
14cdd33
feat(freestyle): add Deno runtime support
tlgimenes Apr 9, 2026
cde369c
fix(freestyle): add deno to VirtualMCPEntitySchema runtime enum
tlgimenes Apr 9, 2026
05c9d8a
feat(freestyle): add scripts/tasks editor to GitHub tab
tlgimenes Apr 9, 2026
17a9230
fix(freestyle): remove script editor, show scripts as read-only tags
tlgimenes Apr 9, 2026
165cc6a
fix(freestyle): save detection results (scripts, runtime) before Free…
tlgimenes Apr 9, 2026
8ab695e
debug(freestyle): add logs to detection, add-repo, and play button
tlgimenes Apr 9, 2026
34529a9
fix(freestyle): remove scripts/tasks list from GitHub tab
tlgimenes Apr 9, 2026
c34aa59
refactor(freestyle): move play button from header to preview empty state
tlgimenes Apr 9, 2026
9bc65b0
refactor(freestyle): unified browser inspector with persistent toolbar
tlgimenes Apr 9, 2026
6854c2a
fix(freestyle): pin browser inspector toolbar to top of content area
tlgimenes Apr 9, 2026
6b21dc5
fix(freestyle): allow run-script when status is stale running without…
tlgimenes Apr 9, 2026
7e43b0e
debug(freestyle): add logs to browser inspector view
tlgimenes Apr 9, 2026
fb09ec9
fix(freestyle): handle empty string vm_domain as null
tlgimenes Apr 9, 2026
2153ab0
debug(freestyle): log run-script error content in browser console
tlgimenes Apr 9, 2026
f70f2a4
fix(freestyle): use snapshot key instead of spec in vms.create()
tlgimenes Apr 9, 2026
7e8f903
fix(freestyle): async VM creation to prevent MCP timeout
tlgimenes Apr 9, 2026
6bf2948
fix(freestyle): use flat create() options instead of VmSpec for domai…
tlgimenes Apr 9, 2026
4a5e4b0
debug(freestyle): extensive logging for VM domain resolution
tlgimenes Apr 9, 2026
1aac780
debug(freestyle): dump full VM info and construct domain from vmId as…
tlgimenes Apr 9, 2026
227a128
debug(freestyle): add VM diagnostics — check listening ports and app …
tlgimenes Apr 9, 2026
afd2416
fix(freestyle): use correct integration key for deno (deno not js)
tlgimenes Apr 9, 2026
e713b38
fix: remove unused freestyle-play-button component
tlgimenes Apr 9, 2026
a42fadf
feat(preview): add terminal log viewer during VM install
tlgimenes Apr 9, 2026
15d490e
fix: resolve rebase merge errors in shell-layout and browser-inspector
tlgimenes Apr 9, 2026
ad0be05
fix(freestyle): update emptyFreestyleMetadata test for terminal_domai…
tlgimenes Apr 9, 2026
7f4bc6b
fix(preview): update metadata with terminal_domain before health-check
tlgimenes Apr 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/mesh/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@freestyle-sh/with-bun": "^0.2.10",
"@freestyle-sh/with-deno": "^0.0.2",
"@freestyle-sh/with-web-terminal": "^0.0.7",
"@inkjs/ui": "^2.0.0",
"@modelcontextprotocol/ext-apps": "^1.2.2",
"@openrouter/ai-sdk-provider": "^2.2.5",
Expand All @@ -57,6 +60,7 @@
"ai-sdk-provider-claude-code": "^3.4.4",
"ai-sdk-provider-codex-cli": "^1.1.0",
"embedded-postgres": "^18.3.0-beta.16",
"freestyle-sandboxes": "^0.1.42",
"ink": "^6.8.0",
"kysely": "^0.28.12",
"nats": "^2.29.3",
Expand Down
1 change: 1 addition & 0 deletions apps/mesh/src/core/context-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1045,6 +1045,7 @@ export async function createMeshContextFactory(
getOrCreateClient: clientPool,
pendingRevalidations: [],
firecrawlApiKey: getSettings().firecrawlApiKey,
freestyleApiKey: getSettings().freestyleApiKey,
};

return ctx;
Expand Down
1 change: 1 addition & 0 deletions apps/mesh/src/core/mesh-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,7 @@ export interface MeshContext {

// External API keys (optional, from settings)
firecrawlApiKey?: string;
freestyleApiKey?: string;

// Automation runner — fires an automation manually (wired in app.ts)
automationRunner?: (
Expand Down
5 changes: 5 additions & 0 deletions apps/mesh/src/freestyle/client.ts
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 });
}
277 changes: 277 additions & 0 deletions apps/mesh/src/freestyle/detect.test.ts
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" } }),
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The deno.jsonc test uses JSON instead of JSONC, so it doesn’t actually validate JSONC parsing.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/freestyle/detect.test.ts, line 78:

<comment>The `deno.jsonc` test uses JSON instead of JSONC, so it doesn’t actually validate JSONC parsing.</comment>

<file context>
@@ -49,16 +49,91 @@ describe("detectRepo", () => {
+    const result = await detectRepo(
+      "owner/repo",
+      mockReader({
+        "deno.jsonc": JSON.stringify({ tasks: { dev: "deno run dev.ts" } }),
+      }),
+    );
</file context>
Suggested change
"deno.jsonc": JSON.stringify({ tasks: { dev: "deno run dev.ts" } }),
"deno.jsonc": `{
// dev task
"tasks": {
"dev": "deno run dev.ts",
},
}`,
Fix with Cubic

}),
);

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();
});
});
Loading
Loading