diff --git a/src/cli/handlers/project.ts b/src/cli/handlers/project.ts index 2b0a8d8ada2..a7bf9ee2abe 100644 --- a/src/cli/handlers/project.ts +++ b/src/cli/handlers/project.ts @@ -10,7 +10,7 @@ import type { ProjectHostSetupUpdateArgs, ProjectHostSetupUpdateResult } from '../../shared/project-types' -import type { ExecutionHostId } from '../../shared/execution-host' +import { getSshTargetIdForExecutionHost, type ExecutionHostId } from '../../shared/execution-host' import type { RepoKind } from '../../shared/repo-types' import type { CommandHandler, HandlerContext } from '../dispatch' import { @@ -111,10 +111,14 @@ export const PROJECT_HANDLERS: Record = { }, 'project setup-existing-folder': async ({ flags, client, cwd, json }) => { const rawPath = getRequiredStringFlag(flags, 'path') + const hostId = getRequiredHostId(flags) + // An SSH host's filesystem is not the CLI's, so resolving a relative path against the client + // cwd would register a path that names the wrong machine. + const pathIsOffClient = client.isRemote || getSshTargetIdForExecutionHost(hostId) !== null const args: ProjectHostSetupExistingFolderArgs = { projectId: getRequiredStringFlag(flags, 'project'), - hostId: getRequiredHostId(flags), - path: resolveRepoPathArgument(rawPath, cwd, client.isRemote, 'Remote project setup'), + hostId, + path: resolveRepoPathArgument(rawPath, cwd, pathIsOffClient, 'Remote project setup'), kind: getOptionalRepoKind(flags), displayName: getOptionalStringFlag(flags, 'display-name') } diff --git a/src/cli/index-project-setup.test.ts b/src/cli/index-project-setup.test.ts index 068c246ba7c..f104e45c39a 100644 --- a/src/cli/index-project-setup.test.ts +++ b/src/cli/index-project-setup.test.ts @@ -481,6 +481,37 @@ describe('orca cli worktree awareness', () => { process.exitCode = priorExitCode }) + it('rejects SSH project setup relative paths, which name the client filesystem', async () => { + // A local CLI reaching an `ssh:*` host is still off-client: resolving `./orca` against the + // CLI cwd would register a path that exists on the wrong machine. + const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) + const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + const priorExitCode = process.exitCode + + await main( + [ + 'project', + 'setup-existing-folder', + '--project', + 'github:stablyai/orca', + '--host', + 'ssh:openclaw', + '--path', + './orca', + '--json' + ], + '/tmp/repo' + ) + + expect(callMock).not.toHaveBeenCalled() + expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain( + 'Remote project setup requires --path to be an absolute path on the remote server.' + ) + expect(process.exitCode).toBe(1) + + process.exitCode = priorExitCode + }) + it('rejects remote repo.add relative paths instead of resolving against client cwd', async () => { const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})