Skip to content

Commit 6b9d9c0

Browse files
committed
Add Gitea forge support
1 parent 9689931 commit 6b9d9c0

40 files changed

Lines changed: 1750 additions & 69 deletions

sidecar/scripts/stage-vendor.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Stage claude-code + codex + gh + glab into `sidecar/dist/vendor/`
1+
// Stage claude-code + codex + gh + glab + tea into `sidecar/dist/vendor/`
22
// for Tauri to ship as bundle resources. macOS host only.
33
//
44
// Cross-arch staging: in CI the host is always Apple Silicon (macos-26
@@ -34,6 +34,7 @@ const BUNDLE_CACHE = join(SIDECAR_ROOT, ".bundle-cache");
3434
// Bumping any version: update SHA256 below + wipe sidecar/.bundle-cache.
3535
// gh: github.com/cli/cli/releases/download/v$VER/gh_${VER}_checksums.txt
3636
// glab: gitlab.com/gitlab-org/cli/-/releases/v$VER/downloads/checksums.txt
37+
// tea: gitea.com/gitea/tea/releases/download/v$VER/checksums.txt
3738
// codex: shasum -a 256 of the npm tarball at
3839
// registry.npmjs.org/@openai/codex/-/codex-$VER-darwin-{arm64,x64}.tgz
3940
// claude-code: shasum -a 256 of the npm tarballs at
@@ -51,6 +52,12 @@ const GLAB_SHA256 = {
5152
amd64: "79d1a4f933919689c5fb7774feb1dd08f30b9c896dff4283b4a7387689ee0531",
5253
} as const;
5354

55+
const TEA_VERSION = "0.14.1";
56+
const TEA_SHA256 = {
57+
arm64: "52c482b964de63977b5a836b4976f0b098a1dfde6486ca7b8d0a6ee29b4f7945",
58+
amd64: "4b6828c7dec67cfbd4b911e9391fc9e32eddeea693025ee99a20c444c251f53a",
59+
} as const;
60+
5461
// Codex version is whatever sidecar/package.json pulled in. The SHAs below
5562
// must match THAT version — bump them together (or staging cross-arch will
5663
// abort with a clear error).
@@ -97,6 +104,8 @@ interface TargetInfo {
97104
ghArch: "arm64" | "amd64";
98105
/** `glab` release naming: `arm64` / `amd64`. */
99106
glabArch: "arm64" | "amd64";
107+
/** `tea` release naming: `arm64` / `amd64`. */
108+
teaArch: "arm64" | "amd64";
100109
}
101110

102111
function infoForArch(arch: DarwinArch): TargetInfo {
@@ -110,6 +119,7 @@ function infoForArch(arch: DarwinArch): TargetInfo {
110119
codexNpmSuffix: "darwin-arm64",
111120
ghArch: "arm64",
112121
glabArch: "arm64",
122+
teaArch: "arm64",
113123
};
114124
}
115125
return {
@@ -121,6 +131,7 @@ function infoForArch(arch: DarwinArch): TargetInfo {
121131
codexNpmSuffix: "darwin-x64",
122132
ghArch: "amd64",
123133
glabArch: "amd64",
134+
teaArch: "amd64",
124135
};
125136
}
126137

@@ -335,6 +346,20 @@ function stageGlabBinary(arch: "arm64" | "amd64"): string {
335346
return binDest;
336347
}
337348

349+
function stageTeaBinary(arch: "arm64" | "amd64"): string {
350+
ensureCacheDir();
351+
const slug = `tea-${TEA_VERSION}-darwin-${arch}`;
352+
const archive = join(BUNDLE_CACHE, slug);
353+
const url = `https://gitea.com/gitea/tea/releases/download/v${TEA_VERSION}/${slug}`;
354+
downloadAndVerify(url, archive, TEA_SHA256[arch]);
355+
356+
const binDest = join(DIST_VENDOR, "tea", "tea");
357+
copyFile(archive, binDest);
358+
chmodSync(binDest, 0o755);
359+
maybeSignMacBinary(binDest, false);
360+
return binDest;
361+
}
362+
338363
// ---------------------------------------------------------------------------
339364
// claude-code — prefer the platform sub-package already on disk; fall back to
340365
// downloading the npm tarball when staging for a non-host architecture.
@@ -527,13 +552,15 @@ stageClaudeCodeBinary(target);
527552
// ----- Codex -----
528553
stageCodexBinary(target);
529554

