diff --git a/apps/server/src/git/Layers/GitHubCli.test.ts b/apps/server/src/git/Layers/GitHubCli.test.ts index aafc796db..eaab0ae6c 100644 --- a/apps/server/src/git/Layers/GitHubCli.test.ts +++ b/apps/server/src/git/Layers/GitHubCli.test.ts @@ -106,6 +106,32 @@ layer("GitHubCliLive", (it) => { }), ); + it.effect("creates a repository from the current directory", () => + Effect.gen(function* () { + mockedRunProcess.mockResolvedValueOnce({ + stdout: "https://github.com/octocat/codething-mvp\n", + stderr: "", + code: 0, + signal: null, + timedOut: false, + }); + + yield* Effect.gen(function* () { + const gh = yield* GitHubCli; + return yield* gh.createRepository({ + cwd: "/repo", + visibility: "private", + }); + }); + + expect(mockedRunProcess).toHaveBeenCalledWith( + "gh", + ["repo", "create", "--source=.", "--private", "--remote", "origin"], + expect.objectContaining({ cwd: "/repo" }), + ); + }), + ); + it.effect("surfaces a friendly error when the pull request is not found", () => Effect.gen(function* () { mockedRunProcess.mockRejectedValueOnce( diff --git a/apps/server/src/git/Layers/GitHubCli.ts b/apps/server/src/git/Layers/GitHubCli.ts index 80ce43659..92e77a53a 100644 --- a/apps/server/src/git/Layers/GitHubCli.ts +++ b/apps/server/src/git/Layers/GitHubCli.ts @@ -241,6 +241,18 @@ const makeGitHubCli = Effect.sync(() => { ), Effect.map(normalizeRepositoryCloneUrls), ), + createRepository: (input) => + execute({ + cwd: input.cwd, + args: [ + "repo", + "create", + "--source=.", + `--${input.visibility}`, + "--remote", + input.remote ?? "origin", + ], + }).pipe(Effect.asVoid), createPullRequest: (input) => execute({ cwd: input.cwd, diff --git a/apps/server/src/git/Layers/GitManager.test.ts b/apps/server/src/git/Layers/GitManager.test.ts index cc80eda23..b8a6836f2 100644 --- a/apps/server/src/git/Layers/GitManager.test.ts +++ b/apps/server/src/git/Layers/GitManager.test.ts @@ -408,6 +408,18 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): { input.bodyFile, ], }).pipe(Effect.asVoid), + createRepository: (input) => + execute({ + cwd: input.cwd, + args: [ + "repo", + "create", + "--source=.", + `--${input.visibility}`, + "--remote", + input.remote ?? "origin", + ], + }).pipe(Effect.asVoid), getDefaultBranch: (input) => execute({ cwd: input.cwd, diff --git a/apps/server/src/git/Services/GitHubCli.ts b/apps/server/src/git/Services/GitHubCli.ts index f10339af4..cd7cd1aa3 100644 --- a/apps/server/src/git/Services/GitHubCli.ts +++ b/apps/server/src/git/Services/GitHubCli.ts @@ -67,6 +67,15 @@ export interface GitHubCliShape { readonly repository: string; }) => Effect.Effect; + /** + * Create a GitHub repository from the current local repository. + */ + readonly createRepository: (input: { + readonly cwd: string; + readonly visibility: "private" | "public" | "internal"; + readonly remote?: string; + }) => Effect.Effect; + /** * Create a pull request from branch context and body file. */ diff --git a/apps/server/src/serverLayers.ts b/apps/server/src/serverLayers.ts index ff9b10d96..266c47dec 100644 --- a/apps/server/src/serverLayers.ts +++ b/apps/server/src/serverLayers.ts @@ -124,6 +124,7 @@ export function makeServerRuntimeServicesLayer() { return Layer.mergeAll( orchestrationReactorLayer, gitCoreLayer, + GitHubCliLive, gitManagerLayer, terminalLayer, KeybindingsLive, diff --git a/apps/server/src/wsServer.test.ts b/apps/server/src/wsServer.test.ts index f12792a31..d31d9f57f 100644 --- a/apps/server/src/wsServer.test.ts +++ b/apps/server/src/wsServer.test.ts @@ -48,6 +48,7 @@ import { ProviderService, type ProviderServiceShape } from "./provider/Services/ import { ProviderHealth, type ProviderHealthShape } from "./provider/Services/ProviderHealth"; import { Open, type OpenShape } from "./open"; import { GitManager, type GitManagerShape } from "./git/Services/GitManager.ts"; +import { GitHubCli, type GitHubCliShape } from "./git/Services/GitHubCli.ts"; import type { GitCoreShape } from "./git/Services/GitCore.ts"; import { GitCore } from "./git/Services/GitCore.ts"; import { GitCommandError, GitManagerError } from "./git/Errors.ts"; @@ -481,6 +482,7 @@ describe("WebSocket Server", () => { open?: OpenShape; gitManager?: GitManagerShape; gitCore?: Pick; + gitHubCli?: Pick; terminalManager?: TerminalManagerShape; } = {}, ): Promise { @@ -517,6 +519,9 @@ describe("WebSocket Server", () => { options.gitCore ? Layer.succeed(GitCore, options.gitCore as unknown as GitCoreShape) : Layer.empty, + options.gitHubCli + ? Layer.succeed(GitHubCli, options.gitHubCli as unknown as GitHubCliShape) + : Layer.empty, options.terminalManager ? Layer.succeed(TerminalManager, options.terminalManager) : Layer.empty, @@ -1630,6 +1635,7 @@ describe("WebSocket Server", () => { }), ); const initRepo = vi.fn(() => Effect.void); + const createRepository = vi.fn(() => Effect.void); const pullCurrentBranch = vi.fn(() => Effect.fail( new GitCommandError({ @@ -1648,6 +1654,9 @@ describe("WebSocket Server", () => { initRepo, pullCurrentBranch, }, + gitHubCli: { + createRepository, + }, }); const addr = server.address(); const port = typeof addr === "object" && addr !== null ? addr.port : 0; @@ -1663,6 +1672,10 @@ describe("WebSocket Server", () => { const initResponse = await sendRequest(ws, WS_METHODS.gitInit, { cwd: "/repo/path" }); expect(initResponse.error).toBeUndefined(); expect(initRepo).toHaveBeenCalledWith({ cwd: "/repo/path" }); + expect(createRepository).toHaveBeenCalledWith({ + cwd: "/repo/path", + visibility: "private", + }); const pullResponse = await sendRequest(ws, WS_METHODS.gitPull, { cwd: "/repo/path" }); expect(pullResponse.result).toBeUndefined(); diff --git a/apps/server/src/wsServer.ts b/apps/server/src/wsServer.ts index 2e6ac51b7..a84270620 100644 --- a/apps/server/src/wsServer.ts +++ b/apps/server/src/wsServer.ts @@ -47,6 +47,7 @@ import { WebSocketServer, type WebSocket } from "ws"; import { createLogger } from "./logger"; import { GitManager } from "./git/Services/GitManager.ts"; +import { GitHubCli } from "./git/Services/GitHubCli.ts"; import { TerminalManager } from "./terminal/Services/Manager.ts"; import { Keybindings } from "./keybindings"; import { searchWorkspaceEntries } from "./workspaceEntries"; @@ -213,6 +214,7 @@ export type ServerCoreRuntimeServices = export type ServerRuntimeServices = | ServerCoreRuntimeServices | GitManager + | GitHubCli | GitCore | TerminalManager | Keybindings @@ -255,6 +257,7 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return< const keybindingsManager = yield* Keybindings; const providerHealth = yield* ProviderHealth; const git = yield* GitCore; + const gitHubCli = yield* GitHubCli; const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; @@ -833,7 +836,11 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return< case WS_METHODS.gitInit: { const body = stripRequestTag(request.body); - return yield* git.initRepo(body); + yield* git.initRepo(body); + return yield* gitHubCli.createRepository({ + cwd: body.cwd, + visibility: "private", + }); } case WS_METHODS.terminalOpen: {