@runloop/rl-cli 1.5.0 → 1.7.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.
@@ -13,6 +13,7 @@ import { NavigationTips } from "../../components/NavigationTips.js";
13
13
  import { createTextColumn, Table } from "../../components/Table.js";
14
14
  import { ActionsPopup } from "../../components/ActionsPopup.js";
15
15
  import { formatTimeAgo } from "../../components/ResourceListView.js";
16
+ import { SearchBar } from "../../components/SearchBar.js";
16
17
  import { output, outputError } from "../../utils/output.js";
17
18
  import { getBlueprintUrl } from "../../utils/url.js";
18
19
  import { colors } from "../../utils/theme.js";
@@ -21,6 +22,7 @@ import { DevboxCreatePage } from "../../components/DevboxCreatePage.js";
21
22
  import { useExitOnCtrlC } from "../../hooks/useExitOnCtrlC.js";
22
23
  import { useViewportHeight } from "../../hooks/useViewportHeight.js";
23
24
  import { useCursorPagination } from "../../hooks/useCursorPagination.js";
25
+ import { useListSearch } from "../../hooks/useListSearch.js";
24
26
  import { useNavigation } from "../../store/navigationStore.js";
25
27
  import { ConfirmationPrompt } from "../../components/ConfirmationPrompt.js";
26
28
  const DEFAULT_PAGE_SIZE = 10;
@@ -39,8 +41,13 @@ const ListBlueprintsUI = ({ onBack, onExit, }) => {
39
41
  const [selectedIndex, setSelectedIndex] = React.useState(0);
40
42
  const [showPopup, setShowPopup] = React.useState(false);
41
43
  const { navigate } = useNavigation();
44
+ // Search state
45
+ const search = useListSearch({
46
+ onSearchSubmit: () => setSelectedIndex(0),
47
+ onSearchClear: () => setSelectedIndex(0),
48
+ });
42
49
  // Calculate overhead for viewport height
43
- const overhead = 13;
50
+ const overhead = 13 + search.getSearchOverhead();
44
51
  const { viewportHeight, terminalWidth } = useViewportHeight({
45
52
  overhead,
46
53
  minHeight: 5,
@@ -70,6 +77,9 @@ const ListBlueprintsUI = ({ onBack, onExit, }) => {
70
77
  if (params.startingAt) {
71
78
  queryParams.starting_after = params.startingAt;
72
79
  }
80
+ if (search.submittedSearchQuery) {
81
+ queryParams.search = search.submittedSearchQuery;
82
+ }
73
83
  // Fetch ONE page only
74
84
  const page = (await client.blueprints.list(queryParams));
75
85
  // Extract data and create defensive copies
@@ -89,7 +99,7 @@ const ListBlueprintsUI = ({ onBack, onExit, }) => {
89
99
  totalCount: page.total_count || pageBlueprints.length,
90
100
  };
91
101
  return result;
92
- }, []);
102
+ }, [search.submittedSearchQuery]);
93
103
  // Use the shared pagination hook
