@runloop/rl-cli 1.8.0 → 1.10.0
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.
- package/README.md +21 -7
- package/dist/cli.js +0 -0
- package/dist/commands/blueprint/delete.js +21 -0
- package/dist/commands/blueprint/list.js +226 -174
- package/dist/commands/blueprint/prune.js +13 -28
- package/dist/commands/devbox/create.js +41 -0
- package/dist/commands/devbox/list.js +142 -110
- package/dist/commands/devbox/rsync.js +69 -41
- package/dist/commands/devbox/scp.js +180 -39
- package/dist/commands/devbox/tunnel.js +4 -19
- package/dist/commands/gateway-config/create.js +53 -0
- package/dist/commands/gateway-config/delete.js +21 -0
- package/dist/commands/gateway-config/get.js +18 -0
- package/dist/commands/gateway-config/list.js +493 -0
- package/dist/commands/gateway-config/update.js +70 -0
- package/dist/commands/snapshot/list.js +11 -2
- package/dist/commands/snapshot/prune.js +265 -0
- package/dist/components/BenchmarkMenu.js +23 -3
- package/dist/components/DetailedInfoView.js +20 -0
- package/dist/components/DevboxActionsMenu.js +26 -62
- package/dist/components/DevboxCreatePage.js +763 -15
- package/dist/components/DevboxDetailPage.js +73 -24
- package/dist/components/GatewayConfigCreatePage.js +272 -0
- package/dist/components/LogsViewer.js +6 -40
- package/dist/components/ResourceDetailPage.js +143 -160
- package/dist/components/ResourceListView.js +3 -33
- package/dist/components/ResourcePicker.js +234 -0
- package/dist/components/SecretCreatePage.js +71 -27
- package/dist/components/SettingsMenu.js +12 -2
- package/dist/components/StateHistory.js +1 -20
- package/dist/components/StatusBadge.js +9 -2
- package/dist/components/StreamingLogsViewer.js +8 -42
- package/dist/components/form/FormTextInput.js +4 -2
- package/dist/components/resourceDetailTypes.js +18 -0
- package/dist/hooks/useInputHandler.js +103 -0
- package/dist/router/Router.js +79 -2
- package/dist/screens/BenchmarkDetailScreen.js +163 -0
- package/dist/screens/BenchmarkJobCreateScreen.js +524 -0
- package/dist/screens/BenchmarkJobDetailScreen.js +614 -0
- package/dist/screens/BenchmarkJobListScreen.js +479 -0
- package/dist/screens/BenchmarkListScreen.js +266 -0
- package/dist/screens/BenchmarkMenuScreen.js +6 -0
- package/dist/screens/BenchmarkRunDetailScreen.js +258 -22
- package/dist/screens/BenchmarkRunListScreen.js +21 -1
- package/dist/screens/BlueprintDetailScreen.js +5 -1
- package/dist/screens/DevboxCreateScreen.js +2 -2
- package/dist/screens/GatewayConfigDetailScreen.js +236 -0
- package/dist/screens/GatewayConfigListScreen.js +7 -0
- package/dist/screens/ScenarioRunDetailScreen.js +6 -0
- package/dist/screens/SecretDetailScreen.js +26 -2
- package/dist/screens/SettingsMenuScreen.js +3 -0
- package/dist/screens/SnapshotDetailScreen.js +6 -0
- package/dist/services/agentService.js +42 -0
- package/dist/services/benchmarkJobService.js +122 -0
- package/dist/services/benchmarkService.js +47 -0
- package/dist/services/gatewayConfigService.js +153 -0
- package/dist/services/scenarioService.js +34 -0
- package/dist/store/benchmarkJobStore.js +66 -0
- package/dist/store/benchmarkStore.js +63 -0
- package/dist/store/gatewayConfigStore.js +83 -0
- package/dist/utils/browser.js +22 -0
- package/dist/utils/clipboard.js +41 -0
- package/dist/utils/commands.js +105 -9
- package/dist/utils/gatewayConfigValidation.js +58 -0
- package/dist/utils/time.js +121 -0
- package/package.json +43 -43
|
@@ -42,6 +42,33 @@ function parseCodeMounts(codeMounts) {
|
|
|
42
42
|
}
|
|
43
43
|
});
|
|
44
44
|
}
|
|
45
|
+
// Parse gateways from ENV_PREFIX=gateway,secret format
|
|
46
|
+
function parseGateways(gateways) {
|
|
47
|
+
const result = {};
|
|
48
|
+
for (const gateway of gateways) {
|
|
49
|
+
const eqIndex = gateway.indexOf("=");
|
|
50
|
+
if (eqIndex === -1) {
|
|
51
|
+
throw new Error(`Invalid gateway format: ${gateway}. Expected ENV_PREFIX=gateway_id_or_name,secret_id_or_name`);
|
|
52
|
+
}
|
|
53
|
+
const envPrefix = gateway.substring(0, eqIndex);
|
|
54
|
+
const valueStr = gateway.substring(eqIndex + 1);
|
|
55
|
+
// Split by comma to get gateway and secret
|
|
56
|
+
const commaIndex = valueStr.indexOf(",");
|
|
57
|
+
if (commaIndex === -1) {
|
|
58
|
+
throw new Error(`Invalid gateway format: ${gateway}. Expected ENV_PREFIX=gateway_id_or_name,secret_id_or_name`);
|
|
59
|
+
}
|
|
60
|
+
const gatewayIdOrName = valueStr.substring(0, commaIndex);
|
|
61
|
+
const secretIdOrName = valueStr.substring(commaIndex + 1);
|
|
62
|
+
if (!envPrefix || !gatewayIdOrName || !secretIdOrName) {
|
|
63
|
+
throw new Error(`Invalid gateway format: ${gateway}. Expected ENV_PREFIX=gateway_id_or_name,secret_id_or_name`);
|
|
64
|
+
}
|
|
65
|
+
result[envPrefix] = {
|
|
66
|
+
gateway: gatewayIdOrName,
|
|
67
|
+
secret: secretIdOrName,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
45
72
|
export async function createDevbox(options = {}) {
|
|
46
73
|
try {
|
|
47
74
|
const client = getClient();
|
|
@@ -65,6 +92,10 @@ export async function createDevbox(options = {}) {
|
|
|
65
92
|
(!options.idleTime && options.idleAction)) {
|
|
66
93
|
outputError("Both --idle-time and --idle-action must be specified together");
|
|
67
94
|
}
|
|
95
|
+
// Validate tunnel option
|
|
96
|
+
if (options.tunnel && !["open", "authenticated"].includes(options.tunnel)) {
|
|
97
|
+
outputError("Invalid tunnel mode. Must be either 'open' or 'authenticated'");
|
|
98
|
+
}
|
|
68
99
|
// Build launch parameters
|
|
69
100
|
const launchParameters = {};
|
|
70
101
|
if (options.resources) {
|
|
@@ -128,6 +159,16 @@ export async function createDevbox(options = {}) {
|
|
|
128
159
|
if (options.secrets && options.secrets.length > 0) {
|
|
129
160
|
createRequest.secrets = parseSecrets(options.secrets);
|
|
130
161
|
}
|
|
162
|
+
// Handle tunnel configuration
|
|
163
|
+
if (options.tunnel) {
|
|
164
|
+
createRequest.tunnel = {
|
|
165
|
+
auth_mode: options.tunnel,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
// Handle gateways
|
|
169
|
+
if (options.gateways && options.gateways.length > 0) {
|
|
170
|
+
createRequest.gateways = parseGateways(options.gateways);
|
|
171
|
+
}
|
|
131
172
|
if (Object.keys(launchParameters).length > 0) {
|
|
132
173
|
createRequest.launch_parameters = launchParameters;
|
|
133
174
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import React from "react";
|
|
3
|
-
import { Box, Text,
|
|
3
|
+
import { Box, Text, useApp } from "ink";
|
|
4
4
|
import figures from "figures";
|
|
5
5
|
import { getClient } from "../../utils/client.js";
|
|
6
6
|
import { SpinnerComponent } from "../../components/Spinner.js";
|
|
@@ -21,6 +21,8 @@ import { useViewportHeight } from "../../hooks/useViewportHeight.js";
|
|
|
21
21
|
import { useExitOnCtrlC } from "../../hooks/useExitOnCtrlC.js";
|
|
22
22
|
import { useCursorPagination } from "../../hooks/useCursorPagination.js";
|
|
23
23
|
import { useListSearch } from "../../hooks/useListSearch.js";
|
|
24
|
+
import { openInBrowser } from "../../utils/browser.js";
|
|
25
|
+
import { useInputHandler, } from "../../hooks/useInputHandler.js";
|
|
24
26
|
import { colors } from "../../utils/theme.js";
|
|
25
27
|
import { useDevboxStore } from "../../store/devboxStore.js";
|
|
26
28
|
const DEFAULT_PAGE_SIZE = 10;
|
|
@@ -302,8 +304,10 @@ const ListDevboxesUI = ({ status, onBack, onExit, onNavigateToDetail, }) => {
|
|
|
302
304
|
const startIndex = currentPage * PAGE_SIZE;
|
|
303
305
|
const endIndex = startIndex + devboxes.length;
|
|
304
306
|
// Filter operations based on devbox status
|
|
307
|
+
const hasTunnel = !!(selectedDevbox?.tunnel && selectedDevbox.tunnel.tunnel_key);
|
|
305
308
|
const operations = selectedDevbox
|
|
306
|
-
? allOperations
|
|
309
|
+
? allOperations
|
|
310
|
+
.filter((op) => {
|
|
307
311
|
const devboxStatus = selectedDevbox.status;
|
|
308
312
|
if (devboxStatus === "suspended") {
|
|
309
313
|
return op.key === "resume" || op.key === "logs";
|
|
@@ -318,127 +322,155 @@ const ListDevboxesUI = ({ status, onBack, onExit, onNavigateToDetail, }) => {
|
|
|
318
322
|
}
|
|
319
323
|
return op.key === "logs" || op.key === "delete";
|
|
320
324
|
})
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
325
|
+
.map((op) => {
|
|
326
|
+
// Dynamic tunnel label based on whether tunnel is active
|
|
327
|
+
if (op.key === "tunnel") {
|
|
328
|
+
return hasTunnel
|
|
329
|
+
? {
|
|
330
|
+
...op,
|
|
331
|
+
label: "Tunnel (Active)",
|
|
332
|
+
color: colors.success,
|
|
333
|
+
icon: figures.tick,
|
|
334
|
+
}
|
|
335
|
+
: op;
|
|
328
336
|
}
|
|
337
|
+
return op;
|
|
338
|
+
})
|
|
339
|
+
: allOperations;
|
|
340
|
+
const closePopup = React.useCallback(() => {
|
|
341
|
+
setShowPopup(false);
|
|
342
|
+
setSelectedOperation(0);
|
|
343
|
+
}, []);
|
|
344
|
+
const handleListEscape = React.useCallback(() => {
|
|
345
|
+
if (search.handleEscape())
|
|
329
346
|
return;
|
|
347
|
+
if (onBack) {
|
|
348
|
+
onBack();
|
|
330
349
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
return;
|
|
350
|
+
else if (onExit) {
|
|
351
|
+
onExit();
|
|
334
352
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
return;
|
|
353
|
+
else {
|
|
354
|
+
inkExit();
|
|
338
355
|
}
|
|
339
|
-
|
|
340
|
-
|
|
356
|
+
}, [search, onBack, onExit, inkExit]);
|
|
357
|
+
const handleOpenInBrowser = React.useCallback(() => {
|
|
358
|
+
if (!selectedDevbox)
|
|
341
359
|
return;
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
setShowPopup(false);
|
|
347
|
-
setSelectedOperation(0);
|
|
348
|
-
}
|
|
349
|
-
else if (key.upArrow && selectedOperation > 0) {
|
|
350
|
-
setSelectedOperation(selectedOperation - 1);
|
|
351
|
-
}
|
|
352
|
-
else if (key.downArrow && selectedOperation < operations.length - 1) {
|
|
353
|
-
setSelectedOperation(selectedOperation + 1);
|
|
354
|
-
}
|
|
355
|
-
else if (key.return) {
|
|
356
|
-
setShowPopup(false);
|
|
357
|
-
setShowActions(true);
|
|
358
|
-
}
|
|
359
|
-
else if (input) {
|
|
360
|
-
const matchedOpIndex = operations.findIndex((op) => op.shortcut === input);
|
|
361
|
-
if (matchedOpIndex !== -1) {
|
|
362
|
-
setSelectedOperation(matchedOpIndex);
|
|
363
|
-
setShowPopup(false);
|
|
364
|
-
setShowActions(true);
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
return;
|
|
368
|
-
}
|
|
369
|
-
// Handle list view
|
|
370
|
-
if (key.upArrow && selectedIndex > 0) {
|
|
371
|
-
setSelectedIndex(selectedIndex - 1);
|
|
372
|
-
}
|
|
373
|
-
else if (key.downArrow && selectedIndex < pageDevboxes - 1) {
|
|
374
|
-
setSelectedIndex(selectedIndex + 1);
|
|
375
|
-
}
|
|
376
|
-
else if ((input === "n" || key.rightArrow) &&
|
|
377
|
-
!loading &&
|
|
378
|
-
!navigating &&
|
|
379
|
-
hasMore) {
|
|
360
|
+
openInBrowser(getDevboxUrl(selectedDevbox.id));
|
|
361
|
+
}, [selectedDevbox]);
|
|
362
|
+
const goToNextPage = React.useCallback(() => {
|
|
363
|
+
if (!loading && !navigating && hasMore) {
|
|
380
364
|
nextPage();
|
|
381
365
|
setSelectedIndex(0);
|
|
382
366
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
hasPrev) {
|
|
367
|
+
}, [loading, navigating, hasMore, nextPage]);
|
|
368
|
+
const goToPrevPage = React.useCallback(() => {
|
|
369
|
+
if (!loading && !navigating && hasPrev) {
|
|
387
370
|
prevPage();
|
|
388
371
|
setSelectedIndex(0);
|
|
389
372
|
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
373
|
+
}, [loading, navigating, hasPrev, prevPage]);
|
|
374
|
+
const inputModes = React.useMemo(() => [
|
|
375
|
+
// Search mode: only escape to cancel, swallow everything else
|
|
376
|
+
{
|
|
377
|
+
name: "search",
|
|
378
|
+
active: () => search.searchMode,
|
|
379
|
+
bindings: {
|
|
380
|
+
escape: () => search.cancelSearch(),
|
|
381
|
+
},
|
|
382
|
+
captureAll: true,
|
|
383
|
+
},
|
|
384
|
+
// Subview guards: swallow all input when a child view is active
|
|
385
|
+
{
|
|
386
|
+
name: "subviews",
|
|
387
|
+
active: () => showDetails || showCreate || showActions,
|
|
388
|
+
bindings: {},
|
|
389
|
+
captureAll: true,
|
|
390
|
+
},
|
|
391
|
+
// Popup navigation
|
|
392
|
+
{
|
|
393
|
+
name: "popup",
|
|
394
|
+
active: () => showPopup,
|
|
395
|
+
bindings: {
|
|
396
|
+
escape: closePopup,
|
|
397
|
+
q: closePopup,
|
|
398
|
+
up: () => {
|
|
399
|
+
if (selectedOperation > 0)
|
|
400
|
+
setSelectedOperation(selectedOperation - 1);
|
|
401
|
+
},
|
|
402
|
+
down: () => {
|
|
403
|
+
if (selectedOperation < operations.length - 1)
|
|
404
|
+
setSelectedOperation(selectedOperation + 1);
|
|
405
|
+
},
|
|
406
|
+
enter: () => {
|
|
407
|
+
setShowPopup(false);
|
|
408
|
+
setShowActions(true);
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
onUnmatched: (input) => {
|
|
412
|
+
const idx = operations.findIndex((op) => op.shortcut === input);
|
|
413
|
+
if (idx !== -1) {
|
|
414
|
+
setSelectedOperation(idx);
|
|
415
|
+
setShowPopup(false);
|
|
416
|
+
setShowActions(true);
|
|
419
417
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
// List navigation (default mode)
|
|
421
|
+
{
|
|
422
|
+
name: "list",
|
|
423
|
+
active: () => true,
|
|
424
|
+
bindings: {
|
|
425
|
+
up: () => {
|
|
426
|
+
if (selectedIndex > 0)
|
|
427
|
+
setSelectedIndex(selectedIndex - 1);
|
|
428
|
+
},
|
|
429
|
+
down: () => {
|
|
430
|
+
if (selectedIndex < devboxes.length - 1)
|
|
431
|
+
setSelectedIndex(selectedIndex + 1);
|
|
432
|
+
},
|
|
433
|
+
n: goToNextPage,
|
|
434
|
+
right: goToNextPage,
|
|
435
|
+
p: goToPrevPage,
|
|
436
|
+
left: goToPrevPage,
|
|
437
|
+
enter: () => {
|
|
438
|
+
if (onNavigateToDetail && selectedDevbox) {
|
|
439
|
+
onNavigateToDetail(selectedDevbox.id);
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
setShowDetails(true);
|
|
443
|
+
}
|
|
444
|
+
},
|
|
445
|
+
a: () => {
|
|
446
|
+
setShowPopup(true);
|
|
447
|
+
setSelectedOperation(0);
|
|
448
|
+
},
|
|
449
|
+
c: () => setShowCreate(true),
|
|
450
|
+
o: handleOpenInBrowser,
|
|
451
|
+
"/": () => search.enterSearchMode(),
|
|
452
|
+
escape: handleListEscape,
|
|
453
|
+
},
|
|
454
|
+
},
|
|
455
|
+
], [
|
|
456
|
+
search,
|
|
457
|
+
showDetails,
|
|
458
|
+
showCreate,
|
|
459
|
+
showActions,
|
|
460
|
+
showPopup,
|
|
461
|
+
closePopup,
|
|
462
|
+
selectedOperation,
|
|
463
|
+
operations,
|
|
464
|
+
selectedIndex,
|
|
465
|
+
devboxes.length,
|
|
466
|
+
goToNextPage,
|
|
467
|
+
goToPrevPage,
|
|
468
|
+
onNavigateToDetail,
|
|
469
|
+
selectedDevbox,
|
|
470
|
+
handleOpenInBrowser,
|
|
471
|
+
handleListEscape,
|
|
472
|
+
]);
|
|
473
|
+
useInputHandler(inputModes);
|
|
442
474
|
// Create view
|
|
443
475
|
if (showCreate) {
|
|
444
476
|
return (_jsx(DevboxCreatePage, { onBack: () => {
|
|
@@ -1,62 +1,90 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Rsync files to/from devbox command
|
|
3
|
+
*
|
|
4
|
+
* Supports standard rsync-like syntax where the devbox ID (dbx_*) is used as a hostname:
|
|
5
|
+
* rli devbox rsync dbx_abc123:/remote/path ./local/path # download
|
|
6
|
+
* rli devbox rsync ./local/path dbx_abc123:/remote/path # upload
|
|
7
|
+
* rli devbox rsync root@dbx_abc123:/remote/path ./local/path # explicit user
|
|
8
|
+
*
|
|
9
|
+
* If no user is specified for a remote path, the devbox's configured user is used.
|
|
10
|
+
* Paths without a dbx_ hostname are treated as local paths.
|
|
3
11
|
*/
|
|
4
|
-
import {
|
|
12
|
+
import { execFile } from "child_process";
|
|
5
13
|
import { promisify } from "util";
|
|
6
|
-
import { getClient } from "../../utils/client.js";
|
|
7
14
|
import { output, outputError } from "../../utils/output.js";
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
15
|
+
import { getProxyCommand, checkSSHTools } from "../../utils/ssh.js";
|
|
16
|
+
import { parseSCPPath, resolveRemote } from "./scp.js";
|
|
17
|
+
const execFileAsync = promisify(execFile);
|
|
18
|
+
/**
|
|
19
|
+
* Build the rsync command for a single-remote transfer (local <-> devbox).
|
|
20
|
+
*/
|
|
21
|
+
export function buildRsyncCommand(opts) {
|
|
22
|
+
// Rsync re-splits the -e value on whitespace internally, so the
|
|
23
|
+
// ProxyCommand (which contains spaces) must be single-quoted.
|
|
24
|
+
const sshTransport = `ssh -i ${opts.sshInfo.keyfilePath} -o 'ProxyCommand=${opts.proxyCommand}' -o StrictHostKeyChecking=no`;
|
|
25
|
+
const rsyncCommand = [
|
|
26
|
+
"rsync",
|
|
27
|
+
"-vrz", // v: verbose, r: recursive, z: compress
|
|
28
|
+
"-e",
|
|
29
|
+
sshTransport,
|
|
30
|
+
];
|
|
31
|
+
if (opts.rsyncOptions) {
|
|
32
|
+
rsyncCommand.push(...opts.rsyncOptions.split(" "));
|
|
33
|
+
}
|
|
34
|
+
// Build src argument
|
|
35
|
+
if (opts.parsedSrc.isRemote) {
|
|
36
|
+
const user = opts.parsedSrc.user || opts.defaultUser;
|
|
37
|
+
rsyncCommand.push(`${user}@${opts.sshInfo.url}:${opts.parsedSrc.path}`);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
rsyncCommand.push(opts.parsedSrc.path);
|
|
41
|
+
}
|
|
42
|
+
// Build dst argument
|
|
43
|
+
if (opts.parsedDst.isRemote) {
|
|
44
|
+
const user = opts.parsedDst.user || opts.defaultUser;
|
|
45
|
+
rsyncCommand.push(`${user}@${opts.sshInfo.url}:${opts.parsedDst.path}`);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
rsyncCommand.push(opts.parsedDst.path);
|
|
49
|
+
}
|
|
50
|
+
return rsyncCommand;
|
|
51
|
+
}
|
|
52
|
+
export async function rsyncFiles(src, dst, options) {
|
|
11
53
|
try {
|
|
12
54
|
// Check if SSH tools are available
|
|
13
55
|
const sshToolsAvailable = await checkSSHTools();
|
|
14
56
|
if (!sshToolsAvailable) {
|
|
15
57
|
outputError("SSH tools (ssh, rsync, openssl) are not available on this system");
|
|
16
58
|
}
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
// Get SSH key
|
|
22
|
-
const sshInfo = await getSSHKey(devboxId);
|
|
23
|
-
if (!sshInfo) {
|
|
24
|
-
outputError("Failed to create SSH key");
|
|
25
|
-
}
|
|
26
|
-
const proxyCommand = getProxyCommand();
|
|
27
|
-
const sshOptions = `-i ${sshInfo.keyfilePath} -o ProxyCommand='${proxyCommand}' -o StrictHostKeyChecking=no`;
|
|
28
|
-
const rsyncCommand = [
|
|
29
|
-
"rsync",
|
|
30
|
-
"-vrz", // v: verbose, r: recursive, z: compress
|
|
31
|
-
"-e",
|
|
32
|
-
`"ssh ${sshOptions}"`,
|
|
33
|
-
];
|
|
34
|
-
if (options.rsyncOptions) {
|
|
35
|
-
rsyncCommand.push(...options.rsyncOptions.split(" "));
|
|
36
|
-
}
|
|
37
|
-
// Handle remote paths (starting with :)
|
|
38
|
-
if (options.src.startsWith(":")) {
|
|
39
|
-
rsyncCommand.push(`${user}@${sshInfo.url}:${options.src.slice(1)}`);
|
|
40
|
-
rsyncCommand.push(options.dst);
|
|
59
|
+
const parsedSrc = parseSCPPath(src);
|
|
60
|
+
const parsedDst = parseSCPPath(dst);
|
|
61
|
+
if (!parsedSrc.isRemote && !parsedDst.isRemote) {
|
|
62
|
+
outputError("At least one of src or dst must be a remote devbox path (e.g. dbx_<id>:/path)");
|
|
41
63
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (options.dst.startsWith(":")) {
|
|
45
|
-
rsyncCommand.push(`${user}@${sshInfo.url}:${options.dst.slice(1)}`);
|
|
46
|
-
}
|
|
47
|
-
else {
|
|
48
|
-
rsyncCommand.push(options.dst);
|
|
49
|
-
}
|
|
64
|
+
if (parsedSrc.isRemote && parsedDst.isRemote) {
|
|
65
|
+
outputError("Devbox-to-devbox rsync is not supported. Only one side can be a remote devbox path.");
|
|
50
66
|
}
|
|
51
|
-
|
|
67
|
+
const devboxId = parsedSrc.isRemote ? parsedSrc.host : parsedDst.host;
|
|
68
|
+
const remote = await resolveRemote(devboxId);
|
|
69
|
+
const proxyCommand = getProxyCommand();
|
|
70
|
+
const rsyncCommand = buildRsyncCommand({
|
|
71
|
+
sshInfo: remote.sshInfo,
|
|
72
|
+
proxyCommand,
|
|
73
|
+
parsedSrc,
|
|
74
|
+
parsedDst,
|
|
75
|
+
defaultUser: remote.defaultUser,
|
|
76
|
+
rsyncOptions: options.rsyncOptions,
|
|
77
|
+
});
|
|
78
|
+
const [cmd, ...args] = rsyncCommand;
|
|
79
|
+
await execFileAsync(cmd, args);
|
|
52
80
|
// Default: just output the destination for easy scripting
|
|
53
81
|
if (!options.output || options.output === "text") {
|
|
54
|
-
console.log(
|
|
82
|
+
console.log(dst);
|
|
55
83
|
}
|
|
56
84
|
else {
|
|
57
85
|
output({
|
|
58
|
-
source:
|
|
59
|
-
destination:
|
|
86
|
+
source: src,
|
|
87
|
+
destination: dst,
|
|
60
88
|
}, { format: options.output, defaultFormat: "json" });
|
|
61
89
|
}
|
|
62
90
|
}
|