Huggy: Deploy to Hugging Face Spaces (Code Blocks)

Add a button to deploy visible code blocks to a Hugging Face Space.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @license MIT 
// @name         Huggy: Deploy to Hugging Face Spaces (Code Blocks)
// @namespace    hf-deploy-userscript
// @version      0.1.1
// @description  Add a button to deploy visible code blocks to a Hugging Face Space.
// @author       You
// @match        https://github.com/*
// @match        https://gist.github.com/*
// @match        https://gitlab.com/*
// @match        https://bitbucket.org/*
// @connect      huggingface.co
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_xmlhttpRequest
// ==/UserScript==

(function () {
  const API_BASE = "https://huggingface.co/api";
  const BTN_CLASS = "hf-deploy-button";

  function injectStyles() {
    if (document.getElementById("hf-deploy-style")) return;
    const style = document.createElement("style");
    style.id = "hf-deploy-style";
    style.textContent = `
      .${BTN_CLASS} { cursor: pointer; padding: 4px 8px; border: 1px solid #444; border-radius: 6px; background:#6e56cf; color:#fff; font-size:12px; margin-left: 8px; }
      .${BTN_CLASS}:hover { filter: brightness(1.05); }
    `;
    document.head.appendChild(style);
  }

  function gmRequest({ method = 'GET', url, headers = {}, data = null }) {
    return new Promise((resolve, reject) => {
      GM_xmlhttpRequest({
        method,
        url,
        headers,
        data,
        onload: (res) => {
          resolve({ status: res.status, ok: res.status >= 200 && res.status < 300, text: res.responseText });
        },
        onerror: (e) => reject(e),
      });
    });
  }

  async function whoami(token) {
    const r = await gmRequest({ method: 'GET', url: `${API_BASE}/whoami-v2`, headers: { Authorization: `Bearer ${token}` } });
    if (!r.ok) throw new Error(`whoami failed: ${r.status}`);
    return JSON.parse(r.text);
  }

  async function ensureSpace(token, namespace, name, sdk, priv) {
    const body = { name, repo_type: "space", space_sdk: sdk, private: !!priv };
    if (namespace) body.organization = namespace;
    const r = await gmRequest({
      method: 'POST',
      url: `${API_BASE}/repos/create`,
      headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
      data: JSON.stringify(body),
    });
    if (!r.ok && r.status !== 409) {
      throw new Error(`create repo failed: ${r.status} ${r.text}`);
    }
  }

  async function commitFiles(token, repoId, filesMap, message) {
    const ops = Object.entries(filesMap).map(([path, content]) => ({ op: "addOrUpdate", path, content }));
    const r = await gmRequest({
      method: 'POST',
      url: `${API_BASE}/spaces/${repoId}/commit/main`,
      headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
      data: JSON.stringify({ operations: ops, summary: message || "Deploy via userscript" }),
    });
    if (!r.ok) throw new Error(`commit failed: ${r.status} ${r.text}`);
  }

  function detectCodeBlocks() {
    const gh = Array.from(document.querySelectorAll(".blob-wrapper pre, .highlight pre, code.language-python, code, pre code"));
    return gh.filter((el) => (el.textContent || "").trim().length > 0);
  }

  function addButtons() {
    injectStyles();
    const blocks = detectCodeBlocks();
    for (const pre of blocks) {
      if (pre.__hfDeployDecorated) continue;
      pre.__hfDeployDecorated = true;

      const container = pre.closest(".Box-header, .file-header, .gist-header, h1, h2, h3") || pre.parentElement;
      if (!container) continue;

      const btn = document.createElement("button");
      btn.className = BTN_CLASS;
      btn.textContent = "Deploy to HF";
      btn.addEventListener("click", async (e) => {
        e.stopPropagation();

        let token = GM_getValue("hf_token", "");
        if (!token) {
          token = prompt("Paste your Hugging Face token (stored locally):", "");
          if (!token) return;
          GM_setValue("hf_token", token);
        }

        const who = await whoami(token);
        const namespace = who.name || who.email || who.user || who.username;
        const code = pre.textContent || "";
        const filename = prompt("Filename to upload (e.g., app.py):", "app.py");
        if (!filename) return;
        const spaceSlug = prompt("Space slug (created if missing):", "deployed-from-browser");
        if (!spaceSlug) return;
        const sdk = prompt("Space SDK (gradio/streamlit/static):", "gradio") || "gradio";
        let requirements = "";
        if (sdk.toLowerCase() === "gradio") {
          requirements = prompt("requirements.txt content (optional):", "gradio>=4.26.0") || "";
        }

        await ensureSpace(token, namespace, spaceSlug, sdk, false);

        const files = {};
        files[filename] = code;
        if (requirements) files["requirements.txt"] = requirements;
        if (sdk.toLowerCase() === "gradio") {
          files["README.md"] = `---\ntitle: Browser Deploy\nsdk: gradio\napp_file: ${filename}\n---\n\nDeployed from a userscript.`;
        }

        await commitFiles(token, `${namespace}/${spaceSlug}`, files, "Deploy via userscript");
        const url = `https://huggingface.co/spaces/${namespace}/${spaceSlug}`;
        if (confirm("Open Space?\n" + url)) window.open(url, "_blank");
      });
      container.appendChild(btn);
    }
  }

  const observer = new MutationObserver(() => addButtons());
  observer.observe(document.documentElement, { childList: true, subtree: true });
  addButtons();
})();