530-
// ----- gh + glab (forge CLIs) -----
555+
// ----- gh + glab + tea (forge CLIs) -----
531556
stageGhBinary(target.ghArch);
532557
stageGlabBinary(target.glabArch);
558+
stageTeaBinary(target.teaArch);
533559

534560
// ----- Summary -----
535561
console.log(`[stage-vendor] ✓ staged → ${DIST_VENDOR}`);
536562
console.log(` claude-code ${humanSize(join(DIST_VENDOR, "claude-code"))}`);
537563
console.log(` codex ${humanSize(join(DIST_VENDOR, "codex"))}`);
538564
console.log(` gh ${humanSize(join(DIST_VENDOR, "gh"))}`);
539565
console.log(` glab ${humanSize(join(DIST_VENDOR, "glab"))}`);
566+
console.log(` tea ${humanSize(join(DIST_VENDOR, "tea"))}`);

src-tauri/src/forge/accounts.rs

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
//! Per-repo gh/glab account binding — orchestration layer.
1+
//! Per-repo gh/glab/tea account binding — orchestration layer.
22
//!
33
//! Mirrors the [`super::provider::WorkspaceForgeBackend`] pattern: a
44
//! [`ForgeAccountBackend`] trait sits in the `forge::` umbrella, with
55
//! provider-specific implementations living under [`super::github::accounts`]
6-
//! and [`super::gitlab::accounts`]. Top-level helpers in this file
6+
//! and [`super::gitlab::accounts`] / [`super::gitea::accounts`]. Top-level helpers in this file
77
//! dispatch by provider so cross-cutting callers (the auto-bind hook,
88
//! the Settings → Account panel, the right-top workspace chip) never
99
//! need to branch on `ForgeProvider` themselves.
@@ -17,7 +17,7 @@ use super::remote::parse_remote;
1717
use super::types::ForgeProvider;
1818
use crate::repos;
1919

20-
/// Public profile of a single gh/glab account, surfaced to the
20+
/// Public profile of a single gh/glab/tea account, surfaced to the
2121
/// frontend's Settings → Account panel. `active` is true for the gh
2222
/// account currently marked active by `gh auth switch`; for GitLab
2323
/// (one-account-per-host) it's always true.
@@ -73,9 +73,9 @@ pub(crate) enum RepoAccess {
7373
}
7474

7575
/// Provider-agnostic account operations. Each method may interpret
76-
/// `host` / `login` slightly differently — GitLab ignores `login` since
77-
/// it has at most one account per host, while GitHub uses `(host,
78-
/// login)` as the full identity.
76+
/// `host` / `login` slightly differently — GitHub uses `(host, login)`
77+
/// as the full identity, while GitLab and Gitea use one account per
78+
/// host and ignore `login` for API execution.
7979
pub(crate) trait ForgeAccountBackend: Sync {
8080
/// Enumerate all accounts (with profile) for this forge.
8181
/// `hosts_hint` is ignored by GitHub (gh exposes its own host list)
@@ -112,13 +112,14 @@ pub(crate) fn backend_for(provider: ForgeProvider) -> Option<&'static dyn ForgeA
112112
match provider {
113113
ForgeProvider::Github => Some(&super::github::accounts::BACKEND),
114114
ForgeProvider::Gitlab => Some(&super::gitlab::accounts::BACKEND),
115+
ForgeProvider::Gitea => Some(&super::gitea::accounts::BACKEND),
115116
ForgeProvider::Unknown => None,
116117
}
117118
}
118119

