Spaces:
Sleeping
Sleeping
mfn
Embedded IDE: file tree + CodeMirror editor, workspace terminal, markdown output; agent deploy/monitor/logs tools + git push; hardware-aware deploy
5f0975b Download src/lib/deploy.ts from mrfakename/spacefactory2: direct link, hf CLI and curl.
- Browser
- Download file 5.21 kB
-
https://huggingface.co/spaces/mrfakename/spacefactory2/resolve/main/src/lib/deploy.ts
- Command line
-
hf download hf://spaces/mrfakename/spacefactory2/src/lib/deploy.ts
-
curl -L -o deploy.ts https://huggingface.co/spaces/mrfakename/spacefactory2/resolve/main/src/lib/deploy.ts
5.21 kB
| import { execFile } from "node:child_process"; | |
| import { promisify } from "node:util"; | |
| import { createRepo, uploadFiles } from "@huggingface/hub"; | |
| import { HF_HUB } from "./config"; | |
| import type { HardwareFlavor } from "./hardware"; | |
| import type { Workspace } from "./agent/workspace"; | |
| const execFileAsync = promisify(execFile); | |
| export interface DeployResult { | |
| url: string; | |
| repoId: string; | |
| hardware: string; | |
| hardwareRequested: boolean; | |
| method: "git" | "upload"; | |
| } | |
| export function spaceRemoteUrl(owner: string, spaceName: string, token: string): string { | |
| return `https://${encodeURIComponent(owner)}:${token}@huggingface.co/spaces/${owner}/${spaceName}`; | |
| } | |
| async function git(workspace: Workspace, args: string[]): Promise<string> { | |
| const { stdout } = await execFileAsync("git", args, { | |
| cwd: workspace.dir, | |
| maxBuffer: 8_000_000, | |
| timeout: 120_000, | |
| }); | |
| return stdout; | |
| } | |
| async function requestHardware( | |
| repoId: string, | |
| token: string, | |
| hardware: HardwareFlavor, | |
| ): Promise<boolean> { | |
| if (hardware === "cpu-basic") return false; | |
| try { | |
| const res = await fetch(`${HF_HUB}/api/spaces/${repoId}/hardware`, { | |
| method: "POST", | |
| headers: { | |
| Authorization: `Bearer ${token}`, | |
| "Content-Type": "application/json", | |
| }, | |
| body: JSON.stringify({ flavor: hardware }), | |
| }); | |
| return res.ok; | |
| } catch { | |
| return false; | |
| } | |
| } | |
| /** Deploy the workspace to the user's Space. Ensures the repo exists, then | |
| * pushes via git (preferred — the agent can also `git push` manually), falling | |
| * back to the Hub upload API if git push fails. */ | |
| export async function deployWorkspace(opts: { | |
| accessToken: string; | |
| owner: string; | |
| spaceName: string; | |
| workspace: Workspace; | |
| isPrivate: boolean; | |
| hardware: HardwareFlavor; | |
| }): Promise<DeployResult> { | |
| const repoId = `${opts.owner}/${opts.spaceName}`; | |
| const repo = { type: "space" as const, name: repoId }; | |
| try { | |
| await createRepo({ | |
| repo, | |
| accessToken: opts.accessToken, | |
| visibility: opts.isPrivate ? "private" : "public", | |
| sdk: "gradio", | |
| }); | |
| } catch (err) { | |
| const message = err instanceof Error ? err.message : String(err); | |
| if (!/already (exists|created)/i.test(message)) throw err; | |
| } | |
| let method: "git" | "upload" = "git"; | |
| try { | |
| const remote = spaceRemoteUrl(opts.owner, opts.spaceName, opts.accessToken); | |
| await git(opts.workspace, ["remote", "remove", "space"]).catch(() => {}); | |
| await git(opts.workspace, ["remote", "add", "space", remote]); | |
| await git(opts.workspace, ["add", "-A"]); | |
| // Commit; ignore "nothing to commit" so re-deploys still push. | |
| await git(opts.workspace, [ | |
| "-c", | |
| "user.name=mfn", | |
| "-c", | |
| "user.email=mfn@example.com", | |
| "commit", | |
| "-m", | |
| "Deploy via SpaceFactory", | |
| ]).catch(() => {}); | |
| await git(opts.workspace, ["push", "--force", "space", "HEAD:main"]); | |
| } catch { | |
| // Fallback: Hub upload API. | |
| method = "upload"; | |
| await uploadFiles({ | |
| repo, | |
| accessToken: opts.accessToken, | |
| files: opts.workspace.toFiles().map((f) => ({ | |
| path: f.path, | |
| content: new Blob([f.content]), | |
| })), | |
| commitTitle: "Deploy via SpaceFactory", | |
| }); | |
| } | |
| const hardwareRequested = await requestHardware( | |
| repoId, | |
| opts.accessToken, | |
| opts.hardware, | |
| ); | |
| return { | |
| url: `${HF_HUB}/spaces/${repoId}`, | |
| repoId, | |
| hardware: opts.hardware, | |
| hardwareRequested, | |
| method, | |
| }; | |
| } | |
| export interface SpaceRuntime { | |
| stage: string; | |
| hardware?: string; | |
| } | |
| export async function getSpaceRuntime( | |
| repoId: string, | |
| token: string, | |
| ): Promise<SpaceRuntime> { | |
| const res = await fetch(`${HF_HUB}/api/spaces/${repoId}`, { | |
| headers: { Authorization: `Bearer ${token}` }, | |
| }); | |
| if (!res.ok) throw new Error(`Could not read Space ${repoId} (HTTP ${res.status})`); | |
| const data = (await res.json()) as { | |
| runtime?: { stage?: string; hardware?: { current?: string } }; | |
| }; | |
| return { | |
| stage: data.runtime?.stage ?? "UNKNOWN", | |
| hardware: data.runtime?.hardware?.current, | |
| }; | |
| } | |
| export async function fetchSpaceLogs( | |
| repoId: string, | |
| token: string, | |
| kind: "build" | "run", | |
| maxLines = 120, | |
| ): Promise<string> { | |
| const res = await fetch(`${HF_HUB}/api/spaces/${repoId}/logs/${kind}`, { | |
| headers: { Authorization: `Bearer ${token}`, Accept: "text/event-stream" }, | |
| signal: AbortSignal.timeout(30_000), | |
| }); | |
| if (!res.ok) throw new Error(`Could not fetch ${kind} logs (HTTP ${res.status})`); | |
| const text = await res.text(); | |
| const lines: string[] = []; | |
| for (const line of text.split("\n")) { | |
| if (!line.startsWith("data:")) continue; | |
| try { | |
| const data = JSON.parse(line.slice(5).trim()) as { data?: string }; | |
| if (data.data != null) lines.push(String(data.data).replace(/\n$/, "")); | |
| } catch { | |
| // ignore keepalives / partial frames | |
| } | |
| } | |
| return lines.slice(-maxLines).join("\n") || "(no logs yet)"; | |
| } | |
| const TERMINAL_STAGES = new Set([ | |
| "RUNNING", | |
| "BUILD_ERROR", | |
| "RUNTIME_ERROR", | |
| "CONFIG_ERROR", | |
| "ERROR", | |
| "APP_STARTING_ERROR", | |
| "DELETING", | |
| "PAUSED", | |
| ]); | |
| export function isTerminalStage(stage: string): boolean { | |
| return TERMINAL_STAGES.has(stage); | |
| } | |