Spaces:
Running
fix: agent_run param mismatch (send agent_name) + add GitHub push-update (3 inputs: repo name, token, username; --force-with-lease)
Browse filesFixes agent_run + adds GitHub push-update:
FIX: handle_agent_run param mismatch
- Backend declared 8 params (prompt, target_language, target_framework,
history_json, skills_json, search_enabled, image_url, agent_name)
but frontend POST body only sent 7 β Gradio raised
'needed: 8, got: 7'. Frontend now sends state.activeAgent as the
8th value, restoring agent_run for ALL prompts (the bug broke agent
mode entirely after the Custom Agents feature shipped).
FEAT: Push Update to GitHub (3 inputs only)
- New backend: code/tools/github.py::push_to_github(repo_name,
github_token, username, branch?, commit_message?, timeout?)
- Workflow: snapshot workspace β fresh git repo in temp dir β
git add -A + git commit β git push --force-with-lease
https://<user>:<token>@github.com/<owner>/<repo>.git <branch>.
- Falls back to plain push if --force-with-lease fails on a brand-new
empty repo (no refs to lease against).
- Token is scrubbed from error messages before being returned.
- New API: push_github(repo_name, github_token, username, ...)
- New UI: 'Push Update to GitHub' section in the Deploy tab with 3
required inputs (repo name, GitHub token, username) + an Advanced
<details> for branch/commit_message.
- Updated README.md and CLAUDE.md with new feature docs.
- CLAUDE.md +45 -0
- README.md +39 -0
- code/server/routes.py +46 -0
- code/tools/__init__.py +2 -0
- code/tools/github.py +224 -0
- index.html +188 -1
|
@@ -176,6 +176,7 @@ and any hard rules.
|
|
| 176 |
| `set_active_agent(name)` | Set/clear the active agent for subsequent prompts |
|
| 177 |
| `import_github(url, branch, subdir, target_subdir, depth, timeout)` | Clone a GitHub repo into the workspace (shallow, heavy dirs stripped) |
|
| 178 |
| `github_url_examples()` | Return accepted GitHub URL formats |
|
|
|
|
| 179 |
|
| 180 |
The `agent_run` endpoint also intercepts `/agent use|reset|delete|list` and
|
| 181 |
dispatches them directly to the agents module, bypassing the model entirely
|
|
@@ -220,6 +221,50 @@ The slash command (defined in `code/commands/builtins/github.md`) instructs
|
|
| 220 |
the agent to invoke `import_github_repo` via bash, then list the top-level
|
| 221 |
files and suggest next steps based on what was imported.
|
| 222 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
## Skills
|
| 224 |
|
| 225 |
| Skill | Description |
|
|
|
|
| 176 |
| `set_active_agent(name)` | Set/clear the active agent for subsequent prompts |
|
| 177 |
| `import_github(url, branch, subdir, target_subdir, depth, timeout)` | Clone a GitHub repo into the workspace (shallow, heavy dirs stripped) |
|
| 178 |
| `github_url_examples()` | Return accepted GitHub URL formats |
|
| 179 |
+
| `push_github(repo_name, github_token, username, branch?, commit_message?, timeout?)` | Snapshot workspace β commit β push to a GitHub repo |
|
| 180 |
|
| 181 |
The `agent_run` endpoint also intercepts `/agent use|reset|delete|list` and
|
| 182 |
dispatches them directly to the agents module, bypassing the model entirely
|
|
|
|
| 221 |
the agent to invoke `import_github_repo` via bash, then list the top-level
|
| 222 |
files and suggest next steps based on what was imported.
|
| 223 |
|
| 224 |
+
## GitHub Push
|
| 225 |
+
|
| 226 |
+
Push the current workspace back to a GitHub repo as a commit. Only 3 inputs
|
| 227 |
+
are required: repo name, GitHub token, username.
|
| 228 |
+
|
| 229 |
+
### How it works (`code/tools/github.py::push_to_github`)
|
| 230 |
+
|
| 231 |
+
1. Snapshot the workspace via `snapshot_workspace()` (returns a
|
| 232 |
+
`{relative_path: content}` dict).
|
| 233 |
+
2. Create a temp dir, `git init -b <branch>` inside it, write the snapshot
|
| 234 |
+
files in, `git add -A` + `git commit -m <message>`.
|
| 235 |
+
3. Build a push URL of the form
|
| 236 |
+
`https://<username>:<token>@github.com/<owner>/<repo>.git` and run
|
| 237 |
+
`git push --force-with-lease <url> <branch>`.
|
| 238 |
+
4. If `--force-with-lease` fails because the remote has no refs yet
|
| 239 |
+
(brand-new empty repo), retry with a plain `git push`.
|
| 240 |
+
5. Delete the temp dir. The token is never logged; error messages scrub it
|
| 241 |
+
before being returned.
|
| 242 |
+
|
| 243 |
+
### Why `--force-with-lease`
|
| 244 |
+
|
| 245 |
+
SoniCoder treats the workspace as the source of truth. `--force-with-lease`
|
| 246 |
+
replaces the remote tip with the workspace snapshot, but fails loudly (rather
|
| 247 |
+
than silently clobbering) if someone else pushed commits in the meantime β
|
| 248 |
+
because the temp repo has no reflog of the remote tip, the lease fails and
|
| 249 |
+
the user is told to pull first.
|
| 250 |
+
|
| 251 |
+
### UI
|
| 252 |
+
|
| 253 |
+
Located in the **Deploy** tab, in a "Push Update to GitHub" section below the
|
| 254 |
+
HuggingFace section. Only 3 fields are required: repo name, token, username.
|
| 255 |
+
An "Advanced" `<details>` exposes optional `branch` and `commit_message`
|
| 256 |
+
fields.
|
| 257 |
+
|
| 258 |
+
### Security
|
| 259 |
+
|
| 260 |
+
- Token is sent over HTTPS to the SoniCoder backend, used once for the push,
|
| 261 |
+
then dropped (not stored, not logged).
|
| 262 |
+
- Error messages are scrubbed to remove the token before being returned.
|
| 263 |
+
- The temp repo is deleted at the end of the call (context manager).
|
| 264 |
+
- The local SoniCoder workspace is never turned into a git repo; the
|
| 265 |
+
workspace's `.git` (if any, e.g. after an import β though imports strip
|
| 266 |
+
`.git`) is never read.
|
| 267 |
+
|
| 268 |
## Skills
|
| 269 |
|
| 270 |
| Skill | Description |
|
|
@@ -26,6 +26,7 @@ Inspired by [Claude Code](https://github.com/anthropics/claude-code), SoniCoder
|
|
| 26 |
- β‘ **Slash Commands** β `/commit`, `/review`, `/feature`, `/design`, `/explain`, `/test`, `/refactor`, `/skill`, `/agent`, `/github`, `/help`
|
| 27 |
- π§ **Custom Agents** β describe a specialized agent in natural language and the AI generates a full persona (system prompt + tool whitelist + auto-loaded skills + temperature + max iterations). Activate via `/agent use <name>` or the Agents panel. Built-ins: `code-reviewer`, `test-writer`.
|
| 28 |
- π₯ **GitHub Import** β paste any GitHub URL (or use `/github <url>`) to shallow-clone a repo into the workspace. Heavy dirs (`.git`, `node_modules`, `__pycache__`, `.venv`, `dist`) are stripped automatically. Supports `branch`, `subdir`, and `target_subdir` options.
|
|
|
|
| 29 |
- πͺ **Hooks System** β pre/post tool execution rules (block dangerous commands, warn on debug code/secrets)
|
| 30 |
- π **Sandboxed Workspace** β agent manipulates files in `./workspace/` (path-escape protected)
|
| 31 |
- β
**Todo Lists** β track multi-step tasks Claude Code-style
|
|
@@ -131,6 +132,44 @@ SoniCoder can clone any public GitHub repository into the sandboxed workspace so
|
|
| 131 |
|
| 132 |
**Security:** Only `github.com` URLs are accepted (HTTPS or SSH form). The clone happens in a temp directory and is then *copied* into the workspace β the upstream repo is never modified. Path-escape protection on `target_subdir` prevents writing outside the workspace.
|
| 133 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
### Custom Agents
|
| 135 |
|
| 136 |
Custom agents are AI-generated personas that layer on top of the base SoniCoder system prompt. Each agent defines:
|
|
|
|
| 26 |
- β‘ **Slash Commands** β `/commit`, `/review`, `/feature`, `/design`, `/explain`, `/test`, `/refactor`, `/skill`, `/agent`, `/github`, `/help`
|
| 27 |
- π§ **Custom Agents** β describe a specialized agent in natural language and the AI generates a full persona (system prompt + tool whitelist + auto-loaded skills + temperature + max iterations). Activate via `/agent use <name>` or the Agents panel. Built-ins: `code-reviewer`, `test-writer`.
|
| 28 |
- π₯ **GitHub Import** β paste any GitHub URL (or use `/github <url>`) to shallow-clone a repo into the workspace. Heavy dirs (`.git`, `node_modules`, `__pycache__`, `.venv`, `dist`) are stripped automatically. Supports `branch`, `subdir`, and `target_subdir` options.
|
| 29 |
+
- π¦ **GitHub Push** β push the current workspace to any GitHub repo with just 3 inputs: repo name, GitHub API token, and username. Uses `--force-with-lease` so the workspace is the source of truth. Available in the Deploy tab.
|
| 30 |
- πͺ **Hooks System** β pre/post tool execution rules (block dangerous commands, warn on debug code/secrets)
|
| 31 |
- π **Sandboxed Workspace** β agent manipulates files in `./workspace/` (path-escape protected)
|
| 32 |
- β
**Todo Lists** β track multi-step tasks Claude Code-style
|
|
|
|
| 132 |
|
| 133 |
**Security:** Only `github.com` URLs are accepted (HTTPS or SSH form). The clone happens in a temp directory and is then *copied* into the workspace β the upstream repo is never modified. Path-escape protection on `target_subdir` prevents writing outside the workspace.
|
| 134 |
|
| 135 |
+
### GitHub Push (Update a GitHub repo)
|
| 136 |
+
|
| 137 |
+
Push the current SoniCoder workspace back to a GitHub repo as a commit. Designed to be minimal β only **3 required inputs**:
|
| 138 |
+
|
| 139 |
+
1. **Repository name** β either `my-app` (combined with your username) or `username/my-app`.
|
| 140 |
+
2. **GitHub API token** β a Personal Access Token (PAT) with `repo` scope. [Create one here](https://github.com/settings/tokens/new?scopes=repo&description=SoniCoder).
|
| 141 |
+
3. **Username** β the GitHub user (or org) that owns the repo and matches the token.
|
| 142 |
+
|
| 143 |
+
**How to use:**
|
| 144 |
+
|
| 145 |
+
1. Open the **Deploy** tab.
|
| 146 |
+
2. Scroll to the **"Push Update to GitHub"** section (below the HuggingFace section).
|
| 147 |
+
3. Fill in the 3 required fields (optionally expand "Advanced" to set `branch` or `commit message`).
|
| 148 |
+
4. Click **π¦ Push to GitHub**. A confirmation dialog shows the target repo + branch.
|
| 149 |
+
5. On success, the status box shows the commit SHA, commit URL, and repo URL.
|
| 150 |
+
|
| 151 |
+
**How it works (under the hood):**
|
| 152 |
+
|
| 153 |
+
1. The workspace is snapshotted (via `snapshot_workspace()` β same function used for HuggingFace deploy).
|
| 154 |
+
2. A fresh git repo is created in a temp dir; the snapshot files are written in.
|
| 155 |
+
3. `git init -b <branch>` β `git add -A` β `git commit -m <message>`.
|
| 156 |
+
4. `git push --force-with-lease https://<username>:<token>@github.com/<owner>/<repo>.git <branch>`.
|
| 157 |
+
5. If `--force-with-lease` fails because the remote has no refs yet (brand-new empty repo), it retries with a plain `git push`.
|
| 158 |
+
6. The temp dir is deleted. The token is never logged; error messages scrub it before being returned to the UI.
|
| 159 |
+
|
| 160 |
+
**API endpoint:**
|
| 161 |
+
|
| 162 |
+
| Endpoint | Description |
|
| 163 |
+
|----------|-------------|
|
| 164 |
+
| `push_github(repo_name, github_token, username, branch?, commit_message?, timeout?)` | Snapshot workspace β commit β push to GitHub |
|
| 165 |
+
|
| 166 |
+
**Security notes:**
|
| 167 |
+
|
| 168 |
+
- The token is sent over HTTPS to the SoniCoder backend, used once for the push, then dropped (not stored, not logged).
|
| 169 |
+
- Error messages are scrubbed to remove the token before being returned to the frontend.
|
| 170 |
+
- `--force-with-lease` is used instead of `--force` so the push fails loudly if the remote moved (rather than silently overwriting someone else's commits). For a brand-new empty repo, it falls back to a plain push.
|
| 171 |
+
- The push happens from a temp dir β your local SoniCoder workspace is never turned into a git repo, and the workspace's `.git` (if any) is never read.
|
| 172 |
+
|
| 173 |
### Custom Agents
|
| 174 |
|
| 175 |
Custom agents are AI-generated personas that layer on top of the base SoniCoder system prompt. Each agent defines:
|
|
@@ -23,6 +23,7 @@ Defines all HTTP and API endpoints:
|
|
| 23 |
- API todo_write β update todo list
|
| 24 |
- API import_github β clone a GitHub repo into the workspace
|
| 25 |
- API github_url_examples β return accepted GitHub URL formats
|
|
|
|
| 26 |
"""
|
| 27 |
|
| 28 |
from __future__ import annotations
|
|
@@ -1185,3 +1186,48 @@ def handle_github_url_examples() -> str:
|
|
| 1185 |
from code.tools.github import list_github_url_examples
|
| 1186 |
result = list_github_url_examples()
|
| 1187 |
yield json.dumps(result, default=str)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
- API todo_write β update todo list
|
| 24 |
- API import_github β clone a GitHub repo into the workspace
|
| 25 |
- API github_url_examples β return accepted GitHub URL formats
|
| 26 |
+
- API push_github β push the current workspace to a GitHub repo
|
| 27 |
"""
|
| 28 |
|
| 29 |
from __future__ import annotations
|
|
|
|
| 1186 |
from code.tools.github import list_github_url_examples
|
| 1187 |
result = list_github_url_examples()
|
| 1188 |
yield json.dumps(result, default=str)
|
| 1189 |
+
|
| 1190 |
+
|
| 1191 |
+
@app.api(name="push_github", concurrency_limit=1)
|
| 1192 |
+
def handle_push_github(
|
| 1193 |
+
repo_name: str,
|
| 1194 |
+
github_token: str,
|
| 1195 |
+
username: str,
|
| 1196 |
+
branch: str = "main",
|
| 1197 |
+
commit_message: str = "",
|
| 1198 |
+
timeout: str = "120",
|
| 1199 |
+
) -> str:
|
| 1200 |
+
"""Push the current workspace to a GitHub repo.
|
| 1201 |
+
|
| 1202 |
+
Requires only 3 user inputs (repo_name, github_token, username) plus
|
| 1203 |
+
optional branch / commit_message / timeout. The workspace is snapshotted
|
| 1204 |
+
(via `snapshot_workspace`), written into a fresh git repo in a temp dir,
|
| 1205 |
+
committed, and pushed to `https://github.com/<username>/<repo_name>.git`
|
| 1206 |
+
using HTTPS basic auth with the token.
|
| 1207 |
+
|
| 1208 |
+
The push uses `--force-with-lease` so it replaces the remote tip with the
|
| 1209 |
+
SoniCoder workspace contents. If the remote doesn't exist yet (no refs
|
| 1210 |
+
to lease against), it retries with a plain push.
|
| 1211 |
+
|
| 1212 |
+
Yields
|
| 1213 |
+
------
|
| 1214 |
+
JSON dict with keys: success, message, repo_full_name, branch,
|
| 1215 |
+
commit_sha, commit_url, repo_url, files_pushed, error (on failure).
|
| 1216 |
+
"""
|
| 1217 |
+
from code.tools.github import push_to_github
|
| 1218 |
+
|
| 1219 |
+
try:
|
| 1220 |
+
timeout_int = int(timeout) if str(timeout).strip() else 120
|
| 1221 |
+
timeout_int = max(10, min(600, timeout_int))
|
| 1222 |
+
except (ValueError, TypeError):
|
| 1223 |
+
timeout_int = 120
|
| 1224 |
+
|
| 1225 |
+
result = push_to_github(
|
| 1226 |
+
repo_name=repo_name,
|
| 1227 |
+
github_token=github_token,
|
| 1228 |
+
username=username,
|
| 1229 |
+
branch=branch or "main",
|
| 1230 |
+
commit_message=commit_message or "",
|
| 1231 |
+
timeout=timeout_int,
|
| 1232 |
+
)
|
| 1233 |
+
yield json.dumps(result, default=str)
|
|
@@ -26,6 +26,7 @@ from code.tools.todos import (
|
|
| 26 |
from code.tools.github import (
|
| 27 |
import_github_repo,
|
| 28 |
list_github_url_examples,
|
|
|
|
| 29 |
)
|
| 30 |
|
| 31 |
__all__ = [
|
|
@@ -42,4 +43,5 @@ __all__ = [
|
|
| 42 |
"todo_update",
|
| 43 |
"import_github_repo",
|
| 44 |
"list_github_url_examples",
|
|
|
|
| 45 |
]
|
|
|
|
| 26 |
from code.tools.github import (
|
| 27 |
import_github_repo,
|
| 28 |
list_github_url_examples,
|
| 29 |
+
push_to_github,
|
| 30 |
)
|
| 31 |
|
| 32 |
__all__ = [
|
|
|
|
| 43 |
"todo_update",
|
| 44 |
"import_github_repo",
|
| 45 |
"list_github_url_examples",
|
| 46 |
+
"push_to_github",
|
| 47 |
]
|
|
@@ -394,3 +394,227 @@ def list_github_url_examples() -> dict[str, Any]:
|
|
| 394 |
"If a /tree/<branch>/<subdir> path is included, only that subdir is imported.",
|
| 395 |
],
|
| 396 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 394 |
"If a /tree/<branch>/<subdir> path is included, only that subdir is imported.",
|
| 395 |
],
|
| 396 |
}
|
| 397 |
+
|
| 398 |
+
|
| 399 |
+
# βββ Push workspace to GitHub ββββββββββββββββββββββββββββββββββββββββββββ
|
| 400 |
+
|
| 401 |
+
|
| 402 |
+
def push_to_github(
|
| 403 |
+
repo_name: str,
|
| 404 |
+
github_token: str,
|
| 405 |
+
username: str,
|
| 406 |
+
branch: str = "main",
|
| 407 |
+
commit_message: str = "",
|
| 408 |
+
timeout: int = 120,
|
| 409 |
+
) -> dict[str, Any]:
|
| 410 |
+
"""Push the current SoniCoder workspace to a GitHub repo.
|
| 411 |
+
|
| 412 |
+
Workflow:
|
| 413 |
+
1. Snapshot the workspace (call `snapshot_workspace()`).
|
| 414 |
+
2. Create a fresh git repo in a temp dir, copy the files in.
|
| 415 |
+
3. `git commit -m <message>`.
|
| 416 |
+
4. `git push https://<token>@github.com/<username>/<repo>.git <branch>`.
|
| 417 |
+
|
| 418 |
+
If the remote repo already exists with history, the push uses
|
| 419 |
+
`--force-with-lease` so the new commit replaces the remote tip. This
|
| 420 |
+
matches the SoniCoder mental model: "the workspace IS the source of
|
| 421 |
+
truth; overwrite whatever's on GitHub with my latest".
|
| 422 |
+
|
| 423 |
+
Parameters
|
| 424 |
+
----------
|
| 425 |
+
repo_name : str
|
| 426 |
+
Repo name. Either "repo" (combined with `username` as
|
| 427 |
+
`username/repo`) or "username/repo" (username arg then ignored).
|
| 428 |
+
github_token : str
|
| 429 |
+
GitHub Personal Access Token (PAT) with `repo` scope.
|
| 430 |
+
username : str
|
| 431 |
+
GitHub username (or org) to push as. Required.
|
| 432 |
+
branch : str
|
| 433 |
+
Target branch (default "main"). Will be created on push if missing.
|
| 434 |
+
commit_message : str
|
| 435 |
+
Commit message. Defaults to a timestamped message.
|
| 436 |
+
timeout : int
|
| 437 |
+
Per-git-command timeout in seconds (default 120).
|
| 438 |
+
|
| 439 |
+
Returns
|
| 440 |
+
-------
|
| 441 |
+
dict with keys:
|
| 442 |
+
success, message, repo_full_name, branch, commit_sha,
|
| 443 |
+
commit_url, repo_url, files_pushed, error (on failure)
|
| 444 |
+
"""
|
| 445 |
+
import datetime
|
| 446 |
+
import subprocess
|
| 447 |
+
|
| 448 |
+
# ββ Validate inputs ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 449 |
+
repo_name = (repo_name or "").strip()
|
| 450 |
+
github_token = (github_token or "").strip()
|
| 451 |
+
username = (username or "").strip()
|
| 452 |
+
branch = (branch or "main").strip() or "main"
|
| 453 |
+
if not commit_message:
|
| 454 |
+
commit_message = (
|
| 455 |
+
f"Update from SoniCoder at "
|
| 456 |
+
f"{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
|
| 457 |
+
)
|
| 458 |
+
|
| 459 |
+
if not repo_name:
|
| 460 |
+
return _gh_err("Repo name is required.")
|
| 461 |
+
if not github_token:
|
| 462 |
+
return _gh_err("GitHub token is required.")
|
| 463 |
+
if not username:
|
| 464 |
+
return _gh_err("Username (or org) is required.")
|
| 465 |
+
|
| 466 |
+
# Normalize repo_name into owner/repo
|
| 467 |
+
if "/" in repo_name:
|
| 468 |
+
owner, name = repo_name.split("/", 1)
|
| 469 |
+
owner = owner.strip() or username
|
| 470 |
+
name = name.strip()
|
| 471 |
+
else:
|
| 472 |
+
owner = username
|
| 473 |
+
name = repo_name
|
| 474 |
+
|
| 475 |
+
if not name:
|
| 476 |
+
return _gh_err(f"Invalid repo_name: {repo_name!r}")
|
| 477 |
+
|
| 478 |
+
repo_full_name = f"{owner}/{name}"
|
| 479 |
+
|
| 480 |
+
if not _git_available():
|
| 481 |
+
return _gh_err("`git` is not installed in this environment. Cannot push.")
|
| 482 |
+
|
| 483 |
+
# ββ Snapshot workspace βββββββββββββββββββββββββββββββββββββββββββ
|
| 484 |
+
from code.tools.fs import snapshot_workspace
|
| 485 |
+
|
| 486 |
+
files = snapshot_workspace()
|
| 487 |
+
if not files:
|
| 488 |
+
return _gh_err(
|
| 489 |
+
"Workspace is empty. Generate or import some code first."
|
| 490 |
+
)
|
| 491 |
+
|
| 492 |
+
# ββ Set up a fresh git repo in temp dir ββββββββββββββββββββββββββ
|
| 493 |
+
with tempfile.TemporaryDirectory(prefix="sonicoder_gh_push_") as tmp:
|
| 494 |
+
repo_dir = os.path.join(tmp, name)
|
| 495 |
+
os.makedirs(repo_dir, exist_ok=True)
|
| 496 |
+
|
| 497 |
+
def _run_git(args: list[str], cwd: str = repo_dir) -> tuple[int, str, str]:
|
| 498 |
+
try:
|
| 499 |
+
proc = subprocess.run(
|
| 500 |
+
["git", *args],
|
| 501 |
+
cwd=cwd,
|
| 502 |
+
capture_output=True,
|
| 503 |
+
text=True,
|
| 504 |
+
timeout=timeout,
|
| 505 |
+
)
|
| 506 |
+
return proc.returncode, proc.stdout, proc.stderr
|
| 507 |
+
except subprocess.TimeoutExpired:
|
| 508 |
+
return 124, "", f"git {' '.join(args)} timed out after {timeout}s"
|
| 509 |
+
|
| 510 |
+
# Init repo
|
| 511 |
+
rc, _, err = _run_git(["init", "-b", branch])
|
| 512 |
+
if rc != 0:
|
| 513 |
+
# Older git doesn't support -b; fall back
|
| 514 |
+
rc, _, err = _run_git(["init"])
|
| 515 |
+
if rc != 0:
|
| 516 |
+
return _gh_err(f"git init failed: {err}")
|
| 517 |
+
# Then checkout/create the branch
|
| 518 |
+
_run_git(["checkout", "-b", branch])
|
| 519 |
+
|
| 520 |
+
# Set committer identity (required for commit)
|
| 521 |
+
_run_git(["config", "user.email", f"{username}@users.noreply.github.com"])
|
| 522 |
+
_run_git(["config", "user.name", username])
|
| 523 |
+
|
| 524 |
+
# Write all snapshot files into the repo dir
|
| 525 |
+
for rel_path, content in files.items():
|
| 526 |
+
# Safety: skip absolute paths and parent-escape attempts
|
| 527 |
+
if os.path.isabs(rel_path) or rel_path.startswith(".."):
|
| 528 |
+
continue
|
| 529 |
+
target = os.path.join(repo_dir, rel_path)
|
| 530 |
+
os.makedirs(os.path.dirname(target) or repo_dir, exist_ok=True)
|
| 531 |
+
try:
|
| 532 |
+
with open(target, "w", encoding="utf-8") as f:
|
| 533 |
+
f.write(content)
|
| 534 |
+
except (OSError, UnicodeEncodeError):
|
| 535 |
+
continue
|
| 536 |
+
|
| 537 |
+
# Stage everything
|
| 538 |
+
rc, _, err = _run_git(["add", "-A"])
|
| 539 |
+
if rc != 0:
|
| 540 |
+
return _gh_err(f"git add failed: {err}")
|
| 541 |
+
|
| 542 |
+
# Check if there's anything to commit
|
| 543 |
+
rc, out, _ = _run_git(["status", "--porcelain"])
|
| 544 |
+
has_changes = bool(out.strip())
|
| 545 |
+
if not has_changes:
|
| 546 |
+
return {
|
| 547 |
+
"success": True,
|
| 548 |
+
"message": "No changes to commit (workspace matches last commit).",
|
| 549 |
+
"repo_full_name": repo_full_name,
|
| 550 |
+
"branch": branch,
|
| 551 |
+
"commit_sha": None,
|
| 552 |
+
"commit_url": None,
|
| 553 |
+
"repo_url": f"https://github.com/{repo_full_name}",
|
| 554 |
+
"files_pushed": 0,
|
| 555 |
+
}
|
| 556 |
+
|
| 557 |
+
# Commit
|
| 558 |
+
rc, out, err = _run_git(["commit", "-m", commit_message])
|
| 559 |
+
if rc != 0:
|
| 560 |
+
return _gh_err(f"git commit failed: {err}")
|
| 561 |
+
|
| 562 |
+
# Get commit SHA
|
| 563 |
+
rc, sha, _ = _run_git(["rev-parse", "HEAD"])
|
| 564 |
+
commit_sha = sha.strip()
|
| 565 |
+
|
| 566 |
+
# Build the push URL with token embedded (HTTPS basic auth).
|
| 567 |
+
# Token is URL-safe enough for typical PATs (alphanumeric).
|
| 568 |
+
push_url = (
|
| 569 |
+
f"https://{username}:{github_token}@github.com/{repo_full_name}.git"
|
| 570 |
+
)
|
| 571 |
+
|
| 572 |
+
# Push with --force-with-lease so we don't silently overwrite
|
| 573 |
+
# someone else's commits if the remote moved.
|
| 574 |
+
rc, out, err = _run_git(
|
| 575 |
+
["push", "--force-with-lease", push_url, branch]
|
| 576 |
+
)
|
| 577 |
+
if rc != 0:
|
| 578 |
+
# If --force-with-lease fails because the remote doesn't exist
|
| 579 |
+
# yet (no refs to lease against), retry with a plain push.
|
| 580 |
+
if "no matching references" in err.lower() or "delete" in err.lower():
|
| 581 |
+
rc, out, err = _run_git(["push", push_url, branch])
|
| 582 |
+
if rc != 0:
|
| 583 |
+
# Don't leak the token in the error
|
| 584 |
+
safe_err = err.replace(github_token, "***").replace(push_url, "https://github.com/{repo_full_name}.git")
|
| 585 |
+
return _gh_err(
|
| 586 |
+
f"git push failed: {safe_err[:400]}. "
|
| 587 |
+
"Check that the token has `repo` scope and the repo exists.",
|
| 588 |
+
repo_full_name=repo_full_name,
|
| 589 |
+
branch=branch,
|
| 590 |
+
commit_sha=commit_sha,
|
| 591 |
+
)
|
| 592 |
+
|
| 593 |
+
# Count files pushed
|
| 594 |
+
rc, count_out, _ = _run_git(["ls-files"])
|
| 595 |
+
files_pushed = len([l for l in count_out.splitlines() if l.strip()])
|
| 596 |
+
|
| 597 |
+
return {
|
| 598 |
+
"success": True,
|
| 599 |
+
"message": (
|
| 600 |
+
f"Pushed {files_pushed} file(s) to {repo_full_name} "
|
| 601 |
+
f"on branch `{branch}` (commit {commit_sha[:8]})."
|
| 602 |
+
),
|
| 603 |
+
"repo_full_name": repo_full_name,
|
| 604 |
+
"branch": branch,
|
| 605 |
+
"commit_sha": commit_sha,
|
| 606 |
+
"commit_url": f"https://github.com/{repo_full_name}/commit/{commit_sha}",
|
| 607 |
+
"repo_url": f"https://github.com/{repo_full_name}",
|
| 608 |
+
"files_pushed": files_pushed,
|
| 609 |
+
}
|
| 610 |
+
|
| 611 |
+
|
| 612 |
+
def _gh_err(message: str, **extras: Any) -> dict[str, Any]:
|
| 613 |
+
"""Build a standard error response for push_to_github."""
|
| 614 |
+
result: dict[str, Any] = {
|
| 615 |
+
"success": False,
|
| 616 |
+
"message": message,
|
| 617 |
+
"error": message,
|
| 618 |
+
}
|
| 619 |
+
result.update(extras)
|
| 620 |
+
return result
|
|
@@ -1418,6 +1418,55 @@ body.hide-thinking .think-block { display: none; }
|
|
| 1418 |
<div class="deploy-status" id="deploy-status"></div>
|
| 1419 |
<div class="project-files" id="project-files"></div>
|
| 1420 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1421 |
</div>
|
| 1422 |
</div>
|
| 1423 |
</div>
|
|
@@ -2894,7 +2943,16 @@ async function sendMessageAgent(prompt) {
|
|
| 2894 |
method: 'POST',
|
| 2895 |
headers: { 'Content-Type': 'application/json' },
|
| 2896 |
body: JSON.stringify({
|
| 2897 |
-
data: [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2898 |
})
|
| 2899 |
});
|
| 2900 |
|
|
@@ -3630,6 +3688,135 @@ async function pushToHuggingFace() {
|
|
| 3630 |
btn.disabled = false;
|
| 3631 |
}
|
| 3632 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3633 |
</script>
|
| 3634 |
</body>
|
| 3635 |
</html>
|
|
|
|
| 1418 |
<div class="deploy-status" id="deploy-status"></div>
|
| 1419 |
<div class="project-files" id="project-files"></div>
|
| 1420 |
</div>
|
| 1421 |
+
|
| 1422 |
+
<!-- βββ GitHub Push Section ββββββββββββββββββββββββββββββββββ -->
|
| 1423 |
+
<div class="deploy-section" style="margin-top:14px;">
|
| 1424 |
+
<div class="deploy-title" style="color:var(--cyan, #00d4ff);text-shadow:0 0 6px rgba(0,212,255,0.4);">📦 Push Update to GitHub</div>
|
| 1425 |
+
<div class="deploy-hint" style="margin-bottom:10px;">
|
| 1426 |
+
Snapshots the current workspace and pushes it as a commit to a
|
| 1427 |
+
GitHub repo. Uses <code>--force-with-lease</code> so the latest
|
| 1428 |
+
workspace contents overwrite the remote tip.
|
| 1429 |
+
</div>
|
| 1430 |
+
|
| 1431 |
+
<!-- Only 3 required inputs -->
|
| 1432 |
+
<div class="deploy-field">
|
| 1433 |
+
<label for="gh-repo-name">1. Repository Name</label>
|
| 1434 |
+
<input type="text" id="gh-repo-name" placeholder="my-app (or username/my-app)" autocomplete="off">
|
| 1435 |
+
<div class="deploy-hint">If you omit the owner prefix, your username is used.</div>
|
| 1436 |
+
</div>
|
| 1437 |
+
|
| 1438 |
+
<div class="deploy-field">
|
| 1439 |
+
<label for="gh-token">2. GitHub API Token</label>
|
| 1440 |
+
<input type="password" id="gh-token" placeholder="ghp_xxxxxxxxxxxxxxxxxxxxx" autocomplete="off">
|
| 1441 |
+
<div class="deploy-hint">
|
| 1442 |
+
Needs <code>repo</code> scope.
|
| 1443 |
+
<a href="https://github.com/settings/tokens/new?scopes=repo&description=SoniCoder" target="_blank" rel="noopener">Create a token ↗</a>
|
| 1444 |
+
</div>
|
| 1445 |
+
</div>
|
| 1446 |
+
|
| 1447 |
+
<div class="deploy-field">
|
| 1448 |
+
<label for="gh-username">3. Username (for push)</label>
|
| 1449 |
+
<input type="text" id="gh-username" placeholder="your-github-username" autocomplete="off">
|
| 1450 |
+
<div class="deploy-hint">The GitHub user (or org) that owns the repo and matches the token.</div>
|
| 1451 |
+
</div>
|
| 1452 |
+
|
| 1453 |
+
<!-- Optional advanced settings (collapsed) -->
|
| 1454 |
+
<details style="margin-top:8px;">
|
| 1455 |
+
<summary style="font-size:10px;color:var(--gray-dim);cursor:pointer;letter-spacing:1px;text-transform:uppercase;">Advanced (optional)</summary>
|
| 1456 |
+
<div class="deploy-field" style="margin-top:8px;">
|
| 1457 |
+
<label for="gh-branch">Branch</label>
|
| 1458 |
+
<input type="text" id="gh-branch" placeholder="main" value="main" autocomplete="off">
|
| 1459 |
+
</div>
|
| 1460 |
+
<div class="deploy-field">
|
| 1461 |
+
<label for="gh-commit-msg">Commit message</label>
|
| 1462 |
+
<input type="text" id="gh-commit-msg" placeholder="Update from SoniCoder" autocomplete="off">
|
| 1463 |
+
<div class="deploy-hint">If empty, a timestamped message is used.</div>
|
| 1464 |
+
</div>
|
| 1465 |
+
</details>
|
| 1466 |
+
|
| 1467 |
+
<button id="btn-push-github" onclick="pushToGithub()" style="margin-top:10px;">📦 Push to GitHub</button>
|
| 1468 |
+
<div class="deploy-status" id="github-deploy-status"></div>
|
| 1469 |
+
</div>
|
| 1470 |
</div>
|
| 1471 |
</div>
|
| 1472 |
</div>
|
|
|
|
| 2943 |
method: 'POST',
|
| 2944 |
headers: { 'Content-Type': 'application/json' },
|
| 2945 |
body: JSON.stringify({
|
| 2946 |
+
data: [
|
| 2947 |
+
prompt,
|
| 2948 |
+
state.targetLanguage,
|
| 2949 |
+
framework,
|
| 2950 |
+
historyJSON,
|
| 2951 |
+
skillsJSON,
|
| 2952 |
+
state.searchEnabled ? 'true' : 'false',
|
| 2953 |
+
state.uploadedImageFileUrl || '',
|
| 2954 |
+
state.activeAgent || ''
|
| 2955 |
+
]
|
| 2956 |
})
|
| 2957 |
});
|
| 2958 |
|
|
|
|
| 3688 |
btn.disabled = false;
|
| 3689 |
}
|
| 3690 |
}
|
| 3691 |
+
|
| 3692 |
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 3693 |
+
// PUSH TO GITHUB (3 inputs: repo name, token, username)
|
| 3694 |
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 3695 |
+
|
| 3696 |
+
async function pushToGithub() {
|
| 3697 |
+
const repoNameEl = document.getElementById('gh-repo-name');
|
| 3698 |
+
const tokenEl = document.getElementById('gh-token');
|
| 3699 |
+
const usernameEl = document.getElementById('gh-username');
|
| 3700 |
+
const branchEl = document.getElementById('gh-branch');
|
| 3701 |
+
const msgEl = document.getElementById('gh-commit-msg');
|
| 3702 |
+
const statusEl = document.getElementById('github-deploy-status');
|
| 3703 |
+
const btn = document.getElementById('btn-push-github');
|
| 3704 |
+
|
| 3705 |
+
if (!repoNameEl || !tokenEl || !usernameEl || !statusEl || !btn) return;
|
| 3706 |
+
|
| 3707 |
+
const repoName = repoNameEl.value.trim();
|
| 3708 |
+
const token = tokenEl.value.trim();
|
| 3709 |
+
const username = usernameEl.value.trim();
|
| 3710 |
+
const branch = (branchEl && branchEl.value.trim()) || 'main';
|
| 3711 |
+
const commitMsg = (msgEl && msgEl.value.trim()) || '';
|
| 3712 |
+
|
| 3713 |
+
// ββ Validate the 3 required inputs βββββββββββββββββββββββββββββββ
|
| 3714 |
+
if (!repoName) {
|
| 3715 |
+
statusEl.className = 'deploy-status error';
|
| 3716 |
+
statusEl.textContent = 'Please enter the repository name.';
|
| 3717 |
+
statusEl.style.display = 'block';
|
| 3718 |
+
repoNameEl.focus();
|
| 3719 |
+
return;
|
| 3720 |
+
}
|
| 3721 |
+
if (!token) {
|
| 3722 |
+
statusEl.className = 'deploy-status error';
|
| 3723 |
+
statusEl.textContent = 'Please enter your GitHub API token.';
|
| 3724 |
+
statusEl.style.display = 'block';
|
| 3725 |
+
tokenEl.focus();
|
| 3726 |
+
return;
|
| 3727 |
+
}
|
| 3728 |
+
if (!username) {
|
| 3729 |
+
statusEl.className = 'deploy-status error';
|
| 3730 |
+
statusEl.textContent = 'Please enter your GitHub username.';
|
| 3731 |
+
statusEl.style.display = 'block';
|
| 3732 |
+
usernameEl.focus();
|
| 3733 |
+
return;
|
| 3734 |
+
}
|
| 3735 |
+
|
| 3736 |
+
// Quick client-side token sanity check (server validates too)
|
| 3737 |
+
if (token.length < 20) {
|
| 3738 |
+
statusEl.className = 'deploy-status error';
|
| 3739 |
+
statusEl.textContent = 'Token looks too short β expected a GitHub PAT (starts with ghp_, github_pat_, etc.).';
|
| 3740 |
+
statusEl.style.display = 'block';
|
| 3741 |
+
return;
|
| 3742 |
+
}
|
| 3743 |
+
|
| 3744 |
+
// Confirm push β it overwrites the remote tip
|
| 3745 |
+
const fullRepo = repoName.includes('/') ? repoName : `${username}/${repoName}`;
|
| 3746 |
+
if (!confirm(
|
| 3747 |
+
`Push workspace to github.com/${fullRepo} on branch "${branch}"?\n\n` +
|
| 3748 |
+
`This will overwrite the remote tip (--force-with-lease).\n` +
|
| 3749 |
+
`Token will be sent to the SoniCoder backend only (not stored).`
|
| 3750 |
+
)) {
|
| 3751 |
+
return;
|
| 3752 |
+
}
|
| 3753 |
+
|
| 3754 |
+
// ββ Disable + show working state βββββββββββββββββββββββββββββββββ
|
| 3755 |
+
btn.disabled = true;
|
| 3756 |
+
btn.textContent = 'β³ Pushing...';
|
| 3757 |
+
statusEl.className = 'deploy-status working';
|
| 3758 |
+
statusEl.textContent = `Pushing to github.com/${fullRepo} on "${branch}"... (this may take 10-60 seconds)`;
|
| 3759 |
+
statusEl.style.display = 'block';
|
| 3760 |
+
|
| 3761 |
+
try {
|
| 3762 |
+
const resp = await fetch('/gradio_api/call/push_github', {
|
| 3763 |
+
method: 'POST',
|
| 3764 |
+
headers: { 'Content-Type': 'application/json' },
|
| 3765 |
+
body: JSON.stringify({
|
| 3766 |
+
data: [repoName, token, username, branch, commitMsg, '180']
|
| 3767 |
+
})
|
| 3768 |
+
});
|
| 3769 |
+
|
| 3770 |
+
if (!resp.ok) throw new Error(`API error: ${resp.status} ${resp.statusText}`);
|
| 3771 |
+
|
| 3772 |
+
const { event_id } = await resp.json();
|
| 3773 |
+
const eventSource = new EventSource(`/gradio_api/call/push_github/${event_id}`);
|
| 3774 |
+
|
| 3775 |
+
eventSource.addEventListener('complete', (e) => {
|
| 3776 |
+
try {
|
| 3777 |
+
const dataArray = JSON.parse(e.data);
|
| 3778 |
+
const result = JSON.parse(dataArray[0]);
|
| 3779 |
+
|
| 3780 |
+
if (result.success) {
|
| 3781 |
+
statusEl.className = 'deploy-status success';
|
| 3782 |
+
const sha = result.commit_sha ? ` (commit ${result.commit_sha.slice(0,8)})` : '';
|
| 3783 |
+
const commitLink = result.commit_url
|
| 3784 |
+
? `<br><a href="${result.commit_url}" target="_blank" rel="noopener">view commit ${result.commit_sha ? result.commit_sha.slice(0,8) : ''} ↗</a>`
|
| 3785 |
+
: '';
|
| 3786 |
+
const repoLink = result.repo_url
|
| 3787 |
+
? `<br><a href="${result.repo_url}" target="_blank" rel="noopener">${result.repo_url} ↗</a>`
|
| 3788 |
+
: '';
|
| 3789 |
+
statusEl.innerHTML =
|
| 3790 |
+
`β ${result.message}${sha}${commitLink}${repoLink}`;
|
| 3791 |
+
addSystemMessage(`π¦ Pushed ${result.files_pushed} file(s) to ${result.repo_full_name}.`);
|
| 3792 |
+
} else {
|
| 3793 |
+
statusEl.className = 'deploy-status error';
|
| 3794 |
+
statusEl.textContent = `β ${result.message || result.error || 'Push failed.'}`;
|
| 3795 |
+
}
|
| 3796 |
+
} catch (err) {
|
| 3797 |
+
statusEl.className = 'deploy-status error';
|
| 3798 |
+
statusEl.textContent = `Parse error: ${err.message}`;
|
| 3799 |
+
}
|
| 3800 |
+
eventSource.close();
|
| 3801 |
+
btn.disabled = false;
|
| 3802 |
+
btn.textContent = 'π¦ Push to GitHub';
|
| 3803 |
+
});
|
| 3804 |
+
|
| 3805 |
+
eventSource.addEventListener('error', (e) => {
|
| 3806 |
+
statusEl.className = 'deploy-status error';
|
| 3807 |
+
statusEl.textContent = `Push failed: ${e.data || 'Network error. Check console.'}`;
|
| 3808 |
+
eventSource.close();
|
| 3809 |
+
btn.disabled = false;
|
| 3810 |
+
btn.textContent = 'π¦ Push to GitHub';
|
| 3811 |
+
});
|
| 3812 |
+
|
| 3813 |
+
} catch (err) {
|
| 3814 |
+
statusEl.className = 'deploy-status error';
|
| 3815 |
+
statusEl.textContent = `Push failed: ${err.message}`;
|
| 3816 |
+
btn.disabled = false;
|
| 3817 |
+
btn.textContent = 'π¦ Push to GitHub';
|
| 3818 |
+
}
|
| 3819 |
+
}
|
| 3820 |
</script>
|
| 3821 |
</body>
|
| 3822 |
</html>
|