94
104
  const { items: blueprints, loading, navigating, error: listError, currentPage, hasMore, hasPrev, totalCount, nextPage, prevPage, } = useCursorPagination({
95
105
  fetchPage,
@@ -99,8 +109,9 @@ const ListBlueprintsUI = ({ onBack, onExit, }) => {
99
109
  pollingEnabled: !showPopup &&
100
110
  !showCreateDevbox &&
101
111
  !executingOperation &&
102
- !showDeleteConfirm,
103
- deps: [PAGE_SIZE],
112
+ !showDeleteConfirm &&
113
+ !search.searchMode,
114
+ deps: [PAGE_SIZE, search.submittedSearchQuery],
104
115
  });
105
116
  // Memoize columns array
106
117
  const blueprintColumns = React.useMemo(() => [
@@ -272,6 +283,13 @@ const ListBlueprintsUI = ({ onBack, onExit, }) => {
272
283
  : allOperations;
273
284
  // Handle input for all views
274
285
  useInput((input, key) => {
286
+ // Handle search mode input
287
+ if (search.searchMode) {
288
+ if (key.escape) {
289
+ search.cancelSearch();
290
+ }
291
+ return;
292
+ }
275
293
  // Handle operation input mode
276
294
  if (executingOperation && !operationResult && !operationError) {
277
295
  // Allow escape/q to cancel any operation, even during loading
@@ -431,7 +449,13 @@ const ListBlueprintsUI = ({ onBack, onExit, }) => {
431
449
  };
432
450
  openBrowser();
433
451
  }
452
+ else if (input === "/") {
453
+ search.enterSearchMode();
454
+ }
434
455
  else if (key.escape) {
456
+ if (search.handleEscape()) {
457
+ return;
458
+ }
435
459
  if (onBack) {
436
460
  onBack();
437
461
  }
@@ -520,7 +544,7 @@ const ListBlueprintsUI = ({ onBack, onExit, }) => {
520
544
  return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Blueprints", active: true }] }), _jsx(ErrorMessage, { message: "Failed to load blueprints", error: listError })] }));
521
545
  }
522
546
  // List view
523
- return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Blueprints", active: true }] }), !showPopup && (_jsx(Table, { data: blueprints, keyExtractor: (blueprint) => blueprint.id, selectedIndex: selectedIndex, title: `blueprints[${totalCount}]`, columns: blueprintColumns, emptyState: _jsxs(Text, { color: colors.textDim, children: [figures.info, " No blueprints found. Try: rli blueprint create"] }) })), !showPopup && (_jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", totalCount] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "total"] }), totalPages > 1 && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), navigating ? (_jsxs(Text, { color: colors.warning, children: [figures.pointer, " Loading page ", currentPage + 1, "..."] })) : (_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Page ", currentPage + 1, " of ", totalPages] }))] })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Showing ", startIndex + 1, "-", endIndex, " of ", totalCount] })] })), showPopup && selectedBlueprintItem && (_jsx(Box, { marginTop: 2, justifyContent: "center", children: _jsx(ActionsPopup, { devbox: selectedBlueprintItem, operations: allOperations.map((op) => ({
547
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Blueprints", active: true }] }), _jsx(SearchBar, { searchMode: search.searchMode, searchQuery: search.searchQuery, submittedSearchQuery: search.submittedSearchQuery, resultCount: totalCount, onSearchChange: search.setSearchQuery, onSearchSubmit: search.submitSearch, placeholder: "Search blueprints..." }), !showPopup && (_jsx(Table, { data: blueprints, keyExtractor: (blueprint) => blueprint.id, selectedIndex: selectedIndex, title: `blueprints[${totalCount}]`, columns: blueprintColumns, emptyState: _jsxs(Text, { color: colors.textDim, children: [figures.info, " No blueprints found. Try: rli blueprint create"] }) })), !showPopup && (_jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", totalCount] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "total"] }), totalPages > 1 && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), navigating ? (_jsxs(Text, { color: colors.warning, children: [figures.pointer, " Loading page ", currentPage + 1, "..."] })) : (_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Page ", currentPage + 1, " of ", totalPages] }))] })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Showing ", startIndex + 1, "-", endIndex, " of ", totalCount] }), search.submittedSearchQuery && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.warning, children: ["Filtered: \"", search.submittedSearchQuery, "\""] })] }))] })), showPopup && selectedBlueprintItem && (_jsx(Box, { marginTop: 2, justifyContent: "center", children: _jsx(ActionsPopup, { devbox: selectedBlueprintItem, operations: allOperations.map((op) => ({
524
548
  key: op.key,
525
549
  label: op.label,
526
550
  color: op.color,
@@ -543,6 +567,7 @@ const ListBlueprintsUI = ({ onBack, onExit, }) => {
543
567
  { key: "Enter", label: "Details" },
544
568
  { key: "a", label: "Actions" },
545
569
  { key: "o", label: "Browser" },
570
+ { key: "/", label: "Search" },
546
571
  { key: "Esc", label: "Back" },
547
572
  ] })] }));
548
573
  };
@@ -17,6 +17,20 @@ function parseEnvVars(envVars) {
17
17
  }
18
18
  return result;
19
19
  }
20
+ // Parse secrets from ENV_VAR=SECRET_NAME format
21
+ function parseSecrets(secrets) {
22
+ const result = {};
23
+ for (const secret of secrets) {
24
+ const eqIndex = secret.indexOf("=");
25
+ if (eqIndex === -1) {
26
+ throw new Error(`Invalid secret format: ${secret}. Expected ENV_VAR=SECRET_NAME`);
27
+ }
28
+ const envVarName = secret.substring(0, eqIndex);
29
+ const secretName = secret.substring(eqIndex + 1);
30
+ result[envVarName] = secretName;
31
+ }
32
+ return result;
33
+ }
20
34
  // Parse code mounts from JSON format
