Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions backend/app/services/device/command_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,14 @@ class LocalDeviceCommandDefinition:
command="ls -a -p",
post_processor="directory_list",
),
"mkdir_p": LocalDeviceCommandDefinition(command="mkdir -p"),
"git_clone": LocalDeviceCommandDefinition(command="git clone"),
"git_branch": LocalDeviceCommandDefinition(command="git branch --show-current"),
"git_branch_list": LocalDeviceCommandDefinition(
command="git branch --format=%(refname:short)"
),
"git_checkout": LocalDeviceCommandDefinition(command="git checkout"),
"git_checkout_new": LocalDeviceCommandDefinition(command="git checkout -b"),
"git_diff_shortstat": LocalDeviceCommandDefinition(command="git diff --shortstat"),
"git_branch_diff_shortstat": LocalDeviceCommandDefinition(
command=GIT_BRANCH_DIFF_SHORTSTAT_COMMAND
Expand Down
24 changes: 24 additions & 0 deletions backend/tests/services/test_local_device_command_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,24 @@ def test_local_device_command_registry_default_includes_diagnostic_commands():
ls_dirs_definition = resolve_local_device_command(
"ls_dirs", settings.LOCAL_DEVICE_COMMANDS
)
mkdir_definition = resolve_local_device_command(
"mkdir_p", settings.LOCAL_DEVICE_COMMANDS
)
git_clone_definition = resolve_local_device_command(
"git_clone", settings.LOCAL_DEVICE_COMMANDS
)
git_branch_definition = resolve_local_device_command(
"git_branch", settings.LOCAL_DEVICE_COMMANDS
)
git_branch_list_definition = resolve_local_device_command(
"git_branch_list", settings.LOCAL_DEVICE_COMMANDS
)
git_checkout_definition = resolve_local_device_command(
"git_checkout", settings.LOCAL_DEVICE_COMMANDS
)
git_checkout_new_definition = resolve_local_device_command(
"git_checkout_new", settings.LOCAL_DEVICE_COMMANDS
)
git_diff_shortstat_definition = resolve_local_device_command(
"git_diff_shortstat", settings.LOCAL_DEVICE_COMMANDS
)
Expand Down Expand Up @@ -66,12 +78,24 @@ def test_local_device_command_registry_default_includes_diagnostic_commands():
assert ls_dirs_definition is not None
assert ls_dirs_definition.command == "ls -a -p"
assert ls_dirs_definition.post_processor == "directory_list"
assert mkdir_definition is not None
assert mkdir_definition.command == "mkdir -p"
assert mkdir_definition.post_processor is None
assert git_clone_definition is not None
assert git_clone_definition.command == "git clone"
assert git_clone_definition.post_processor is None
assert git_branch_definition is not None
assert git_branch_definition.command == "git branch --show-current"
assert git_branch_definition.post_processor is None
assert git_branch_list_definition is not None
assert git_branch_list_definition.command == "git branch --format=%(refname:short)"
assert git_branch_list_definition.post_processor is None
assert git_checkout_definition is not None
assert git_checkout_definition.command == "git checkout"
assert git_checkout_definition.post_processor is None
assert git_checkout_new_definition is not None
assert git_checkout_new_definition.command == "git checkout -b"
assert git_checkout_new_definition.post_processor is None
assert git_diff_shortstat_definition is not None
assert git_diff_shortstat_definition.command == "git diff --shortstat"
assert git_diff_shortstat_definition.post_processor is None
Expand Down
4 changes: 4 additions & 0 deletions wework/src/App.plugins.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,12 @@ const workbenchValue: WorkbenchContextValue = {
getDeviceHomeDirectory: vi.fn(),
getProjectWorkspaceRoot: vi.fn(),
listDeviceDirectories: vi.fn(),
createDeviceDirectory: vi.fn(),
loadEnvironmentInfo: vi.fn(),
commitEnvironmentChanges: vi.fn(),
listEnvironmentBranches: vi.fn(),
checkoutEnvironmentBranch: vi.fn(),
createEnvironmentBranch: vi.fn(),
setInput: vi.fn(),
sendCurrentInput: vi.fn(),
}
Expand Down
21 changes: 21 additions & 0 deletions wework/src/api/adapters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,4 +251,25 @@ describe('REST adapters', () => {
expect.objectContaining({ command_key: 'project_workspace_root' }),
)
})

test('creates a directory through the device command API', async () => {
const client = mockClient()
vi.mocked(client.post).mockResolvedValueOnce({
success: true,
stdout: '',
stderr: '',
})

const api = createDeviceApi(client)

await expect(api.createDirectory('device-1', '/home/ubuntu/new-app')).resolves.toBeUndefined()

expect(client.post).toHaveBeenCalledWith(
'/devices/device-1/commands',
expect.objectContaining({
command_key: 'mkdir_p',
args: ['/home/ubuntu/new-app'],
}),
)
})
})
15 changes: 15 additions & 0 deletions wework/src/api/devices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,21 @@ export function createDeviceApi(client: HttpClient) {
return Array.isArray(response.stdout) ? response.stdout : []
},