119120
// ---------------- Top-level dispatchers ----------------
120121

121-
/// All gh accounts plus one glab account per `gitlab_hosts` entry.
122+
/// All gh accounts plus one glab/tea account per known host entry.
122123
/// Errors from individual backends are logged and skipped so a transient
123124
/// problem with one CLI doesn't blank the whole panel.
124125
pub(crate) fn list_forge_accounts(gitlab_hosts: &[String]) -> Vec<ForgeAccount> {
@@ -141,6 +142,15 @@ pub(crate) fn list_forge_accounts(gitlab_hosts: &[String]) -> Vec<ForgeAccount>
141142
),
142143
}
143144
}
145+
if let Some(backend) = backend_for(ForgeProvider::Gitea) {
146+
match backend.list_accounts(gitlab_hosts) {
147+
Ok(items) => accounts.extend(items),
148+
Err(error) => tracing::warn!(
149+
error = %format!("{error:#}"),
150+
"Failed to enumerate Gitea accounts"
151+
),
152+
}
153+
}
144154
accounts
145155
}
146156

@@ -153,6 +163,7 @@ pub(crate) fn invalidate_caches_for_host(provider: ForgeProvider, host: &str) {
153163
match provider {
154164
ForgeProvider::Github => crate::forge::github::accounts::invalidate_caches_for_host(host),
155165
ForgeProvider::Gitlab => crate::forge::gitlab::accounts::invalidate_caches_for_host(host),
166+
ForgeProvider::Gitea => crate::forge::gitea::accounts::invalidate_caches_for_host(host),
156167
ForgeProvider::Unknown => {}
157168
}
158169
}

src-tauri/src/forge/bundled.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
1-
//! Paths to bundled `gh` / `glab` inside `Resources/vendor/`.
1+
//! Paths to bundled `gh` / `glab` / `tea` inside `Resources/vendor/`.
22
33
use std::path::{Path, PathBuf};
44
use std::sync::OnceLock;
55

66
pub const GH_PATH_ENV: &str = "HELMOR_GH_BIN_PATH";
77
pub const GLAB_PATH_ENV: &str = "HELMOR_GLAB_BIN_PATH";
8+
pub const TEA_PATH_ENV: &str = "HELMOR_TEA_BIN_PATH";
89

910
#[derive(Debug, Default, Clone)]
1011
pub struct BundledForgeCliPaths {
1112
pub gh: Option<PathBuf>,
1213
pub glab: Option<PathBuf>,
14+
pub tea: Option<PathBuf>,
1315
}
1416

1517
static BUNDLED_PATHS: OnceLock<BundledForgeCliPaths> = OnceLock::new();
@@ -23,6 +25,7 @@ pub fn init() {
2325
tracing::info!(
2426
gh = ?paths.and_then(|p| p.gh.as_deref()),
2527
glab = ?paths.and_then(|p| p.glab.as_deref()),
28+
tea = ?paths.and_then(|p| p.tea.as_deref()),
2629
"Resolved bundled forge CLI paths"
2730
);
2831
}
@@ -41,6 +44,7 @@ pub fn bundled_path_for(program: &str) -> Option<PathBuf> {
4144
match program {
4245
"gh" => cached.gh.clone(),
4346
"glab" => cached.glab.clone(),
47+
"tea" => cached.tea.clone(),
4448
_ => None,
4549
}
4650
}
@@ -49,6 +53,7 @@ fn env_key_for(program: &str) -> Option<&'static str> {
4953
match program {
5054
"gh" => Some(GH_PATH_ENV),
5155
"glab" => Some(GLAB_PATH_ENV),
56+
"tea" => Some(TEA_PATH_ENV),
5257
_ => None,
5358
}
5459
}
@@ -77,13 +82,16 @@ fn resolve_for_exe(exe: &Path) -> Option<BundledForgeCliPaths> {
7782

7883
let gh_name = if cfg!(windows) { "gh.exe" } else { "gh" };
7984
let glab_name = if cfg!(windows) { "glab.exe" } else { "glab" };
85+
let tea_name = if cfg!(windows) { "tea.exe" } else { "tea" };
8086

8187
let gh = resources_dir.join(format!("vendor/gh/{gh_name}"));
8288
let glab = resources_dir.join(format!("vendor/glab/{glab_name}"));
89+
let tea = resources_dir.join(format!("vendor/tea/{tea_name}"));
8390

8491
Some(BundledForgeCliPaths {
8592
gh: gh.is_file().then_some(gh),
8693
glab: glab.is_file().then_some(glab),
94+
tea: tea.is_file().then_some(tea),
8795
})
8896
}
8997