21
35
  function parseCodeMounts(codeMounts) {
22
36
  return codeMounts.map((mount) => {
@@ -110,6 +124,10 @@ export async function createDevbox(options = {}) {
110
124
  if (options.codeMounts && options.codeMounts.length > 0) {
111
125
  createRequest.code_mounts = parseCodeMounts(options.codeMounts);
112
126
  }
127
+ // Handle secrets
128
+ if (options.secrets && options.secrets.length > 0) {
129
+ createRequest.secrets = parseSecrets(options.secrets);
130
+ }
113
131
  if (Object.keys(launchParameters).length > 0) {
114
132
  createRequest.launch_parameters = launchParameters;
115
133
  }
@@ -1,7 +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
3
  import { Box, Text, useInput, useApp } from "ink";
4
- import TextInput from "ink-text-input";
5
4
  import figures from "figures";
6
5
  import { getClient } from "../../utils/client.js";
7
6
  import { SpinnerComponent } from "../../components/Spinner.js";
@@ -11,6 +10,7 @@ import { Breadcrumb } from "../../components/Breadcrumb.js";
11
10
  import { NavigationTips } from "../../components/NavigationTips.js";
12
11
  import { Table, createTextColumn } from "../../components/Table.js";
13
12
  import { formatTimeAgo } from "../../components/ResourceListView.js";
13
+ import { SearchBar } from "../../components/SearchBar.js";
14
14
  import { output, outputError } from "../../utils/output.js";
15
15
  import { DevboxDetailPage } from "../../components/DevboxDetailPage.js";
16
16
  import { DevboxCreatePage } from "../../components/DevboxCreatePage.js";
@@ -20,6 +20,7 @@ import { getDevboxUrl } from "../../utils/url.js";
20
20
  import { useViewportHeight } from "../../hooks/useViewportHeight.js";
21
21
  import { useExitOnCtrlC } from "../../hooks/useExitOnCtrlC.js";
22
22
  import { useCursorPagination } from "../../hooks/useCursorPagination.js";
23
+ import { useListSearch } from "../../hooks/useListSearch.js";
23
24
  import { colors } from "../../utils/theme.js";
24
25
  import { useDevboxStore } from "../../store/devboxStore.js";
25
26
  const DEFAULT_PAGE_SIZE = 10;
@@ -31,9 +32,11 @@ const ListDevboxesUI = ({ status, onBack, onExit, onNavigateToDetail, }) => {
31
32
  const [showActions, setShowActions] = React.useState(false);
32
33
  const [showPopup, setShowPopup] = React.useState(false);
33
34
  const [selectedOperation, setSelectedOperation] = React.useState(0);
34
- const [searchMode, setSearchMode] = React.useState(false);
35
- const [searchQuery, setSearchQuery] = React.useState("");
36
- const [submittedSearchQuery, setSubmittedSearchQuery] = React.useState("");
35
+ // Search state using shared hook
36
+ const search = useListSearch({
37
+ onSearchSubmit: () => setSelectedIndex(0),
38
+ onSearchClear: () => setSelectedIndex(0),
39
+ });
37
40
  // Get devbox store setter to sync data for detail screen
38
41
  const setDevboxesInStore = useDevboxStore((state) => state.setDevboxes);
39
42
  // Calculate overhead for viewport height:
@@ -44,7 +47,7 @@ const ListDevboxesUI = ({ status, onBack, onExit, onNavigateToDetail, }) => {
44
47
  // - Help bar (marginTop + content): 2 lines
45
48
  // - Safety buffer for edge cases: 1 line
46
49
  // Total: 13 lines base + 2 if searching
47
- const overhead = 13 + (searchMode || submittedSearchQuery ? 2 : 0);
50
+ const overhead = 13 + search.getSearchOverhead();
48
51
  const { viewportHeight, terminalWidth } = useViewportHeight({
49
52
  overhead,
50
53
  minHeight: 5,
@@ -64,8 +67,8 @@ const ListDevboxesUI = ({ status, onBack, onExit, onNavigateToDetail, }) => {
64
67
  if (status) {
65
68
  queryParams.status = status;
66
69
  }
67
- if (submittedSearchQuery) {
68
- queryParams.search = submittedSearchQuery;
70
+ if (search.submittedSearchQuery) {
71
+ queryParams.search = search.submittedSearchQuery;
69
72
  }
70
73
  // Fetch ONE page only
71
74
  const page = (await client.devboxes.list(queryParams));
@@ -81,15 +84,19 @@ const ListDevboxesUI = ({ status, onBack, onExit, onNavigateToDetail, }) => {
81
84
  totalCount: page.total_count || pageDevboxes.length,
82
85
  };
83
86
  return result;
84
- }, [status, submittedSearchQuery]);
87
+ }, [status, search.submittedSearchQuery]);
85
88
  // Use the shared pagination hook
86
89
  const { items: devboxes, loading, navigating, error, currentPage, hasMore, hasPrev, totalCount, nextPage, prevPage, } = useCursorPagination({
87
90
  fetchPage,
88
91
  pageSize: PAGE_SIZE,
89
92
  getItemId: (devbox) => devbox.id,
90
93
  pollInterval: 2000,
91
- pollingEnabled: !showDetails && !showCreate && !showActions && !showPopup && !searchMode,
92
- deps: [status, submittedSearchQuery, PAGE_SIZE],
94
+ pollingEnabled: !showDetails &&
95
+ !showCreate &&
96
+ !showActions &&
97
+ !showPopup &&
98
+ !search.searchMode,
99
+ deps: [status, search.submittedSearchQuery, PAGE_SIZE],
93
100
  });
94
101
  // Sync devboxes to store for detail screen
95
102
  React.useEffect(() => {
@@ -315,10 +322,9 @@ const ListDevboxesUI = ({ status, onBack, onExit, onNavigateToDetail, }) => {
315
322
  useInput((input, key) => {
316
323
  const pageDevboxes = devboxes.length;
317
324
  // Skip input handling when in search mode - let TextInput handle it
318
- if (searchMode) {
325
+ if (search.searchMode) {
319
326
  if (key.escape) {
320
- setSearchMode(false);
321
- setSearchQuery("");
327
+ search.cancelSearch();
322
328
  }
323
329
  return;
324
330
  }
@@ -416,24 +422,20 @@ const ListDevboxesUI = ({ status, onBack, onExit, onNavigateToDetail, }) => {
416
422
  openBrowser();
417
423
  }
418
424
  else if (input === "/") {
419
- setSearchMode(true);
425
+ search.enterSearchMode();
420
426
  }
421
427
  else if (key.escape) {
422
- if (submittedSearchQuery) {
423
- setSubmittedSearchQuery("");
424
- setSearchQuery("");
425
- setSelectedIndex(0);
428
+ if (search.handleEscape()) {
429
+ return;
430
+ }
431
+ if (onBack) {
432
+ onBack();
433
+ }
434
+ else if (onExit) {
435
+ onExit();
426
436
  }
427
437
  else {
428
- if (onBack) {
429
- onBack();
430
- }
431
- else if (onExit) {
432
- onExit();
433
- }
434
- else {
435
- inkExit();
436
- }
438
+ inkExit();
437
439
  }
438
440
  }
439
441
  });
@@ -472,13 +474,7 @@ const ListDevboxesUI = ({ status, onBack, onExit, onNavigateToDetail, }) => {
472
474
  return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes", active: true }] }), _jsx(ErrorMessage, { message: "Failed to list devboxes", error: error })] }));
473
475
  }
474
476
  // Main list view
475
- return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes", active: true }] }), searchMode && (_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: colors.primary, children: [figures.pointerSmall, " Search: "] }), _jsx(TextInput, { value: searchQuery, onChange: setSearchQuery, placeholder: "Type to search...", onSubmit: () => {
476
- setSearchMode(false);
477
- setSubmittedSearchQuery(searchQuery);
478
- setSelectedIndex(0);
479
- } }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[Enter to search, Esc to cancel]"] })] })), !searchMode && submittedSearchQuery && (_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: colors.primary, children: [figures.info, " Searching for: "] }), _jsx(Text, { color: colors.warning, bold: true, children: submittedSearchQuery.length > 50
480
- ? submittedSearchQuery.substring(0, 50) + "..."
481
- : submittedSearchQuery }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "(", totalCount, " results) [/ to edit, Esc to clear]"] })] })), !showPopup && (_jsx(Table, { data: devboxes, keyExtractor: (devbox) => devbox.id, selectedIndex: selectedIndex, title: "devboxes", columns: tableColumns, emptyState: _jsxs(Text, { color: colors.textDim, children: [figures.info, " No devboxes found. Press [c] to create one."] }) })), !showPopup && (_jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", totalCount] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "total"] }), totalPages > 1 && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), navigating ? (_jsxs(Text, { color: colors.warning, children: [figures.pointer, " Loading page ", currentPage + 1, "..."] })) : (_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Page ", currentPage + 1, " of ", totalPages] }))] })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Showing ", startIndex + 1, "-", endIndex, " of ", totalCount] }), submittedSearchQuery && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.warning, children: ["Filtered: \"", submittedSearchQuery, "\""] })] }))] })), showPopup && selectedDevbox && (_jsx(Box, { marginTop: 2, justifyContent: "center", children: _jsx(ActionsPopup, { devbox: selectedDevbox, operations: operations, selectedOperation: selectedOperation, onClose: () => setShowPopup(false) }) })), _jsx(NavigationTips, { showArrows: true, tips: [
477
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes", active: true }] }), _jsx(SearchBar, { searchMode: search.searchMode, searchQuery: search.searchQuery, submittedSearchQuery: search.submittedSearchQuery, resultCount: totalCount, onSearchChange: search.setSearchQuery, onSearchSubmit: search.submitSearch, placeholder: "Search devboxes..." }), !showPopup && (_jsx(Table, { data: devboxes, keyExtractor: (devbox) => devbox.id, selectedIndex: selectedIndex, title: "devboxes", columns: tableColumns, emptyState: _jsxs(Text, { color: colors.textDim, children: [figures.info, " No devboxes found. Press [c] to create one."] }) })), !showPopup && (_jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", totalCount] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "total"] }), totalPages > 1 && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), navigating ? (_jsxs(Text, { color: colors.warning, children: [figures.pointer, " Loading page ", currentPage + 1, "..."] })) : (_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Page ", currentPage + 1, " of ", totalPages] }))] })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Showing ", startIndex + 1, "-", endIndex, " of ", totalCount] }), search.submittedSearchQuery && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.warning, children: ["Filtered: \"", search.submittedSearchQuery, "\""] })] }))] })), showPopup && selectedDevbox && (_jsx(Box, { marginTop: 2, justifyContent: "center", children: _jsx(ActionsPopup, { devbox: selectedDevbox, operations: operations, selectedOperation: selectedOperation, onClose: () => setShowPopup(false) }) })), _jsx(NavigationTips, { showArrows: true, tips: [
482
478
  {
483
479
  icon: `${figures.arrowLeft}${figures.arrowRight}`,
484
480
  label: "Page",
@@ -50,11 +50,36 @@ export async function createTunnel(devboxId, options) {
50
50
  `${localPort}:localhost:${remotePort}`,
51
51
  `${user}@${sshInfo.url}`,
52
52
  ];
53
+ const tunnelUrl = `http://localhost:${localPort}`;
53
54
  console.log(`Starting tunnel: local port ${localPort} -> remote port ${remotePort}`);
55
+ console.log(`Tunnel URL: ${tunnelUrl}`);
54
56
  console.log("Press Ctrl+C to stop the tunnel.");
55
57
  const tunnelProcess = spawn("/usr/bin/ssh", tunnelArgs, {
56
58
  stdio: "inherit",
57
59
  });
60
+ // Open browser if --open flag is set
61
+ if (options.open) {
62
+ // Small delay to let the tunnel establish
63
+ setTimeout(async () => {
64
+ const { exec } = await import("child_process");
65
+ const platform = process.platform;
66
+ let openCommand;
67
+ if (platform === "darwin") {
68
+ openCommand = `open "${tunnelUrl}"`;
69
+ }
70
+ else if (platform === "win32") {
71
+ openCommand = `start "${tunnelUrl}"`;
72
+ }
73
+ else {
74
+ openCommand = `xdg-open "${tunnelUrl}"`;
75
+ }
76
+ exec(openCommand, (error) => {
77
+ if (error) {
78
+ console.log(`\nCould not open browser: ${error.message}`);
79
+ }
80
+ });
81
+ }, 1000);
82
+ }
58
83
  tunnelProcess.on("close", (code) => {
59
84
  console.log("\nTunnel closed.");
60
85
  processUtils.exit(code || 0);
@@ -12,11 +12,13 @@ import { NavigationTips } from "../../components/NavigationTips.js";
12
12
  import { Table, createTextColumn } from "../../components/Table.js";
13
13
  import { ActionsPopup } from "../../components/ActionsPopup.js";
14
14
  import { formatTimeAgo } from "../../components/ResourceListView.js";
15
+ import { SearchBar } from "../../components/SearchBar.js";
15
16
  import { output, outputError } from "../../utils/output.js";
16
17
  import { colors } from "../../utils/theme.js";
17
18
  import { useViewportHeight } from "../../hooks/useViewportHeight.js";
18
19
  import { useExitOnCtrlC } from "../../hooks/useExitOnCtrlC.js";
19
20
  import { useCursorPagination } from "../../hooks/useCursorPagination.js";
21
+ import { useListSearch } from "../../hooks/useListSearch.js";
20
22
  import { useNavigation } from "../../store/navigationStore.js";
21
23
  import { NetworkPolicyCreatePage } from "../../components/NetworkPolicyCreatePage.js";
22
24
  import { ConfirmationPrompt } from "../../components/ConfirmationPrompt.js";
@@ -60,8 +62,13 @@ const ListNetworkPoliciesUI = ({ onBack, onExit, }) => {
60
62
  const [showEditPolicy, setShowEditPolicy] = React.useState(false);
61
63
  const [editingPolicy, setEditingPolicy] = React.useState(null);
62
64
  const [showDeleteConfirm, setShowDeleteConfirm] = React.useState(false);
65
+ // Search state
66
+ const search = useListSearch({
67
+ onSearchSubmit: () => setSelectedIndex(0),
68
+ onSearchClear: () => setSelectedIndex(0),
69
+ });
63
70
  // Calculate overhead for viewport height
64
- const overhead = 13;
71
+ const overhead = 13 + search.getSearchOverhead();
65
72
  const { viewportHeight, terminalWidth } = useViewportHeight({
66
73
  overhead,
67
74
  minHeight: 5,
@@ -90,6 +97,9 @@ const ListNetworkPoliciesUI = ({ onBack, onExit, }) => {
90
97
  if (params.startingAt) {
91
98
  queryParams.starting_after = params.startingAt;
92
99
  }
100
+ if (search.submittedSearchQuery) {
101
+ queryParams.search = search.submittedSearchQuery;
102
+ }
93
103
  // Fetch ONE page only
94
104
  const page = (await client.networkPolicies.list(queryParams));
95
105
  // Extract data and create defensive copies
@@ -115,7 +125,7 @@ const ListNetworkPoliciesUI = ({ onBack, onExit, }) => {
115
125
  totalCount: page.total_count || pagePolicies.length,
116
126
  };
117
127
  return result;
118
- }, []);
128
+ }, [search.submittedSearchQuery]);
119
129
  // Use the shared pagination hook
120
130
  const { items: policies, loading, navigating, error, currentPage, hasMore, hasPrev, totalCount, nextPage, prevPage, refresh, } = useCursorPagination({
121
131
  fetchPage,
@@ -126,8 +136,9 @@ const ListNetworkPoliciesUI = ({ onBack, onExit, }) => {
126
136
  !executingOperation &&
127
137
  !showCreatePolicy &&
128
138
  !showEditPolicy &&
129
- !showDeleteConfirm,
130
- deps: [PAGE_SIZE],
139
+ !showDeleteConfirm &&
140
+ !search.searchMode,
141
+ deps: [PAGE_SIZE, search.submittedSearchQuery],
131
142
  });
132
143
  // Operations for a specific network policy (shown in popup)
133
144
  const operations = React.useMemo(() => [
@@ -232,6 +243,13 @@ const ListNetworkPoliciesUI = ({ onBack, onExit, }) => {
232
243
  }
233
244
  };
234
245
  useInput((input, key) => {
246
+ // Handle search mode input
247
+ if (search.searchMode) {
248
+ if (key.escape) {
249
+ search.cancelSearch();
250
+ }
251
+ return;
252
+ }
235
253
  // Handle operation result display
236
254
  if (operationResult || operationError) {
237
255
  if (input === "q" || key.escape || key.return) {
@@ -363,7 +381,13 @@ const ListNetworkPoliciesUI = ({ onBack, onExit, }) => {
363
381
  setEditingPolicy(selectedPolicyItem);
364
382
  setShowEditPolicy(true);
365
383
  }
384
+ else if (input === "/") {
385
+ search.enterSearchMode();
386
+ }
366
387
  else if (key.escape) {
388
+ if (search.handleEscape()) {
389
+ return;
390
+ }
367
391
  if (onBack) {
368
392
  onBack();
369
393
  }
@@ -443,7 +467,7 @@ const ListNetworkPoliciesUI = ({ onBack, onExit, }) => {
443
467
  return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Network Policies", active: true }] }), _jsx(ErrorMessage, { message: "Failed to list network policies", error: error })] }));
444
468
  }
445
469
  // Main list view
446
- return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Network Policies", active: true }] }), !showPopup && (_jsx(Table, { data: policies, keyExtractor: (policy) => policy.id, selectedIndex: selectedIndex, title: `network_policies[${totalCount}]`, columns: columns, emptyState: _jsxs(Text, { color: colors.textDim, children: [figures.info, " No network policies found. Press [c] to create one."] }) })), !showPopup && (_jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", totalCount] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "total"] }), totalPages > 1 && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), navigating ? (_jsxs(Text, { color: colors.warning, children: [figures.pointer, " Loading page ", currentPage + 1, "..."] })) : (_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Page ", currentPage + 1, " of ", totalPages] }))] })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Showing ", startIndex + 1, "-", endIndex, " of ", totalCount] })] })), showPopup && selectedPolicyItem && (_jsx(Box, { marginTop: 2, justifyContent: "center", children: _jsx(ActionsPopup, { devbox: selectedPolicyItem, operations: operations.map((op) => ({
470
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Network Policies", active: true }] }), _jsx(SearchBar, { searchMode: search.searchMode, searchQuery: search.searchQuery, submittedSearchQuery: search.submittedSearchQuery, resultCount: totalCount, onSearchChange: search.setSearchQuery, onSearchSubmit: search.submitSearch, placeholder: "Search network policies..." }), !showPopup && (_jsx(Table, { data: policies, keyExtractor: (policy) => policy.id, selectedIndex: selectedIndex, title: `network_policies[${totalCount}]`, columns: columns, emptyState: _jsxs(Text, { color: colors.textDim, children: [figures.info, " No network policies found. Press [c] to create one."] }) })), !showPopup && (_jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", totalCount] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "total"] }), totalPages > 1 && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), navigating ? (_jsxs(Text, { color: colors.warning, children: [figures.pointer, " Loading page ", currentPage + 1, "..."] })) : (_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Page ", currentPage + 1, " of ", totalPages] }))] })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Showing ", startIndex + 1, "-", endIndex, " of ", totalCount] }), search.submittedSearchQuery && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.warning, children: ["Filtered: \"", search.submittedSearchQuery, "\""] })] }))] })), showPopup && selectedPolicyItem && (_jsx(Box, { marginTop: 2, justifyContent: "center", children: _jsx(ActionsPopup, { devbox: selectedPolicyItem, operations: operations.map((op) => ({
447
471
  key: op.key,
448
472
  label: op.label,
449
473
  color: op.color,
@@ -467,6 +491,7 @@ const ListNetworkPoliciesUI = ({ onBack, onExit, }) => {
467
491
  { key: "c", label: "Create" },
468
492
  { key: "e", label: "Edit" },
469
493
  { key: "a", label: "Actions" },
494
+ { key: "/", label: "Search" },
470
495
  { key: "Esc", label: "Back" },
471
496
  ] })] }));
472
497
  };
@@ -14,11 +14,13 @@ import { NavigationTips } from "../../components/NavigationTips.js";
14
14
  import { Table, createTextColumn } from "../../components/Table.js";
15
15
  import { ActionsPopup } from "../../components/ActionsPopup.js";
16
16
  import { formatTimeAgo } from "../../components/ResourceListView.js";
17
+ import { SearchBar } from "../../components/SearchBar.js";
17
18
  import { output, outputError } from "../../utils/output.js";
18
19
  import { colors } from "../../utils/theme.js";
19
20
  import { useViewportHeight } from "../../hooks/useViewportHeight.js";
20
21
  import { useExitOnCtrlC } from "../../hooks/useExitOnCtrlC.js";
21
22
  import { useCursorPagination } from "../../hooks/useCursorPagination.js";
23
+ import { useListSearch } from "../../hooks/useListSearch.js";
22
24
  import { useNavigation } from "../../store/navigationStore.js";
23
25
  import { formatFileSize } from "../../services/objectService.js";
24
26
  import { ConfirmationPrompt } from "../../components/ConfirmationPrompt.js";
@@ -38,8 +40,13 @@ const ListObjectsUI = ({ onBack, onExit, }) => {
38
40
  const [showDownloadPrompt, setShowDownloadPrompt] = React.useState(false);
39
41
  const [downloadPath, setDownloadPath] = React.useState("");
40
42
  const [showDeleteConfirm, setShowDeleteConfirm] = React.useState(false);
43
+ // Search state
44
+ const search = useListSearch({
45
+ onSearchSubmit: () => setSelectedIndex(0),
46
+ onSearchClear: () => setSelectedIndex(0),
47
+ });
41
48
  // Calculate overhead for viewport height
42
- const overhead = 13;
49
+ const overhead = 13 + search.getSearchOverhead();
43
50
  const { viewportHeight, terminalWidth } = useViewportHeight({
44
51
  overhead,
45
52
  minHeight: 5,
@@ -95,6 +102,9 @@ const ListObjectsUI = ({ onBack, onExit, }) => {
95
102
  if (params.startingAt) {
96
103
  queryParams.starting_after = params.startingAt;
97
104
  }
105
+ if (search.submittedSearchQuery) {
106
+ queryParams.search = search.submittedSearchQuery;
107
+ }
98
108
  // Fetch ONE page only
99
109
  const result = await client.objects.list(queryParams);
100
110
  // Extract data and create defensive copies
@@ -120,7 +130,7 @@ const ListObjectsUI = ({ onBack, onExit, }) => {
120
130
  hasMore: pageResult.has_more || false,
121
131
  totalCount: pageResult.total_count || pageObjects.length,
122
132
  };
123
- }, []);
133
+ }, [search.submittedSearchQuery]);
124
134
  // Use the shared pagination hook
125
135
  const { items: objects, loading, navigating, error, currentPage, hasMore, hasPrev, totalCount, nextPage, prevPage, refresh, } = useCursorPagination({
126
136
  fetchPage,
@@ -130,8 +140,9 @@ const ListObjectsUI = ({ onBack, onExit, }) => {
130
140
  pollingEnabled: !showPopup &&
131
141
  !executingOperation &&
132
142
  !showDownloadPrompt &&
133
- !showDeleteConfirm,
134
- deps: [PAGE_SIZE],
143
+ !showDeleteConfirm &&
144
+ !search.searchMode,
145
+ deps: [PAGE_SIZE, search.submittedSearchQuery],
135
146
  });
136
147
  // Operations for objects
137
148
  const operations = React.useMemo(() => [
@@ -274,6 +285,13 @@ const ListObjectsUI = ({ onBack, onExit, }) => {
274
285
  }
275
286
  };
276
287
  useInput((input, key) => {
288
+ // Handle search mode input
289
+ if (search.searchMode) {
290
+ if (key.escape) {
291
+ search.cancelSearch();
292
+ }
293
+ return;
294
+ }
277
295
  // Handle operation result display
278
296
  if (operationResult || operationError) {
279
297
  if (input === "q" || key.escape || key.return) {
@@ -397,7 +415,13 @@ const ListObjectsUI = ({ onBack, onExit, }) => {
397
415
  setShowPopup(true);
398
416
  setSelectedOperation(0);
399
417
  }
418
+ else if (input === "/") {
419
+ search.enterSearchMode();
420
+ }
400
421
  else if (key.escape) {
422
+ if (search.handleEscape()) {
423
+ return;
424
+ }
401
425
  if (onBack) {
402
426
  onBack();
403
427
  }
@@ -470,7 +494,7 @@ const ListObjectsUI = ({ onBack, onExit, }) => {
470
494
  return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Storage Objects", active: true }] }), _jsx(ErrorMessage, { message: "Failed to list storage objects", error: error })] }));
471
495
  }
472
496
  // Main list view
473
- return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Storage Objects", active: true }] }), !showPopup && (_jsx(Table, { data: objects, keyExtractor: (obj) => obj.id, selectedIndex: selectedIndex, title: `storage_objects[${totalCount}]`, columns: columns, emptyState: _jsxs(Text, { color: colors.textDim, children: [figures.info, " No storage objects found. Try: rli object upload", " ", "<file>"] }) })), !showPopup && (_jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", totalCount] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "total"] }), totalPages > 1 && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), navigating ? (_jsxs(Text, { color: colors.warning, children: [figures.pointer, " Loading page ", currentPage + 1, "..."] })) : (_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Page ", currentPage + 1, " of ", totalPages] }))] })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Showing ", startIndex + 1, "-", endIndex, " of ", totalCount] })] })), showPopup && selectedObjectItem && (_jsx(Box, { marginTop: 2, justifyContent: "center", children: _jsx(ActionsPopup, { devbox: selectedObjectItem, operations: operations.map((op) => ({
497
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Storage Objects", active: true }] }), _jsx(SearchBar, { searchMode: search.searchMode, searchQuery: search.searchQuery, submittedSearchQuery: search.submittedSearchQuery, resultCount: totalCount, onSearchChange: search.setSearchQuery, onSearchSubmit: search.submitSearch, placeholder: "Search storage objects..." }), !showPopup && (_jsx(Table, { data: objects, keyExtractor: (obj) => obj.id, selectedIndex: selectedIndex, title: `storage_objects[${totalCount}]`, columns: columns, emptyState: _jsxs(Text, { color: colors.textDim, children: [figures.info, " No storage objects found. Try: rli object upload", " ", "<file>"] }) })), !showPopup && (_jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", totalCount] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "total"] }), totalPages > 1 && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), navigating ? (_jsxs(Text, { color: colors.warning, children: [figures.pointer, " Loading page ", currentPage + 1, "..."] })) : (_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Page ", currentPage + 1, " of ", totalPages] }))] })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Showing ", startIndex + 1, "-", endIndex, " of ", totalCount] }), search.submittedSearchQuery && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.warning, children: ["Filtered: \"", search.submittedSearchQuery, "\""] })] }))] })), showPopup && selectedObjectItem && (_jsx(Box, { marginTop: 2, justifyContent: "center", children: _jsx(ActionsPopup, { devbox: selectedObjectItem, operations: operations.map((op) => ({
474
498
  key: op.key,
475
499
  label: op.label,
476
500
  color: op.color,
@@ -490,6 +514,7 @@ const ListObjectsUI = ({ onBack, onExit, }) => {
490
514
  },
491
515
  { key: "Enter", label: "Details" },
492
516
  { key: "a", label: "Actions" },
517
+ { key: "/", label: "Search" },
493
518
  { key: "Esc", label: "Back" },
494
519
  ] })] }));
495
520
  };