async createDirectory(deviceId: string, path: string): Promise<void> {
const response = await client.post<DeviceCommandResponse>(
`/devices/${encodeURIComponent(deviceId)}/commands`,
{
command_key: 'mkdir_p',
args: [path],
timeout_seconds: 15,
max_output_bytes: 4096,
},
)
if (!response.success) {
throw new Error(response.error || response.stderr || 'Failed to create directory')
}
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

executeCommand(
deviceId: string,
data: {
Expand Down
63 changes: 63 additions & 0 deletions wework/src/api/environment.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { describe, expect, test, vi } from 'vitest'
import {
buildPullRequestUrl,
checkoutProjectBranch,
commitProjectChanges,
createAndCheckoutProjectBranch,
listProjectBranches,
loadProjectEnvironment,
parseGitShortStat,
} from './environment'
Expand Down Expand Up @@ -315,3 +318,63 @@ describe('commitProjectChanges', () => {
expect(executeCommand).not.toHaveBeenCalled()
})
})

describe('branch environment commands', () => {
const project = {
id: 1,
name: 'Wegent',
config: {
mode: 'workspace',
execution: { targetType: 'local' as const, deviceId: 'device-123' },
workspace: { source: 'local_path' as const, localPath: '/workspace/Wegent' },
},
}

test('lists branches sorted by name', async () => {
const executeCommand = vi.fn().mockResolvedValue({
success: true,
stdout: 'human/zebra\nmain\nhuman/alpaca\n',
stderr: '',
})

await expect(listProjectBranches({ executeCommand }, project)).resolves.toEqual([
'human/alpaca',
'human/zebra',
'main',
])
expect(executeCommand).toHaveBeenCalledWith('device-123', {
command_key: 'git_branch_list',
path: '/workspace/Wegent',
timeout_seconds: 15,
max_output_bytes: 1024 * 64,
})
})

test('checks out an existing branch', async () => {
const executeCommand = vi.fn().mockResolvedValue({ success: true, stdout: '', stderr: '' })

await checkoutProjectBranch({ executeCommand }, project, 'human/alpaca')

expect(executeCommand).toHaveBeenCalledWith('device-123', {
command_key: 'git_checkout',
path: '/workspace/Wegent',
args: ['human/alpaca'],
timeout_seconds: 30,
max_output_bytes: 8192,
})
})

test('creates and checks out a new branch', async () => {
const executeCommand = vi.fn().mockResolvedValue({ success: true, stdout: '', stderr: '' })

await createAndCheckoutProjectBranch({ executeCommand }, project, 'human/new-branch')

expect(executeCommand).toHaveBeenCalledWith('device-123', {
command_key: 'git_checkout_new',
path: '/workspace/Wegent',
args: ['human/new-branch'],
timeout_seconds: 30,
max_output_bytes: 8192,
})
})
})
63 changes: 63 additions & 0 deletions wework/src/api/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,3 +226,66 @@ export async function commitProjectChanges(
maxOutputBytes: 8192,
})
}

export async function listProjectBranches(
api: DeviceCommandApi,
project: ProjectWithTasks | null,
): Promise<string[]> {
if (!project) {
throw new Error('Project is required')
}

const { deviceId, path } = commandContext(project)
const output = await runGitCommand(api, deviceId, 'git_branch_list', path, {
timeoutSeconds: 15,
maxOutputBytes: 1024 * 64,
})

return output
.split('\n')
.map(branch => branch.trim())
.filter(Boolean)
.sort((left, right) => left.localeCompare(right))
}

export async function checkoutProjectBranch(
api: DeviceCommandApi,
project: ProjectWithTasks | null,
branchName: string,
): Promise<void> {
const trimmedBranch = branchName.trim()
if (!trimmedBranch) {
throw new Error('Branch name is required')
}
if (!project) {
throw new Error('Project is required')
}

const { deviceId, path } = commandContext(project)
await runGitCommand(api, deviceId, 'git_checkout', path, {
args: [trimmedBranch],
timeoutSeconds: 30,
maxOutputBytes: 8192,
})
}

export async function createAndCheckoutProjectBranch(
api: DeviceCommandApi,
project: ProjectWithTasks | null,
branchName: string,
): Promise<void> {
const trimmedBranch = branchName.trim()
if (!trimmedBranch) {
throw new Error('Branch name is required')
}
if (!project) {
throw new Error('Project is required')
}

const { deviceId, path } = commandContext(project)
await runGitCommand(api, deviceId, 'git_checkout_new', path, {
args: [trimmedBranch],
timeoutSeconds: 30,
maxOutputBytes: 8192,
})
}
Comment thread
Micro66 marked this conversation as resolved.
Resolved
3 changes: 3 additions & 0 deletions wework/src/components/layout/DesktopSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ interface DesktopSidebarProps {
onGetDeviceHomeDirectory: (deviceId: string) => Promise<string>
onGetProjectWorkspaceRoot: (deviceId: string) => Promise<string>
onListDeviceDirectories: (deviceId: string, path: string) => Promise<string[]>
onCreateDeviceDirectory: (deviceId: string, path: string) => Promise<void>
onOpenSettings: () => void
onLogout: () => void
}
Expand Down Expand Up @@ -510,6 +511,7 @@ export function DesktopSidebar({
onGetDeviceHomeDirectory,
onGetProjectWorkspaceRoot,
onListDeviceDirectories,
onCreateDeviceDirectory,
onOpenSettings,
onLogout,
}: DesktopSidebarProps) {
Expand Down Expand Up @@ -796,6 +798,7 @@ export function DesktopSidebar({
onGetDeviceHomeDirectory={onGetDeviceHomeDirectory}
onGetProjectWorkspaceRoot={onGetProjectWorkspaceRoot}
onListDeviceDirectories={onListDeviceDirectories}
onCreateDeviceDirectory={onCreateDeviceDirectory}
/>
<TextInputDialog
open={renamingProject !== null}
Expand Down
Loading