reallink-cli 0.1.15 → 0.1.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,316 +1,489 @@
1
- use anyhow::Result;
2
- use clap::{ArgAction, Args, Subcommand};
3
- use serde::{Deserialize, Serialize};
4
- use std::path::PathBuf;
5
-
6
- const DEFAULT_UNREAL_PLUGIN_BASE_URL: &str = "https://real-agent.link/plugins/unreal";
7
-
8
- #[derive(Debug, Serialize, Deserialize, Clone)]
9
- #[serde(rename_all = "camelCase")]
10
- pub(crate) struct UnrealLinkRecord {
11
- pub project_id: String,
12
- pub uproject_path: String,
13
- pub project_root: String,
14
- pub engine_root: Option<String>,
15
- pub editor_path: String,
16
- pub created_at_epoch_ms: u128,
17
- pub updated_at_epoch_ms: u128,
18
- }
19
-
20
- #[derive(Debug, Serialize, Deserialize, Clone)]
21
- #[serde(rename_all = "camelCase")]
22
- pub(crate) struct UnrealLinksConfig {
23
- pub version: u32,
24
- pub default_project_id: Option<String>,
25
- pub links: Vec<UnrealLinkRecord>,
26
- }
27
-
28
- impl Default for UnrealLinksConfig {
29
- fn default() -> Self {
30
- Self {
31
- version: 1,
32
- default_project_id: None,
33
- links: Vec::new(),
34
- }
35
- }
36
- }
37
-
38
- #[derive(Debug, Serialize, Deserialize, Clone)]
39
- #[serde(rename_all = "camelCase")]
40
- pub(crate) struct PluginIndexFile {
41
- pub schema_version: Option<u32>,
42
- #[serde(default)]
43
- pub plugins: Vec<PluginIndexPlugin>,
44
- }
45
-
46
- #[derive(Debug, Serialize, Deserialize, Clone)]
47
- #[serde(rename_all = "camelCase")]
48
- pub(crate) struct PluginIndexPlugin {
49
- pub name: String,
50
- pub latest: Option<String>,
51
- #[serde(default)]
52
- pub versions: Vec<PluginIndexVersion>,
53
- }
54
-
55
- #[derive(Debug, Serialize, Deserialize, Clone)]
56
- #[serde(rename_all = "camelCase")]
57
- pub(crate) struct PluginIndexVersion {
58
- pub version: String,
59
- pub archive_url: Option<String>,
60
- pub sha256: Option<String>,
61
- }
62
-
63
- #[derive(Subcommand)]
64
- pub(crate) enum LinkCommands {
65
- Unreal(LinkUnrealArgs),
66
- List,
67
- Use(LinkUseArgs),
68
- Doctor(LinkDoctorArgs),
69
- Paths(LinkPathsArgs),
70
- Remove(LinkRemoveArgs),
71
- Open(LinkOpenArgs),
72
- Run(LinkRunArgs),
73
- Plugin {
74
- #[command(subcommand)]
75
- command: LinkPluginCommands,
76
- },
77
- /// Register a local source folder for a project
78
- Source(LinkSourceArgs),
79
- /// Connect to Reallink and bridge local source to agents
80
- Connect(LinkConnectArgs),
81
- }
82
-
83
- #[derive(Subcommand)]
84
- pub(crate) enum LinkPluginCommands {
85
- Install(LinkPluginInstallArgs),
86
- List(LinkPluginListArgs),
87
- }
88
-
89
- #[derive(Args)]
90
- pub(crate) struct LinkUnrealArgs {
91
- #[arg(long, help = "Remote Reallink project id to bind")]
92
- pub project_id: String,
93
- #[arg(long, help = "Path to .uproject file or a folder containing one")]
94
- pub uproject: PathBuf,
95
- #[arg(long, help = "Path to Unreal Engine root (contains Engine/)")]
96
- pub engine_root: Option<PathBuf>,
97
- #[arg(long, help = "Path to Unreal editor binary (UnrealEditor/UE4Editor)")]
98
- pub editor: Option<PathBuf>,
99
- #[arg(long, help = "Set this link as default for `reallink link open`")]
100
- pub set_default: bool,
101
- #[arg(long, help = "Skip remote project verification (useful when offline)")]
102
- pub no_verify_remote: bool,
103
- #[arg(
104
- long,
105
- help = "Sync a sanitized Unreal link manifest into project assets (.reallink/link/unreal-link.latest.json)"
106
- )]
107
- pub sync_project: bool,
108
- #[arg(
109
- long,
110
- help = "When syncing project manifest, include full local paths (disabled by default)"
111
- )]
112
- pub include_local_paths: bool,
113
- #[arg(long)]
114
- pub base_url: Option<String>,
115
- }
116
-
117
- #[derive(Args)]
118
- pub(crate) struct LinkUseArgs {
119
- #[arg(long)]
120
- pub project_id: String,
121
- }
122
-
123
- #[derive(Args)]
124
- pub(crate) struct LinkRemoveArgs {
125
- #[arg(long)]
126
- pub project_id: Option<String>,
127
- #[arg(long)]
128
- pub uproject: Option<PathBuf>,
129
- }
130
-
131
- #[derive(Args)]
132
- pub(crate) struct LinkOpenArgs {
133
- #[arg(long)]
134
- pub project_id: Option<String>,
135
- #[arg(long)]
136
- pub uproject: Option<PathBuf>,
137
- #[arg(long, help = "Wait for Unreal process to exit")]
138
- pub wait: bool,
139
- #[arg(
140
- long = "arg",
141
- action = ArgAction::Append,
142
- help = "Additional argument forwarded to Unreal editor"
143
- )]
144
- pub extra_arg: Vec<String>,
145
- }
146
-
147
- #[derive(Args)]
148
- pub(crate) struct LinkRunArgs {
149
- #[arg(long)]
150
- pub project_id: Option<String>,
151
- #[arg(long)]
152
- pub uproject: Option<PathBuf>,
153
- #[arg(long, help = "Commandlet name to execute via -run=<name>")]
154
- pub commandlet: Option<String>,
155
- #[arg(long, help = "Add Unreal -log flag")]
156
- pub log: bool,
157
- #[arg(
158
- long,
159
- help = "Add headless-friendly flags (-unattended -nop4 -nosplash -nullrhi)"
160
- )]
161
- pub headless: bool,
162
- #[arg(long, help = "Do not wait for process exit")]
163
- pub no_wait: bool,
164
- #[arg(
165
- long = "arg",
166
- action = ArgAction::Append,
167
- help = "Additional argument forwarded to Unreal editor"
168
- )]
169
- pub extra_arg: Vec<String>,
170
- }
171
-
172
- #[derive(Args)]
173
- pub(crate) struct LinkDoctorArgs {
174
- #[arg(long)]
175
- pub project_id: Option<String>,
176
- #[arg(long)]
177
- pub uproject: Option<PathBuf>,
178
- #[arg(
179
- long,
180
- help = "Verify linked project access against API using saved session"
181
- )]
182
- pub verify_remote: bool,
183
- #[arg(long)]
184
- pub base_url: Option<String>,
185
- }
186
-
187
- #[derive(Args)]
188
- pub(crate) struct LinkPathsArgs {
189
- #[arg(long)]
190
- pub project_id: Option<String>,
191
- #[arg(long)]
192
- pub uproject: Option<PathBuf>,
193
- }
194
-
195
- #[derive(Args)]
196
- pub(crate) struct LinkPluginInstallArgs {
197
- #[arg(long, help = "Plugin name (also used for target folder name)")]
198
- pub name: String,
199
- #[arg(long, default_value = "latest", help = "Plugin version channel or tag")]
200
- pub version: String,
201
- #[arg(long, help = "Install against linked project id")]
202
- pub project_id: Option<String>,
203
- #[arg(long, help = "Install against linked uproject path")]
204
- pub uproject: Option<PathBuf>,
205
- #[arg(
206
- long,
207
- help = "Install into Engine/Plugins/Marketplace instead of Project/Plugins"
208
- )]
209
- pub engine: bool,
210
- #[arg(long, help = "Overwrite existing plugin directory")]
211
- pub force: bool,
212
- #[arg(
213
- long,
214
- help = "Override full plugin zip URL. If omitted, URL is composed from --base-url/--name/--version"
215
- )]
216
- pub url: Option<String>,
217
- #[arg(
218
- long,
219
- default_value = DEFAULT_UNREAL_PLUGIN_BASE_URL,
220
- help = "Public plugin bucket base URL"
221
- )]
222
- pub base_url: String,
223
- #[arg(
224
- long,
225
- help = "Resolve plugin version/url from plugin index instead of using URL template"
226
- )]
227
- pub use_index: bool,
228
- #[arg(
229
- long,
230
- default_value = "index.json",
231
- help = "Plugin index path under base URL (used with --use-index)"
232
- )]
233
- pub index_path: String,
234
- #[arg(long, help = "Expected SHA-256 hex for downloaded plugin zip")]
235
- pub sha256: Option<String>,
236
- }
237
-
238
- #[derive(Args)]
239
- pub(crate) struct LinkPluginListArgs {
240
- #[arg(
241
- long,
242
- default_value = DEFAULT_UNREAL_PLUGIN_BASE_URL,
243
- help = "Public plugin bucket base URL"
244
- )]
245
- pub base_url: String,
246
- #[arg(
247
- long,
248
- default_value = "index.json",
249
- help = "Plugin index path under base URL"
250
- )]
251
- pub index_path: String,
252
- }
253
-
254
- // ---------------------------------------------------------------------------
255
- // Source link args
256
- // ---------------------------------------------------------------------------
257
-
258
- #[derive(Args)]
259
- pub(crate) struct LinkSourceArgs {
260
- #[arg(long, help = "Project ID to bind the source folder to")]
261
- pub project_id: String,
262
- #[arg(long, help = "Local directory path containing source files")]
263
- pub path: PathBuf,
264
- #[arg(long, default_value = "project", help = "Label for this folder (project, engine, etc.)")]
265
- pub label: String,
266
- #[arg(long)]
267
- pub base_url: Option<String>,
268
- }
269
-
270
- #[derive(Args)]
271
- pub(crate) struct LinkConnectArgs {
272
- #[arg(long, help = "Project ID to connect. Uses default if omitted")]
273
- pub project_id: Option<String>,
274
- #[arg(long, help = "Push file index + key source files to R2 on connect")]
275
- pub sync: bool,
276
- #[arg(long, help = "Connect service URL override")]
277
- pub connect_url: Option<String>,
278
- #[arg(long)]
279
- pub base_url: Option<String>,
280
- }
281
-
282
- #[derive(Debug, Serialize, Deserialize, Clone)]
283
- #[serde(rename_all = "camelCase")]
284
- pub(crate) struct SourceLinkRecord {
285
- pub project_id: String,
286
- pub folder_path: String,
287
- pub folder_label: String,
288
- pub created_at_epoch_ms: u128,
289
- }
290
-
291
- #[derive(Debug, Serialize, Deserialize, Clone, Default)]
292
- #[serde(rename_all = "camelCase")]
293
- pub(crate) struct SourceLinksConfig {
294
- pub version: u32,
295
- pub links: Vec<SourceLinkRecord>,
296
- }
297
-
298
- pub(crate) async fn dispatch(client: &reqwest::Client, command: LinkCommands) -> Result<()> {
299
- match command {
300
- LinkCommands::Unreal(args) => crate::link_unreal_command(client, args).await?,
301
- LinkCommands::List => crate::link_list_command().await?,
302
- LinkCommands::Use(args) => crate::link_use_command(args).await?,
303
- LinkCommands::Doctor(args) => crate::link_doctor_command(client, args).await?,
304
- LinkCommands::Paths(args) => crate::link_paths_command(args).await?,
305
- LinkCommands::Remove(args) => crate::link_remove_command(args).await?,
306
- LinkCommands::Open(args) => crate::link_open_command(args).await?,
307
- LinkCommands::Run(args) => crate::link_run_command(args).await?,
308
- LinkCommands::Plugin { command } => match command {
309
- LinkPluginCommands::Install(args) => crate::link_plugin_install_command(client, args).await?,
310
- LinkPluginCommands::List(args) => crate::link_plugin_list_command(client, args).await?,
311
- },
312
- LinkCommands::Source(args) => crate::link_source_command(client, args).await?,
313
- LinkCommands::Connect(args) => crate::link_connect_command(client, args).await?,
314
- }
315
- Ok(())
316
- }
1
+ use anyhow::Result;
2
+ use clap::{ArgAction, Args, Subcommand, ValueEnum};
3
+ use serde::{Deserialize, Serialize};
4
+ use std::path::PathBuf;
5
+
6
+ const DEFAULT_UNREAL_PLUGIN_BASE_URL: &str = "https://real-agent.link/plugins/unreal";
7
+
8
+ #[derive(Debug, Serialize, Deserialize, Clone)]
9
+ #[serde(rename_all = "camelCase")]
10
+ pub(crate) struct UnrealLinkRecord {
11
+ pub project_id: String,
12
+ pub uproject_path: String,
13
+ pub project_root: String,
14
+ pub engine_root: Option<String>,
15
+ pub editor_path: String,
16
+ pub created_at_epoch_ms: u128,
17
+ pub updated_at_epoch_ms: u128,
18
+ }
19
+
20
+ #[derive(Debug, Serialize, Deserialize, Clone)]
21
+ #[serde(rename_all = "camelCase")]
22
+ pub(crate) struct UnrealLinksConfig {
23
+ pub version: u32,
24
+ pub default_project_id: Option<String>,
25
+ pub links: Vec<UnrealLinkRecord>,
26
+ }
27
+
28
+ impl Default for UnrealLinksConfig {
29
+ fn default() -> Self {
30
+ Self {
31
+ version: 1,
32
+ default_project_id: None,
33
+ links: Vec::new(),
34
+ }
35
+ }
36
+ }
37
+
38
+ #[derive(Debug, Serialize, Deserialize, Clone)]
39
+ #[serde(rename_all = "camelCase")]
40
+ pub(crate) struct PluginIndexFile {
41
+ pub schema_version: Option<u32>,
42
+ #[serde(default)]
43
+ pub plugins: Vec<PluginIndexPlugin>,
44
+ }
45
+
46
+ #[derive(Debug, Serialize, Deserialize, Clone)]
47
+ #[serde(rename_all = "camelCase")]
48
+ pub(crate) struct PluginIndexPlugin {
49
+ pub name: String,
50
+ pub latest: Option<String>,
51
+ #[serde(default)]
52
+ pub versions: Vec<PluginIndexVersion>,
53
+ }
54
+
55
+ #[derive(Debug, Serialize, Deserialize, Clone)]
56
+ #[serde(rename_all = "camelCase")]
57
+ pub(crate) struct PluginIndexVersion {
58
+ pub version: String,
59
+ pub archive_url: Option<String>,
60
+ pub sha256: Option<String>,
61
+ }
62
+
63
+ #[derive(Subcommand)]
64
+ pub(crate) enum LinkCommands {
65
+ Unreal(LinkUnrealArgs),
66
+ List,
67
+ Use(LinkUseArgs),
68
+ Doctor(LinkDoctorArgs),
69
+ Paths(LinkPathsArgs),
70
+ Remove(LinkRemoveArgs),
71
+ Open(LinkOpenArgs),
72
+ Run(LinkRunArgs),
73
+ Plugin {
74
+ #[command(subcommand)]
75
+ command: LinkPluginCommands,
76
+ },
77
+ /// Register a local source folder for a project
78
+ Source(LinkSourceArgs),
79
+ /// Connect to Reallink and bridge local source to agents
80
+ Connect(LinkConnectArgs),
81
+ /// Manage P2P signaling sessions for direct CLI-to-CLI transports
82
+ P2p {
83
+ #[command(subcommand)]
84
+ command: LinkP2PCommands,
85
+ },
86
+ }
87
+
88
+ #[derive(Subcommand)]
89
+ pub(crate) enum LinkPluginCommands {
90
+ Install(LinkPluginInstallArgs),
91
+ List(LinkPluginListArgs),
92
+ }
93
+
94
+ #[derive(Subcommand)]
95
+ pub(crate) enum LinkP2PCommands {
96
+ Create(LinkP2PCreateArgs),
97
+ List(LinkP2PListArgs),
98
+ Get(LinkP2PGetArgs),
99
+ Wait(LinkP2PWaitArgs),
100
+ Signal(LinkP2PSignalArgs),
101
+ }
102
+
103
+ #[derive(Args)]
104
+ pub(crate) struct LinkUnrealArgs {
105
+ #[arg(long, help = "Remote Reallink project id to bind")]
106
+ pub project_id: String,
107
+ #[arg(long, help = "Path to .uproject file or a folder containing one")]
108
+ pub uproject: PathBuf,
109
+ #[arg(long, help = "Path to Unreal Engine root (contains Engine/)")]
110
+ pub engine_root: Option<PathBuf>,
111
+ #[arg(long, help = "Path to Unreal editor binary (UnrealEditor/UE4Editor)")]
112
+ pub editor: Option<PathBuf>,
113
+ #[arg(long, help = "Set this link as default for `reallink link open`")]
114
+ pub set_default: bool,
115
+ #[arg(long, help = "Skip remote project verification (useful when offline)")]
116
+ pub no_verify_remote: bool,
117
+ #[arg(
118
+ long,
119
+ help = "Sync a sanitized Unreal link manifest into project assets (.reallink/link/unreal-link.latest.json)"
120
+ )]
121
+ pub sync_project: bool,
122
+ #[arg(
123
+ long,
124
+ help = "When syncing project manifest, include full local paths (disabled by default)"
125
+ )]
126
+ pub include_local_paths: bool,
127
+ #[arg(long)]
128
+ pub base_url: Option<String>,
129
+ }
130
+
131
+ #[derive(Args)]
132
+ pub(crate) struct LinkUseArgs {
133
+ #[arg(long)]
134
+ pub project_id: String,
135
+ }
136
+
137
+ #[derive(Args)]
138
+ pub(crate) struct LinkRemoveArgs {
139
+ #[arg(long)]
140
+ pub project_id: Option<String>,
141
+ #[arg(long)]
142
+ pub uproject: Option<PathBuf>,
143
+ }
144
+
145
+ #[derive(Args)]
146
+ pub(crate) struct LinkOpenArgs {
147
+ #[arg(long)]
148
+ pub project_id: Option<String>,
149
+ #[arg(long)]
150
+ pub uproject: Option<PathBuf>,
151
+ #[arg(long, help = "Wait for Unreal process to exit")]
152
+ pub wait: bool,
153
+ #[arg(
154
+ long = "arg",
155
+ action = ArgAction::Append,
156
+ help = "Additional argument forwarded to Unreal editor"
157
+ )]
158
+ pub extra_arg: Vec<String>,
159
+ }
160
+
161
+ #[derive(Args)]
162
+ pub(crate) struct LinkRunArgs {
163
+ #[arg(long)]
164
+ pub project_id: Option<String>,
165
+ #[arg(long)]
166
+ pub uproject: Option<PathBuf>,
167
+ #[arg(long, help = "Commandlet name to execute via -run=<name>")]
168
+ pub commandlet: Option<String>,
169
+ #[arg(long, help = "Add Unreal -log flag")]
170
+ pub log: bool,
171
+ #[arg(
172
+ long,
173
+ help = "Add headless-friendly flags (-unattended -nop4 -nosplash -nullrhi)"
174
+ )]
175
+ pub headless: bool,
176
+ #[arg(long, help = "Do not wait for process exit")]
177
+ pub no_wait: bool,
178
+ #[arg(
179
+ long = "arg",
180
+ action = ArgAction::Append,
181
+ help = "Additional argument forwarded to Unreal editor"
182
+ )]
183
+ pub extra_arg: Vec<String>,
184
+ }
185
+
186
+ #[derive(Args)]
187
+ pub(crate) struct LinkDoctorArgs {
188
+ #[arg(long)]
189
+ pub project_id: Option<String>,
190
+ #[arg(long)]
191
+ pub uproject: Option<PathBuf>,
192
+ #[arg(
193
+ long,
194
+ help = "Verify linked project access against API using saved session"
195
+ )]
196
+ pub verify_remote: bool,
197
+ #[arg(long)]
198
+ pub base_url: Option<String>,
199
+ }
200
+
201
+ #[derive(Args)]
202
+ pub(crate) struct LinkPathsArgs {
203
+ #[arg(long)]
204
+ pub project_id: Option<String>,
205
+ #[arg(long)]
206
+ pub uproject: Option<PathBuf>,
207
+ }
208
+
209
+ #[derive(Args)]
210
+ pub(crate) struct LinkPluginInstallArgs {
211
+ #[arg(long, help = "Plugin name (also used for target folder name)")]
212
+ pub name: String,
213
+ #[arg(long, default_value = "latest", help = "Plugin version channel or tag")]
214
+ pub version: String,
215
+ #[arg(long, help = "Install against linked project id")]
216
+ pub project_id: Option<String>,
217
+ #[arg(long, help = "Install against linked uproject path")]
218
+ pub uproject: Option<PathBuf>,
219
+ #[arg(
220
+ long,
221
+ help = "Install into Engine/Plugins/Marketplace instead of Project/Plugins"
222
+ )]
223
+ pub engine: bool,
224
+ #[arg(long, help = "Overwrite existing plugin directory")]
225
+ pub force: bool,
226
+ #[arg(
227
+ long,
228
+ help = "Override full plugin zip URL. If omitted, URL is composed from --base-url/--name/--version"
229
+ )]
230
+ pub url: Option<String>,
231
+ #[arg(
232
+ long,
233
+ default_value = DEFAULT_UNREAL_PLUGIN_BASE_URL,
234
+ help = "Public plugin bucket base URL"
235
+ )]
236
+ pub base_url: String,
237
+ #[arg(
238
+ long,
239
+ help = "Resolve plugin version/url from plugin index instead of using URL template"
240
+ )]
241
+ pub use_index: bool,
242
+ #[arg(
243
+ long,
244
+ default_value = "index.json",
245
+ help = "Plugin index path under base URL (used with --use-index)"
246
+ )]
247
+ pub index_path: String,
248
+ #[arg(long, help = "Expected SHA-256 hex for downloaded plugin zip")]
249
+ pub sha256: Option<String>,
250
+ }
251
+
252
+ #[derive(Args)]
253
+ pub(crate) struct LinkPluginListArgs {
254
+ #[arg(
255
+ long,
256
+ default_value = DEFAULT_UNREAL_PLUGIN_BASE_URL,
257
+ help = "Public plugin bucket base URL"
258
+ )]
259
+ pub base_url: String,
260
+ #[arg(
261
+ long,
262
+ default_value = "index.json",
263
+ help = "Plugin index path under base URL"
264
+ )]
265
+ pub index_path: String,
266
+ }
267
+
268
+ // ---------------------------------------------------------------------------
269
+ // Source link args
270
+ // ---------------------------------------------------------------------------
271
+
272
+ #[derive(Args)]
273
+ pub(crate) struct LinkSourceArgs {
274
+ #[arg(long, help = "Project ID to bind the source folder to")]
275
+ pub project_id: String,
276
+ #[arg(long, help = "Local directory path containing source files")]
277
+ pub path: PathBuf,
278
+ #[arg(
279
+ long,
280
+ default_value = "project",
281
+ help = "Label for this folder (project, engine, etc.)"
282
+ )]
283
+ pub label: String,
284
+ #[arg(long)]
285
+ pub base_url: Option<String>,
286
+ }
287
+
288
+ #[derive(Args)]
289
+ pub(crate) struct LinkConnectArgs {
290
+ #[arg(long, help = "Project ID to connect. Uses default if omitted")]
291
+ pub project_id: Option<String>,
292
+ #[arg(
293
+ long,
294
+ help = "Stable workspace identifier for this linked client session"
295
+ )]
296
+ pub workspace_id: Option<String>,
297
+ #[arg(long, help = "Push file index + key source files to R2 on connect")]
298
+ pub sync: bool,
299
+ #[arg(
300
+ long,
301
+ default_value = "websocket",
302
+ help = "Transport used by this linked client session"
303
+ )]
304
+ pub transport: String,
305
+ #[arg(long, help = "Connect service URL override")]
306
+ pub connect_url: Option<String>,
307
+ #[arg(long)]
308
+ pub base_url: Option<String>,
309
+ #[arg(long, help = "Bearer token override for agent/API-token usage")]
310
+ pub access_token: Option<String>,
311
+ }
312
+
313
+ #[derive(Clone, Debug, ValueEnum)]
314
+ pub(crate) enum LinkP2PRole {
315
+ Agent,
316
+ Client,
317
+ }
318
+
319
+ impl LinkP2PRole {
320
+ pub(crate) fn as_api_str(&self) -> &'static str {
321
+ match self {
322
+ Self::Agent => "agent",
323
+ Self::Client => "client",
324
+ }
325
+ }
326
+ }
327
+
328
+ #[derive(Clone, Debug, ValueEnum)]
329
+ pub(crate) enum LinkP2PSignalType {
330
+ Offer,
331
+ Answer,
332
+ Candidate,
333
+ Ready,
334
+ }
335
+
336
+ impl LinkP2PSignalType {
337
+ pub(crate) fn as_api_str(&self) -> &'static str {
338
+ match self {
339
+ Self::Offer => "offer",
340
+ Self::Answer => "answer",
341
+ Self::Candidate => "candidate",
342
+ Self::Ready => "ready",
343
+ }
344
+ }
345
+ }
346
+
347
+ #[derive(Args)]
348
+ pub(crate) struct LinkP2PCreateArgs {
349
+ #[arg(long, help = "Project ID to attach the P2P session to")]
350
+ pub project_id: String,
351
+ #[arg(long, help = "Stable workspace identifier for this P2P session")]
352
+ pub workspace_id: Option<String>,
353
+ #[arg(
354
+ long,
355
+ help = "Optional source clientId already registered under the workspace"
356
+ )]
357
+ pub client_id: Option<String>,
358
+ #[arg(long, help = "Optional peer clientId expected to join this session")]
359
+ pub peer_client_id: Option<String>,
360
+ #[arg(long, help = "Session TTL in seconds")]
361
+ pub ttl_seconds: Option<u64>,
362
+ #[arg(
363
+ long,
364
+ help = "Inline JSON object or @path/to/file.json with additional metadata"
365
+ )]
366
+ pub metadata: Option<String>,
367
+ #[arg(long)]
368
+ pub base_url: Option<String>,
369
+ #[arg(long, help = "Bearer token override for agent/API-token usage")]
370
+ pub access_token: Option<String>,
371
+ }
372
+
373
+ #[derive(Args)]
374
+ pub(crate) struct LinkP2PGetArgs {
375
+ #[arg(long, help = "Project ID owning the P2P session")]
376
+ pub project_id: String,
377
+ #[arg(long, help = "P2P session identifier")]
378
+ pub session_id: String,
379
+ #[arg(long)]
380
+ pub base_url: Option<String>,
381
+ #[arg(long, help = "Bearer token override for agent/API-token usage")]
382
+ pub access_token: Option<String>,
383
+ }
384
+
385
+ #[derive(Args)]
386
+ pub(crate) struct LinkP2PListArgs {
387
+ #[arg(long, help = "Project ID owning the P2P sessions")]
388
+ pub project_id: String,
389
+ #[arg(long)]
390
+ pub workspace_id: Option<String>,
391
+ #[arg(long)]
392
+ pub client_id: Option<String>,
393
+ #[arg(long)]
394
+ pub status: Option<String>,
395
+ #[arg(long, default_value_t = 50)]
396
+ pub limit: u32,
397
+ #[arg(long)]
398
+ pub base_url: Option<String>,
399
+ #[arg(long, help = "Bearer token override for agent/API-token usage")]
400
+ pub access_token: Option<String>,
401
+ }
402
+
403
+ #[derive(Args)]
404
+ pub(crate) struct LinkP2PWaitArgs {
405
+ #[arg(long, help = "Project ID owning the P2P session")]
406
+ pub project_id: String,
407
+ #[arg(long, help = "P2P session identifier")]
408
+ pub session_id: String,
409
+ #[arg(long, default_value = "ready")]
410
+ pub status: String,
411
+ #[arg(long, default_value_t = 30_000)]
412
+ pub timeout_ms: u64,
413
+ #[arg(long, default_value_t = 1_000)]
414
+ pub poll_interval_ms: u64,
415
+ #[arg(long)]
416
+ pub base_url: Option<String>,
417
+ #[arg(long, help = "Bearer token override for agent/API-token usage")]
418
+ pub access_token: Option<String>,
419
+ }
420
+
421
+ #[derive(Args)]
422
+ pub(crate) struct LinkP2PSignalArgs {
423
+ #[arg(long, help = "Project ID owning the P2P session")]
424
+ pub project_id: String,
425
+ #[arg(long, help = "P2P session identifier")]
426
+ pub session_id: String,
427
+ #[arg(long, value_enum)]
428
+ pub role: LinkP2PRole,
429
+ #[arg(long = "signal-type", value_enum)]
430
+ pub signal_type: LinkP2PSignalType,
431
+ #[arg(
432
+ long,
433
+ help = "Inline JSON object or @path/to/file.json carrying the signal payload"
434
+ )]
435
+ pub payload: String,
436
+ #[arg(long, help = "Idempotency key for this signal")]
437
+ pub signal_id: Option<String>,
438
+ #[arg(long, help = "Optional registered clientId originating this signal")]
439
+ pub from_client_id: Option<String>,
440
+ #[arg(long)]
441
+ pub base_url: Option<String>,
442
+ #[arg(long, help = "Bearer token override for agent/API-token usage")]
443
+ pub access_token: Option<String>,
444
+ }
445
+
446
+ #[derive(Debug, Serialize, Deserialize, Clone)]
447
+ #[serde(rename_all = "camelCase")]
448
+ pub(crate) struct SourceLinkRecord {
449
+ pub project_id: String,
450
+ pub folder_path: String,
451
+ pub folder_label: String,
452
+ pub created_at_epoch_ms: u128,
453
+ }
454
+
455
+ #[derive(Debug, Serialize, Deserialize, Clone, Default)]
456
+ #[serde(rename_all = "camelCase")]
457
+ pub(crate) struct SourceLinksConfig {
458
+ pub version: u32,
459
+ pub links: Vec<SourceLinkRecord>,
460
+ }
461
+
462
+ pub(crate) async fn dispatch(client: &reqwest::Client, command: LinkCommands) -> Result<()> {
463
+ match command {
464
+ LinkCommands::Unreal(args) => crate::link_unreal_command(client, args).await?,
465
+ LinkCommands::List => crate::link_list_command().await?,
466
+ LinkCommands::Use(args) => crate::link_use_command(args).await?,
467
+ LinkCommands::Doctor(args) => crate::link_doctor_command(client, args).await?,
468
+ LinkCommands::Paths(args) => crate::link_paths_command(args).await?,
469
+ LinkCommands::Remove(args) => crate::link_remove_command(args).await?,
470
+ LinkCommands::Open(args) => crate::link_open_command(args).await?,
471
+ LinkCommands::Run(args) => crate::link_run_command(args).await?,
472
+ LinkCommands::Plugin { command } => match command {
473
+ LinkPluginCommands::Install(args) => {
474
+ crate::link_plugin_install_command(client, args).await?
475
+ }
476
+ LinkPluginCommands::List(args) => crate::link_plugin_list_command(client, args).await?,
477
+ },
478
+ LinkCommands::Source(args) => crate::link_source_command(client, args).await?,
479
+ LinkCommands::Connect(args) => crate::link_connect_command(client, args).await?,
480
+ LinkCommands::P2p { command } => match command {
481
+ LinkP2PCommands::Create(args) => crate::link_p2p_create_command(client, args).await?,
482
+ LinkP2PCommands::List(args) => crate::link_p2p_list_command(client, args).await?,
483
+ LinkP2PCommands::Get(args) => crate::link_p2p_get_command(client, args).await?,
484
+ LinkP2PCommands::Wait(args) => crate::link_p2p_wait_command(client, args).await?,
485
+ LinkP2PCommands::Signal(args) => crate::link_p2p_signal_command(client, args).await?,
486
+ },
487
+ }
488
+ Ok(())
489
+ }