Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
74 changes: 74 additions & 0 deletions apps/desktop/src/bun/rpc/deploy-vercel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { stat } from "node:fs/promises";

import type { RuntimeClient, RuntimeId } from "@llm-space/runtime/runtime";

import { VercelDeployError, deploySingleFile, deployStaticFolder } from "../vercel";

export interface DeployVercelInput {
runtimeId?: RuntimeId;
/**
* Workspace-relative deploy target: a folder of static files (must contain
* an `index.html`) or one self-contained HTML file (served as the site's
* index page).
*/
path: string;
}

export type DeployVercelResult =
| { ok: true; url: string; fileCount: number }
| { ok: false; error: string };

interface DeployVercelHandlerDependencies {
getRuntime: (runtimeId: RuntimeId) => RuntimeClient;
getToken: () => string | null;
}

/**
* Create the Bun RPC handler that deploys a workspace folder of static files
* to Vercel. The token is read inside the bun process; the renderer receives
* only the resulting URL or a friendly error.
*/
export function createDeployVercelHandler({
getRuntime,
getToken,
}: DeployVercelHandlerDependencies) {
return async ({
runtimeId,
path,
}: DeployVercelInput): Promise<DeployVercelResult> => {
const token = getToken();
if (!token) {
return {
ok: false,
error: "Vercel token is not configured. Add one in Settings → Account.",
};
}
try {
if (!runtimeId) {
throw new VercelDeployError("No runtime is available for this deploy.");
}
const runtime = getRuntime(runtimeId);
if (runtime.info().status !== "connected") {
throw new VercelDeployError(`Runtime is not connected: ${runtimeId}`);
}
const resolved = await runtime.fsRealpath(path);
// A single HTML file deploys as a self-contained page; anything else
// deploys as a folder.
const isFile = (await stat(resolved)).isFile();
const result = isFile
? await deploySingleFile({ token, file: resolved })
: await deployStaticFolder({ token, dir: resolved });
return {
ok: true,
url: result.url,
fileCount: result.fileCount,
};
} catch (error) {
return {
ok: false,
error:
error instanceof Error ? error.message : "Vercel deployment failed.",
};
}
};
}
23 changes: 23 additions & 0 deletions apps/desktop/src/bun/rpc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,15 @@ import type { RemoteServerManager } from "../remote";
import type { RuntimeRouter } from "../runtime";
import type { SkillsManager } from "../skills";
import type { UpdaterService } from "../updates";
import { VercelTokenStore } from "../vercel";

import { createDeployVercelHandler } from "./deploy-vercel";
import { ensureRootDir } from "./ensure-root-dir";
import { fsReveal } from "./fs-reveal";
import { createPromptFileRpcHandlers } from "./prompt-files";
import { createShareThreadHandler } from "./share-thread";
import { forwardStreamThread } from "./stream-thread-request";
import { createVercelPreflightHandler } from "./vercel-preflight";

/**
* The stream handler references its RPC instance inside the initializer, so an
Expand Down Expand Up @@ -91,6 +94,7 @@ export function createMainWindowRPC({
}: MainWindowRPCDependencies): MainWindowRPC {
const getRuntime = runtimeRouter.get.bind(runtimeRouter);
const promptFileRequests = createPromptFileRpcHandlers(getRuntime);
const vercelTokens = new VercelTokenStore();
const rpc: MainWindowRPC = BrowserView.defineRPC<DesktopRPCType>({
maxRequestTime: MAX_REQUEST_TIME_MS,
handlers: {
Expand Down Expand Up @@ -310,6 +314,25 @@ export function createMainWindowRPC({
Promise.resolve(memoryStore.updateMemory({ id, content, tags })),
memoryExport: () => Promise.resolve(memoryStore.exportMemories()),
memoryClearArchive: () => Promise.resolve(memoryStore.clearArchive()),
// Vercel static deployments: the token is read from
// `settings/vercel.json` inside the bun process; the renderer only sees
// the configured flag and the deploy outcome (URL or friendly error).
getVercelStatus: () =>
Promise.resolve({ configured: vercelTokens.isConfigured() }),
setVercelToken: ({ token }) => {
vercelTokens.set(token);
return Promise.resolve(null);
},
removeVercelToken: () => {
vercelTokens.clear();
return Promise.resolve(null);
},
deployToVercel: createDeployVercelHandler({
getRuntime,
getToken: () => vercelTokens.getAccessToken(),
}),
vercelPreflight: createVercelPreflightHandler({ getRuntime }),

fsReveal: async ({ path }) => {
await fsReveal(path, { skillsManager });
return null;
Expand Down
60 changes: 60 additions & 0 deletions apps/desktop/src/bun/rpc/vercel-preflight.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { stat } from "node:fs/promises";

import type { RuntimeClient, RuntimeId } from "@llm-space/runtime/runtime";

import { VercelDeployError, collectSingleFile, collectStaticFiles } from "../vercel";

export interface VercelPreflightInput {
runtimeId?: RuntimeId;
/** Workspace-relative deploy target: a static folder or one HTML file. */
path: string;
}

interface VercelPreflightHandlerDependencies {
getRuntime: (runtimeId: RuntimeId) => RuntimeClient;
}

/**
* Create the Bun RPC handler that validates a deploy target for Vercel
* without uploading: for folders, index.html presence, file count, and size
* limits; for a single HTML file, its size. Metadata only — file contents
* are never read.
*/
export function createVercelPreflightHandler({
getRuntime,
}: VercelPreflightHandlerDependencies) {
return async ({
runtimeId,
path,
}: VercelPreflightInput): Promise<
| { ok: true; fileCount: number; totalBytes: number }
| { ok: false; error: string }
> => {
try {
if (!runtimeId) {
throw new VercelDeployError("No runtime is available for this deploy.");
}
const runtime = getRuntime(runtimeId);
if (runtime.info().status !== "connected") {
throw new VercelDeployError(`Runtime is not connected: ${runtimeId}`);
}
const resolved = await runtime.fsRealpath(path);
const collected = (await stat(resolved)).isFile()
? await collectSingleFile(resolved, { withContents: false })
: await collectStaticFiles(resolved, { withContents: false });
return {
ok: true,
fileCount: collected.files.length,
totalBytes: collected.totalBytes,
};
} catch (error) {
return {
ok: false,
error:
error instanceof Error
? error.message
: "This folder cannot be deployed to Vercel.",
};
}
};
}
10 changes: 10 additions & 0 deletions apps/desktop/src/bun/vercel/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export { VercelTokenStore } from "./vercel-token-store";
export {
VercelDeployError,
collectSingleFile,
collectStaticFiles,
deploySingleFile,
deployStaticFolder,
type CollectedDeployment,
type DeployResult,
} from "./vercel-client";
Loading