Skip to content

Commit dc674ed

Browse files
committed
feat(launcher): add container rsync command
- Adds host-visible rsync command with push/pull and completion hints. - Adds wrapper execution logic for Docker containers.
1 parent ddc84b0 commit dc674ed

File tree

6 files changed

+474
-3
lines changed

6 files changed

+474
-3
lines changed

crates/agent-workspace/src/cli.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ pub enum CliCommand {
3636
#[command(disable_help_flag = true)]
3737
Create(PassthroughArgs),
3838
#[command(disable_help_flag = true)]
39+
Rsync(PassthroughArgs),
40+
#[command(disable_help_flag = true)]
3941
Ls(PassthroughArgs),
4042
#[command(disable_help_flag = true)]
4143
Rm(PassthroughArgs),
@@ -77,6 +79,10 @@ impl CliCommand {
7779
subcommand: "create",
7880
args: args.args,
7981
},
82+
Self::Rsync(args) => ForwardRequest {
83+
subcommand: "rsync",
84+
args: args.args,
85+
},
8086
Self::Ls(args) => ForwardRequest {
8187
subcommand: "ls",
8288
args: args.args,
@@ -159,7 +165,9 @@ mod tests {
159165
cmd.write_long_help(&mut out).expect("write help");
160166

161167
let help = String::from_utf8(out).expect("utf8");
162-
for subcommand in ["auth", "create", "ls", "rm", "exec", "reset", "tunnel"] {
168+
for subcommand in [
169+
"auth", "create", "rsync", "ls", "rm", "exec", "reset", "tunnel",
170+
] {
163171
assert!(
164172
help.contains(subcommand),
165173
"help should include subcommand {subcommand}"

crates/agent-workspace/src/completion/engine.rs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ pub(crate) fn complete<P: WorkspaceProvider>(
3333
match subcommand {
3434
"auth" => complete_auth(current, &args_before, &mut workspace_ctx),
3535
"create" => complete_create(current, &args_before),
36+
"rsync" => complete_rsync(current, &args_before, &mut workspace_ctx),
3637
"ls" => complete_ls(current, &args_before),
3738
"rm" => complete_rm(&args_before, &mut workspace_ctx),
3839
"exec" => complete_exec(current, &args_before, &mut workspace_ctx),
@@ -57,6 +58,7 @@ fn complete_top_level(_current: &str) -> Vec<Candidate> {
5758
&[
5859
("auth", "Update auth material in workspace"),
5960
("create", "Create a new workspace"),
61+
("rsync", "Sync files between host and container"),
6062
("ls", "List workspaces"),
6163
("rm", "Remove workspace(s)"),
6264
("exec", "Run command in workspace"),
@@ -68,6 +70,89 @@ fn complete_top_level(_current: &str) -> Vec<Candidate> {
6870
out
6971
}
7072

73+
fn complete_rsync<P: WorkspaceProvider>(
74+
current: &str,
75+
args_before: &[String],
76+
workspace_ctx: &mut WorkspaceContext<'_, P>,
77+
) -> Vec<Candidate> {
78+
if let Some((option, inline)) = value_option(args_before, current, &["--user"]) {
79+
return value_suggestions_described(
80+
&option,
81+
inline,
82+
&[
83+
("0", "UID 0 (root)"),
84+
("root", "Root user"),
85+
("agent", "Default agent user"),
86+
("codex", "Alternate codex user"),
87+
],
88+
);
89+
}
90+
91+
let mut direction: Option<&str> = None;
92+
let mut idx = 0usize;
93+
while idx < args_before.len() {
94+
let token = args_before[idx].as_str();
95+
if token.starts_with('-') {
96+
idx += 1;
97+
continue;
98+
}
99+
direction = Some(token);
100+
idx += 1;
101+
break;
102+
}
103+
104+
let mut out: Vec<Candidate> = Vec::new();
105+
if direction.is_none() {
106+
push_described_values(
107+
&mut out,
108+
&[
109+
("push", "Sync host files into workspace"),
110+
("pull", "Sync workspace files to host"),
111+
("--help", "Show help for rsync"),
112+
("-h", "Show help for rsync"),
113+
],
114+
);
115+
push_global_options(&mut out);
116+
return out;
117+
}
118+
119+
let mut positional_seen = 0usize;
120+
let mut j = idx;
121+
while j < args_before.len() {
122+
let token = args_before[j].as_str();
123+
match token {
124+
"--user" | "-u" => j += 2,
125+
_ if token.starts_with("--user=") => j += 1,
126+
"--root" | "--delete" | "--dry-run" | "-n" | "--help" | "-h" => j += 1,
127+
_ if token.starts_with('-') => j += 1,
128+
_ => {
129+
positional_seen += 1;
130+
j += 1;
131+
}
132+
}
133+
}
134+
135+
push_described_values(
136+
&mut out,
137+
&[
138+
("--user", "Run rsync inside container as user"),
139+
("--root", "Run rsync inside container as root"),
140+
("--delete", "Delete files not present at source"),
141+
("--dry-run", "Preview changes without writing"),
142+
("-n", "Alias of --dry-run"),
143+
("--help", "Show help for rsync"),
144+
("-h", "Show help for rsync"),
145+
],
146+
);
147+
push_global_options(&mut out);
148+
149+
if positional_seen == 0 {
150+
out.extend(workspace_ctx.workspace_candidates(None));
151+
}
152+
153+
out
154+
}
155+
71156
fn complete_create(current: &str, args_before: &[String]) -> Vec<Candidate> {
72157
if let Some((option, inline)) = value_option(
73158
args_before,

crates/agent-workspace/src/completion/fixtures/matrix_cases.tsv

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
# name|words(; separated)|cword|expected candidates(; separated)
2-
top-level|agent-workspace-launcher;|1|auth;create;ls;rm;exec;reset;tunnel;--runtime;--help;--version;-h;-V
2+
top-level|agent-workspace-launcher;|1|auth;create;rsync;ls;rm;exec;reset;tunnel;--runtime;--help;--version;-h;-V
33
runtime-value|agent-workspace-launcher;--runtime;|2|container;host
44
runtime-inline-value|agent-workspace-launcher;--runtime=|1|--runtime=container;--runtime=host
55
create-flags|agent-workspace-launcher;create;|2|--name;--image;--ref;--private-repo;--no-work-repos;--no-extras;--no-pull;--help;-h;--runtime
66
create-ref-values|agent-workspace-launcher;create;--ref;|3|origin/main;origin/master
77
create-ref-inline-values|agent-workspace-launcher;create;--ref=|2|--ref=origin/main;--ref=origin/master
8+
rsync-subcommands|agent-workspace-launcher;rsync;|2|push;pull;--help;-h;--runtime
9+
rsync-flags-and-workspace|agent-workspace-launcher;rsync;push;|3|--user;--root;--delete;--dry-run;-n;--help;-h;--runtime;container-ws
10+
rsync-user-values|agent-workspace-launcher;rsync;push;--user;|4|0;root;agent;codex
811
ls-flags|agent-workspace-launcher;ls;|2|--json;--output;--help;-h;--runtime
912
ls-output-values|agent-workspace-launcher;ls;--output;|3|json
1013
ls-output-inline-values|agent-workspace-launcher;ls;--output=|2|--output=json

crates/agent-workspace/src/completion/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,9 @@ mod engine_tests {
235235
.map(|candidate| candidate.value)
236236
.collect();
237237

238-
for expected in ["auth", "create", "ls", "rm", "exec", "reset", "tunnel"] {
238+
for expected in [
239+
"auth", "create", "rsync", "ls", "rm", "exec", "reset", "tunnel",
240+
] {
239241
assert!(values.iter().any(|value| value == expected));
240242
}
241243
}

crates/agent-workspace/src/launcher.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ fn dispatch_host(subcommand: &str, args: &[OsString]) -> i32 {
5858
match subcommand {
5959
"auth" => auth::run(args),
6060
"create" => create::run(args),
61+
"rsync" => {
62+
eprintln!("error: rsync is only available in container runtime");
63+
eprintln!("hint: retry with '--runtime container'");
64+
EXIT_RUNTIME
65+
}
6166
"ls" => ls::run(args),
6267
"rm" => rm::run(args),
6368
"exec" => exec::run(args),

0 commit comments

Comments
 (0)