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 11:15:40 +00:00
committed by GitHub
co-authored by Mohamed Boudra
parent bac302626f
commit 218097b7cc
15 changed files with 1223 additions and 78 deletions
+54 -1
View File
@@ -1,7 +1,15 @@
import { useCallback } from "react";
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
import { useSessionStore } from "@/stores/session-store";
import { openProjectDirectly, type OpenProjectResult } from "@/hooks/open-project";
import { useWorkspaceLayoutStore } from "@/stores/workspace-layout-store";
import { generateDraftId } from "@/stores/draft-keys";
import { navigateToWorkspace } from "@/stores/navigation-active-workspace-store";
import {
openGithubRepoDirectly,
openProjectDirectly,
type OpenProjectResult,
type WorkspaceGithubCloneProtocol,
} from "@/hooks/open-project";
export function useOpenProject(
serverId: string | null,
@@ -40,3 +48,48 @@ export function useOpenProject(
],
);
}
export function useOpenGithubRepo(
serverId: string | null,
): (
repo: string,
targetDirectory: string,
cloneProtocol?: WorkspaceGithubCloneProtocol,
) => Promise<boolean> {
const normalizedServerId = serverId?.trim() ?? "";
const client = useHostRuntimeClient(normalizedServerId);
const isConnected = useHostRuntimeIsConnected(normalizedServerId);
const mergeWorkspaces = useSessionStore((state) => state.mergeWorkspaces);
const setHasHydratedWorkspaces = useSessionStore((state) => state.setHasHydratedWorkspaces);
const openDraftTab = useCallback((workspaceKey: string) => {
return useWorkspaceLayoutStore.getState().openTabFocused(workspaceKey, {
kind: "draft",
draftId: generateDraftId(),
});
}, []);
return useCallback(
async (repo: string, targetDirectory: string, cloneProtocol?: WorkspaceGithubCloneProtocol) => {
return openGithubRepoDirectly({
serverId: normalizedServerId,
repo,
targetDirectory,
...(cloneProtocol ? { cloneProtocol } : {}),
isConnected,
client,
mergeWorkspaces,
setHasHydratedWorkspaces,
openDraftTab,
navigateToWorkspace,
});
},
[
client,
isConnected,
mergeWorkspaces,
normalizedServerId,
openDraftTab,
setHasHydratedWorkspaces,
],
);
}