summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAiden Cline <[email protected]>2025-12-16 11:19:08 -0600
committerAiden Cline <[email protected]>2025-12-16 11:19:18 -0600
commit3ac42e96326d93d50b681cbae3b92a2ad1841e8c (patch)
treea95a4202d6f5e641c4071aab4c3c734b14f92a2d
parent9c26bb7c6c7d326ee11cdbd27586ee508bdff581 (diff)
downloadopencode-3ac42e96326d93d50b681cbae3b92a2ad1841e8c.tar.gz
opencode-3ac42e96326d93d50b681cbae3b92a2ad1841e8c.zip
fix: github install cmd if repo has . in it
-rw-r--r--.opencode/bun.lock6
-rw-r--r--.opencode/package.json2
-rw-r--r--packages/opencode/src/cli/cmd/github.ts25
-rw-r--r--packages/opencode/test/cli/github-remote.test.ts80
4 files changed, 99 insertions, 14 deletions
diff --git a/.opencode/bun.lock b/.opencode/bun.lock
index e2b8a4eeb..40e7c71af 100644
--- a/.opencode/bun.lock
+++ b/.opencode/bun.lock
@@ -5,7 +5,7 @@
"": {
"dependencies": {
"@octokit/rest": "^22.0.1",
- "@opencode-ai/plugin": "0.0.0-dev-202512161535",
+ "@opencode-ai/plugin": "0.0.0-dev-202512161610",
},
},
},
@@ -34,9 +34,9 @@
"@octokit/types": ["@octokit/[email protected]", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="],
- "@opencode-ai/plugin": ["@opencode-ai/[email protected]", "", { "dependencies": { "@opencode-ai/sdk": "0.0.0-dev-202512161535", "zod": "4.1.8" } }, "sha512-aHPm0T9EtKUYs5mTZKpYwcDTk3jP/YMSZGPfCAwruKitnktRFu0TxJQdEJwDfUokI4f1nkoGDkBgsbHw+pBHsA=="],
+ "@opencode-ai/plugin": ["@opencode-ai/[email protected]", "", { "dependencies": { "@opencode-ai/sdk": "0.0.0-dev-202512161610", "zod": "4.1.8" } }, "sha512-5TDOK75WgWeS/Lul+6OkDT0ESYAFhemCD67OjFcNCONpVgicqoiAgDunmQ2TpsZ+bl0S5kxw4wFGKkFjzBIZ2g=="],
- "@opencode-ai/sdk": ["@opencode-ai/[email protected]", "", {}, "sha512-koVbuyuhNnEWMJtkIxSTcg8HQ34c4ShvBHv4dwebvVB2+ftjN/wcqPDx4RAwaxyFaY050qf1qobHHMXWWzDRwQ=="],
+ "@opencode-ai/sdk": ["@opencode-ai/[email protected]", "", {}, "sha512-bnAwQ4DNdHqSoqMJfnZbH16qp0WnFSJpYWTmOdr/9hRu5SDjdmPx/QUlZGBg0yovuHJXqd1Fb/FLgljZ9QqGRA=="],
"before-after-hook": ["[email protected]", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="],
diff --git a/.opencode/package.json b/.opencode/package.json
index 685332a7e..78c21b45f 100644
--- a/.opencode/package.json
+++ b/.opencode/package.json
@@ -1,6 +1,6 @@
{
"dependencies": {
"@octokit/rest": "^22.0.1",
- "@opencode-ai/plugin": "0.0.0-dev-202512161535"
+ "@opencode-ai/plugin": "0.0.0-dev-202512161610"
}
}
diff --git a/packages/opencode/src/cli/cmd/github.ts b/packages/opencode/src/cli/cmd/github.ts
index 480a38230..c7d403395 100644
--- a/packages/opencode/src/cli/cmd/github.ts
+++ b/packages/opencode/src/cli/cmd/github.ts
@@ -128,6 +128,19 @@ const AGENT_USERNAME = "opencode-agent[bot]"
const AGENT_REACTION = "eyes"
const WORKFLOW_FILE = ".github/workflows/opencode.yml"
+// Parses GitHub remote URLs in various formats:
+// - https://github.com/owner/repo.git
+// - https://github.com/owner/repo
+// - [email protected]:owner/repo.git
+// - [email protected]:owner/repo
+// - ssh://[email protected]/owner/repo.git
+// - ssh://[email protected]/owner/repo
+export function parseGitHubRemote(url: string): { owner: string; repo: string } | null {
+ const match = url.match(/^(?:(?:https?|ssh):\/\/)?(?:git@)?github\.com[:/]([^/]+)\/([^/]+?)(?:\.git)?$/)
+ if (!match) return null
+ return { owner: match[1], repo: match[2] }
+}
+
export const GithubCommand = cmd({
command: "github",
describe: "manage GitHub agent",
@@ -197,20 +210,12 @@ export const GithubInstallCommand = cmd({
// Get repo info
const info = (await $`git remote get-url origin`.quiet().nothrow().text()).trim()
- // match https or git pattern
- // ie. https://github.com/sst/opencode.git
- // ie. https://github.com/sst/opencode
- // ie. [email protected]:sst/opencode.git
- // ie. [email protected]:sst/opencode
- // ie. ssh://[email protected]/sst/opencode.git
- // ie. ssh://[email protected]/sst/opencode
- const parsed = info.match(/^(?:(?:https?|ssh):\/\/)?(?:git@)?github\.com[:/]([^/]+)\/([^/.]+?)(?:\.git)?$/)
+ const parsed = parseGitHubRemote(info)
if (!parsed) {
prompts.log.error(`Could not find git repository. Please run this command from a git repository.`)
throw new UI.CancelledError()
}
- const [, owner, repo] = parsed
- return { owner, repo, root: Instance.worktree }
+ return { owner: parsed.owner, repo: parsed.repo, root: Instance.worktree }
}
async function promptProvider() {
diff --git a/packages/opencode/test/cli/github-remote.test.ts b/packages/opencode/test/cli/github-remote.test.ts
new file mode 100644
index 000000000..80102d986
--- /dev/null
+++ b/packages/opencode/test/cli/github-remote.test.ts
@@ -0,0 +1,80 @@
+import { test, expect } from "bun:test"
+import { parseGitHubRemote } from "../../src/cli/cmd/github"
+
+test("parses https URL with .git suffix", () => {
+ expect(parseGitHubRemote("https://github.com/sst/opencode.git")).toEqual({ owner: "sst", repo: "opencode" })
+})
+
+test("parses https URL without .git suffix", () => {
+ expect(parseGitHubRemote("https://github.com/sst/opencode")).toEqual({ owner: "sst", repo: "opencode" })
+})
+
+test("parses git@ URL with .git suffix", () => {
+ expect(parseGitHubRemote("[email protected]:sst/opencode.git")).toEqual({ owner: "sst", repo: "opencode" })
+})
+
+test("parses git@ URL without .git suffix", () => {
+ expect(parseGitHubRemote("[email protected]:sst/opencode")).toEqual({ owner: "sst", repo: "opencode" })
+})
+
+test("parses ssh:// URL with .git suffix", () => {
+ expect(parseGitHubRemote("ssh://[email protected]/sst/opencode.git")).toEqual({ owner: "sst", repo: "opencode" })
+})
+
+test("parses ssh:// URL without .git suffix", () => {
+ expect(parseGitHubRemote("ssh://[email protected]/sst/opencode")).toEqual({ owner: "sst", repo: "opencode" })
+})
+
+test("parses http URL", () => {
+ expect(parseGitHubRemote("http://github.com/owner/repo")).toEqual({ owner: "owner", repo: "repo" })
+})
+
+test("parses URL with hyphenated owner and repo names", () => {
+ expect(parseGitHubRemote("https://github.com/my-org/my-repo.git")).toEqual({ owner: "my-org", repo: "my-repo" })
+})
+
+test("parses URL with underscores in names", () => {
+ expect(parseGitHubRemote("[email protected]:my_org/my_repo.git")).toEqual({ owner: "my_org", repo: "my_repo" })
+})
+
+test("parses URL with numbers in names", () => {
+ expect(parseGitHubRemote("https://github.com/org123/repo456")).toEqual({ owner: "org123", repo: "repo456" })
+})
+
+test("parses repos with dots in the name", () => {
+ expect(parseGitHubRemote("https://github.com/socketio/socket.io.git")).toEqual({
+ owner: "socketio",
+ repo: "socket.io",
+ })
+ expect(parseGitHubRemote("https://github.com/vuejs/vue.js")).toEqual({
+ owner: "vuejs",
+ repo: "vue.js",
+ })
+ expect(parseGitHubRemote("[email protected]:mrdoob/three.js.git")).toEqual({
+ owner: "mrdoob",
+ repo: "three.js",
+ })
+ expect(parseGitHubRemote("https://github.com/jashkenas/backbone.git")).toEqual({
+ owner: "jashkenas",
+ repo: "backbone",
+ })
+})
+
+test("returns null for non-github URLs", () => {
+ expect(parseGitHubRemote("https://gitlab.com/owner/repo.git")).toBeNull()
+ expect(parseGitHubRemote("[email protected]:owner/repo.git")).toBeNull()
+ expect(parseGitHubRemote("https://bitbucket.org/owner/repo")).toBeNull()
+})
+
+test("returns null for invalid URLs", () => {
+ expect(parseGitHubRemote("not-a-url")).toBeNull()
+ expect(parseGitHubRemote("")).toBeNull()
+ expect(parseGitHubRemote("github.com")).toBeNull()
+ expect(parseGitHubRemote("https://github.com/")).toBeNull()
+ expect(parseGitHubRemote("https://github.com/owner")).toBeNull()
+})
+
+test("returns null for URLs with extra path segments", () => {
+ expect(parseGitHubRemote("https://github.com/owner/repo/tree/main")).toBeNull()
+ expect(parseGitHubRemote("https://github.com/owner/repo/blob/main/file.ts")).toBeNull()
+})