feat: clone GitHub repo into a workspace (#1331)

* feat: clone GitHub repo into a workspace

Add an end-to-end "clone a GitHub repo and register it as a Paseo
workspace" flow: a new workspace.github.clone RPC, daemon handler,
client method, CLI `paseo clone` command, and a GitHub-repo mode in
the project picker modal. Gated behind the workspaceGithubClone
server capability flag.

- protocol: workspace.github.clone request/response schemas + feature flag
- server: handleWorkspaceGithubCloneRequest, normalizeCloneRepository
- client: DaemonClient.cloneGithubWorkspace
- cli: `paseo clone <repo> --dir <path> [--protocol https|ssh]`
- app: GitHub-repo mode, clone-protocol picker, error surfacing

Review fixes:
- CLI clone now checks the workspaceGithubClone capability and fails
  fast with a clear "update the host" error instead of hanging for the
  full request timeout against an older daemon.
- Replace the duplicated client-side URL-detection regex (app + CLI)
  with a shared isCompleteGitRemote() in @getpaseo/protocol/git-remote,
  backed by parseGitRemoteLocation so clients classify remotes
  identically to the daemon (fixes confusing errors for git://, ftp://,
  file:// inputs).
- Add git-remote.test.ts covering the shared classifier.

* Fix GitHub clone failure handling

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
This commit is contained in:
Matt Cowger
2026-07-13 04:15:40 -07:00
committed by GitHub
parent bac302626f
commit 218097b7cc
15 changed files with 1223 additions and 78 deletions

View File

@@ -56,6 +56,8 @@ import type {
ProjectIconResponse,
ProjectAddResponse,
OpenProjectResponseMessage,
WorkspaceGithubCloneProtocol,
WorkspaceGithubCloneResponse,
ArchiveWorkspaceResponseMessage,
WorkspaceSetupStatusResponseMessage,
ListCommandsResponse,
@@ -148,6 +150,8 @@ const perfNow: () => number =
? () => performance.now()
: () => Date.now();
const WORKSPACE_GITHUB_CLONE_TIMEOUT_MS = 5 * 60 * 1000;
interface ImportAgentInputBase {
cwd?: string;
labels?: Record<string, string>;
@@ -758,6 +762,7 @@ export interface RenameTerminalInput {
}
type OpenProjectPayload = OpenProjectResponseMessage["payload"];
type ProjectAddPayload = ProjectAddResponse["payload"];
type WorkspaceGithubClonePayload = WorkspaceGithubCloneResponse["payload"];
type ArchiveWorkspacePayload = ArchiveWorkspaceResponseMessage["payload"];
type WorkspaceSetupStatusPayload = WorkspaceSetupStatusResponseMessage["payload"];
@@ -1988,6 +1993,23 @@ export class DaemonClient {
});
}
async cloneGithubWorkspace(
input: { repo: string; targetDirectory: string; cloneProtocol?: WorkspaceGithubCloneProtocol },
requestId?: string,
): Promise<WorkspaceGithubClonePayload> {
const message = {
type: "workspace.github.clone.request",
repo: input.repo,
targetDirectory: input.targetDirectory,
...(input.cloneProtocol ? { cloneProtocol: input.cloneProtocol } : {}),
} as const;
return this.sendNamespacedCorrelatedSessionRequest<"workspace.github.clone.response">({
requestId,
message,
timeout: WORKSPACE_GITHUB_CLONE_TIMEOUT_MS,
});
}
async startWorkspaceScript(
workspaceId: string,
scriptName: string,