@@ -93,6 +101,7 @@ impl BundledForgeCliPaths {
93101
BundledForgeCliPaths {
94102
gh: self.gh.or(fallback.gh),
95103
glab: self.glab.or(fallback.glab),
104+
tea: self.tea.or(fallback.tea),
96105
}
97106
}
98107
}
@@ -110,13 +119,16 @@ fn resolve_for_dev_workspace(workspace_root: &Path) -> BundledForgeCliPaths {
110119
let vendor = workspace_root.join("sidecar/dist/vendor");
111120
let gh_name = if cfg!(windows) { "gh.exe" } else { "gh" };
112121
let glab_name = if cfg!(windows) { "glab.exe" } else { "glab" };
122+
let tea_name = if cfg!(windows) { "tea.exe" } else { "tea" };
113123

114124
let gh = vendor.join(format!("gh/{gh_name}"));
115125
let glab = vendor.join(format!("glab/{glab_name}"));
126+
let tea = vendor.join(format!("tea/{tea_name}"));
116127

117128
BundledForgeCliPaths {
118129
gh: gh.is_file().then_some(gh),
119130
glab: glab.is_file().then_some(glab),
131+
tea: tea.is_file().then_some(tea),
120132
}
121133
}
122134

src-tauri/src/forge/cli_status.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//! Terminal-side helpers for the gh / glab auth-login flow:
1+
//! Terminal-side helpers for the gh / glab / tea auth-login flow:
22
//! - [`forge_cli_auth_command`] — produces the shell command we hand
33
//! off to the embedded Helmor terminal session.
44
//! - [`labels_for`] — provider-name / cli-name / connect-action
@@ -33,6 +33,16 @@ pub(crate) fn forge_cli_auth_command(
3333
bundled_program_token("glab")?
3434
)
3535
}
36+
ForgeProvider::Gitea => {
37+
let host = host.unwrap_or("gitea.com");
38+
if host.contains(['\n', '\r']) {
39+
bail!("Invalid hostname (contains newline): {host:?}");
40+
}
41+
format!(
42+
"{} login add --name helmor-{host} --url https://{host}",
43+
bundled_program_token("tea")?
44+
)
45+
}
3646
ForgeProvider::Unknown => bail!("Unknown forge provider."),
3747
})
3848
}
@@ -83,6 +93,13 @@ pub(crate) fn labels_for(provider: ForgeProvider) -> ForgeLabels {
8393
change_request_full_name: "merge request".to_string(),
8494
connect_action: "Connect GitLab".to_string(),
8595
},
96+
ForgeProvider::Gitea => ForgeLabels {
97+
provider_name: "Gitea".to_string(),
98+
cli_name: "tea".to_string(),
99+
change_request_name: "PR".to_string(),
100+
change_request_full_name: "pull request".to_string(),
101+
connect_action: "Connect Gitea".to_string(),
102+
},
86103
ForgeProvider::Unknown => ForgeLabels {
87104
provider_name: "Git".to_string(),
88105
cli_name: String::new(),

0 commit comments

Comments
 (0)