@runloop/rl-cli 1.4.1 → 1.6.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 +10 -0
- package/dist/commands/blueprint/list.js +30 -5
- package/dist/commands/devbox/list.js +30 -34
- package/dist/commands/network-policy/list.js +30 -5
- package/dist/commands/object/list.js +30 -5
- package/dist/commands/secret/create.js +27 -0
- package/dist/commands/secret/delete.js +54 -0
- package/dist/commands/secret/get.js +23 -0
- package/dist/commands/secret/list.js +23 -0
- package/dist/commands/secret/update.js +27 -0
- package/dist/commands/snapshot/list.js +30 -5
- package/dist/components/SearchBar.js +24 -0
- package/dist/hooks/useListSearch.js +54 -0
- package/dist/utils/commands.js +47 -0
- package/dist/utils/output.js +8 -1
- package/dist/utils/stdin.js +66 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -140,6 +140,16 @@ rli network-policy create # Create a new network policy
|
|
|
140
140
|
rli network-policy delete <id> # Delete a network policy
|
|
141
141
|
```
|
|
142
142
|
|
|
143
|
+
### Secret Commands (alias: `s`)
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
rli secret create <name> # Create a new secret. Value can be pip...
|
|
147
|
+
rli secret list # List all secrets
|
|
148
|
+
rli secret get <name> # Get secret metadata by name
|
|
149
|
+
rli secret update <name> # Update a secret value (value from std...
|
|
150
|
+
rli secret delete <name> # Delete a secret
|
|
151
|
+
```
|
|
152
|
+
|
|
143
153
|
### Mcp Commands
|
|
144
154
|
|
|
145
155
|
```bash
|
|
@@ -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
|
-
|
|
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
|
};
|
|
@@ -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
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
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 + (
|
|
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 &&
|
|
92
|
-
|
|
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
|
-
|
|
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
|
-
|
|
425
|
+
search.enterSearchMode();
|
|
420
426
|
}
|
|
421
427
|
else if (key.escape) {
|
|
422
|
-
if (
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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
|
-
|
|
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, {
|
|
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",
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create secret command
|
|
3
|
+
*/
|
|
4
|
+
import { getClient } from "../../utils/client.js";
|
|
5
|
+
import { output, outputError } from "../../utils/output.js";
|
|
6
|
+
import { getSecretValue } from "../../utils/stdin.js";
|
|
7
|
+
export async function createSecret(name, options = {}) {
|
|
8
|
+
try {
|
|
9
|
+
// Get secret value from stdin (piped) or interactive prompt
|
|
10
|
+
const value = await getSecretValue();
|
|
11
|
+
if (!value) {
|
|
12
|
+
outputError("Secret value cannot be empty", new Error("Empty value"));
|
|
13
|
+
}
|
|
14
|
+
const client = getClient();
|
|
15
|
+
const secret = await client.secrets.create({ name, value });
|
|
16
|
+
// Default: just output the ID for easy scripting
|
|
17
|
+
if (!options.output || options.output === "text") {
|
|
18
|
+
console.log(secret.id);
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
output(secret, { format: options.output, defaultFormat: "json" });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
outputError("Failed to create secret", error);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delete secret command
|
|
3
|
+
*/
|
|
4
|
+
import * as readline from "readline";
|
|
5
|
+
import { getClient } from "../../utils/client.js";
|
|
6
|
+
import { output } from "../../utils/output.js";
|
|
7
|
+
/**
|
|
8
|
+
* Prompt for confirmation
|
|
9
|
+
*/
|
|
10
|
+
async function confirm(message) {
|
|
11
|
+
return new Promise((resolve) => {
|
|
12
|
+
const rl = readline.createInterface({
|
|
13
|
+
input: process.stdin,
|
|
14
|
+
output: process.stdout,
|
|
15
|
+
});
|
|
16
|
+
rl.question(`${message} [y/N] `, (answer) => {
|
|
17
|
+
rl.close();
|
|
18
|
+
resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export async function deleteSecret(name, options = {}) {
|
|
23
|
+
try {
|
|
24
|
+
const client = getClient();
|
|
25
|
+
// Confirm deletion unless --yes flag is passed
|
|
26
|
+
if (!options.yes) {
|
|
27
|
+
const confirmed = await confirm(`Are you sure you want to delete secret "${name}"?`);
|
|
28
|
+
if (!confirmed) {
|
|
29
|
+
console.log("Aborted.");
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// Delete by name
|
|
34
|
+
const secret = await client.secrets.delete(name);
|
|
35
|
+
// Default: show confirmation message
|
|
36
|
+
if (!options.output || options.output === "text") {
|
|
37
|
+
console.log(`Deleted secret "${name}" (${secret.id})`);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
output({ id: secret.id, name, status: "deleted" }, { format: options.output, defaultFormat: "json" });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
45
|
+
if (errorMessage.includes("404") || errorMessage.includes("not found")) {
|
|
46
|
+
console.error(`Error: Secret "${name}" not found`);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
console.error(`Error: Failed to delete secret`);
|
|
50
|
+
console.error(` ${errorMessage}`);
|
|
51
|
+
}
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get secret metadata command
|
|
3
|
+
*
|
|
4
|
+
* Note: The API doesn't have a direct "get by name" endpoint,
|
|
5
|
+
* so we list all secrets and filter by name.
|
|
6
|
+
*/
|
|
7
|
+
import { getClient } from "../../utils/client.js";
|
|
8
|
+
import { output, outputError } from "../../utils/output.js";
|
|
9
|
+
export async function getSecret(name, options = {}) {
|
|
10
|
+
try {
|
|
11
|
+
const client = getClient();
|
|
12
|
+
// List all secrets and find by name
|
|
13
|
+
const result = await client.secrets.list({ limit: 5000 });
|
|
14
|
+
const secret = result.secrets?.find((s) => s.name === name);
|
|
15
|
+
if (!secret) {
|
|
16
|
+
outputError(`Secret "${name}" not found`, new Error("Secret not found"));
|
|
17
|
+
}
|
|
18
|
+
output(secret, { format: options.output, defaultFormat: "json" });
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
outputError("Failed to get secret", error);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List secrets command
|
|
3
|
+
*/
|
|
4
|
+
import { getClient } from "../../utils/client.js";
|
|
5
|
+
import { output, outputError } from "../../utils/output.js";
|
|
6
|
+
const DEFAULT_PAGE_SIZE = 20;
|
|
7
|
+
export async function listSecrets(options = {}) {
|
|
8
|
+
try {
|
|
9
|
+
const client = getClient();
|
|
10
|
+
const limit = options.limit
|
|
11
|
+
? parseInt(options.limit, 10)
|
|
12
|
+
: DEFAULT_PAGE_SIZE;
|
|
13
|
+
// Fetch secrets
|
|
14
|
+
const result = await client.secrets.list({ limit });
|
|
15
|
+
// Extract secrets array
|
|
16
|
+
const secrets = result.secrets || [];
|
|
17
|
+
// Default: output JSON for lists
|
|
18
|
+
output(secrets, { format: options.output, defaultFormat: "json" });
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
outputError("Failed to list secrets", error);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Update secret command
|
|
3
|
+
*/
|
|
4
|
+
import { getClient } from "../../utils/client.js";
|
|
5
|
+
import { output, outputError } from "../../utils/output.js";
|
|
6
|
+
import { getSecretValue } from "../../utils/stdin.js";
|
|
7
|
+
export async function updateSecret(name, options = {}) {
|
|
8
|
+
try {
|
|
9
|
+
// Get new secret value from stdin (piped) or interactive prompt
|
|
10
|
+
const value = await getSecretValue();
|
|
11
|
+
if (!value) {
|
|
12
|
+
outputError("Secret value cannot be empty", new Error("Empty value"));
|
|
13
|
+
}
|
|
14
|
+
const client = getClient();
|
|
15
|
+
const secret = await client.secrets.update(name, { value });
|
|
16
|
+
// Default: just output the ID for easy scripting
|
|
17
|
+
if (!options.output || options.output === "text") {
|
|
18
|
+
console.log(secret.id);
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
output(secret, { format: options.output, defaultFormat: "json" });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
outputError("Failed to update secret", error);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -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 { DevboxCreatePage } from "../../components/DevboxCreatePage.js";
|
|
21
23
|
import { useNavigation } from "../../store/navigationStore.js";
|
|
22
24
|
import { ConfirmationPrompt } from "../../components/ConfirmationPrompt.js";
|
|
@@ -35,8 +37,13 @@ const ListSnapshotsUI = ({ devboxId, onBack, onExit, }) => {
|
|
|
35
37
|
const [operationLoading, setOperationLoading] = React.useState(false);
|
|
36
38
|
const [showCreateDevbox, setShowCreateDevbox] = React.useState(false);
|
|
37
39
|
const [showDeleteConfirm, setShowDeleteConfirm] = React.useState(false);
|
|
40
|
+
// Search state
|
|
41
|
+
const search = useListSearch({
|
|
42
|
+
onSearchSubmit: () => setSelectedIndex(0),
|
|
43
|
+
onSearchClear: () => setSelectedIndex(0),
|
|
44
|
+
});
|
|
38
45
|
// Calculate overhead for viewport height
|
|
39
|
-
const overhead = 13;
|
|
46
|
+
const overhead = 13 + search.getSearchOverhead();
|
|
40
47
|
const { viewportHeight, terminalWidth } = useViewportHeight({
|
|
41
48
|
overhead,
|
|
42
49
|
minHeight: 5,
|
|
@@ -67,6 +74,9 @@ const ListSnapshotsUI = ({ devboxId, onBack, onExit, }) => {
|
|
|
67
74
|
if (devboxId) {
|
|
68
75
|
queryParams.devbox_id = devboxId;
|
|
69
76
|
}
|
|
77
|
+
if (search.submittedSearchQuery) {
|
|
78
|
+
queryParams.search = search.submittedSearchQuery;
|
|
79
|
+
}
|
|
70
80
|
// Fetch ONE page only
|
|
71
81
|
const page = (await client.devboxes.listDiskSnapshots(queryParams));
|
|
72
82
|
// Extract data and create defensive copies
|
|
@@ -87,7 +97,7 @@ const ListSnapshotsUI = ({ devboxId, onBack, onExit, }) => {
|
|
|
87
97
|
totalCount: page.total_count || pageSnapshots.length,
|
|
88
98
|
};
|
|
89
99
|
return result;
|
|
90
|
-
}, [devboxId]);
|
|
100
|
+
}, [devboxId, search.submittedSearchQuery]);
|
|
91
101
|
// Use the shared pagination hook
|
|
92
102
|
const { items: snapshots, loading, navigating, error, currentPage, hasMore, hasPrev, totalCount, nextPage, prevPage, refresh, } = useCursorPagination({
|
|
93
103
|
fetchPage,
|
|
@@ -97,8 +107,9 @@ const ListSnapshotsUI = ({ devboxId, onBack, onExit, }) => {
|
|
|
97
107
|
pollingEnabled: !showPopup &&
|
|
98
108
|
!executingOperation &&
|
|
99
109
|
!showCreateDevbox &&
|
|
100
|
-
!showDeleteConfirm
|
|
101
|
-
|
|
110
|
+
!showDeleteConfirm &&
|
|
111
|
+
!search.searchMode,
|
|
112
|
+
deps: [devboxId, PAGE_SIZE, search.submittedSearchQuery],
|
|
102
113
|
});
|
|
103
114
|
// Operations for snapshots
|
|
104
115
|
const operations = React.useMemo(() => [
|
|
@@ -180,6 +191,13 @@ const ListSnapshotsUI = ({ devboxId, onBack, onExit, }) => {
|
|
|
180
191
|
}
|
|
181
192
|
};
|
|
182
193
|
useInput((input, key) => {
|
|
194
|
+
// Handle search mode input
|
|
195
|
+
if (search.searchMode) {
|
|
196
|
+
if (key.escape) {
|
|
197
|
+
search.cancelSearch();
|
|
198
|
+
}
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
183
201
|
// Handle operation result display
|
|
184
202
|
if (operationResult || operationError) {
|
|
185
203
|
if (input === "q" || key.escape || key.return) {
|
|
@@ -289,7 +307,13 @@ const ListSnapshotsUI = ({ devboxId, onBack, onExit, }) => {
|
|
|
289
307
|
setShowPopup(true);
|
|
290
308
|
setSelectedOperation(0);
|
|
291
309
|
}
|
|
310
|
+
else if (input === "/") {
|
|
311
|
+
search.enterSearchMode();
|
|
312
|
+
}
|
|
292
313
|
else if (key.escape) {
|
|
314
|
+
if (search.handleEscape()) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
293
317
|
if (onBack) {
|
|
294
318
|
onBack();
|
|
295
319
|
}
|
|
@@ -374,7 +398,7 @@ const ListSnapshotsUI = ({ devboxId, onBack, onExit, }) => {
|
|
|
374
398
|
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
375
399
|
{ label: "Snapshots", active: !devboxId },
|
|
376
400
|
...(devboxId ? [{ label: `Devbox: ${devboxId}`, active: true }] : []),
|
|
377
|
-
] }), !showPopup && (_jsx(Table, { data: snapshots, keyExtractor: (snapshot) => snapshot.id, selectedIndex: selectedIndex, title: `snapshots[${totalCount}]`, columns: columns, emptyState: _jsxs(Text, { color: colors.textDim, children: [figures.info, " No snapshots found. Try: rli snapshot create", " ", "<devbox-id>"] }) })), !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 && selectedSnapshotItem && (_jsx(Box, { marginTop: 2, justifyContent: "center", children: _jsx(ActionsPopup, { devbox: selectedSnapshotItem, operations: operations.map((op) => ({
|
|
401
|
+
] }), _jsx(SearchBar, { searchMode: search.searchMode, searchQuery: search.searchQuery, submittedSearchQuery: search.submittedSearchQuery, resultCount: totalCount, onSearchChange: search.setSearchQuery, onSearchSubmit: search.submitSearch, placeholder: "Search snapshots..." }), !showPopup && (_jsx(Table, { data: snapshots, keyExtractor: (snapshot) => snapshot.id, selectedIndex: selectedIndex, title: `snapshots[${totalCount}]`, columns: columns, emptyState: _jsxs(Text, { color: colors.textDim, children: [figures.info, " No snapshots found. Try: rli snapshot create", " ", "<devbox-id>"] }) })), !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 && selectedSnapshotItem && (_jsx(Box, { marginTop: 2, justifyContent: "center", children: _jsx(ActionsPopup, { devbox: selectedSnapshotItem, operations: operations.map((op) => ({
|
|
378
402
|
key: op.key,
|
|
379
403
|
label: op.label,
|
|
380
404
|
color: op.color,
|
|
@@ -394,6 +418,7 @@ const ListSnapshotsUI = ({ devboxId, onBack, onExit, }) => {
|
|
|
394
418
|
},
|
|
395
419
|
{ key: "Enter", label: "Details" },
|
|
396
420
|
{ key: "a", label: "Actions" },
|
|
421
|
+
{ key: "/", label: "Search" },
|
|
397
422
|
{ key: "Esc", label: "Back" },
|
|
398
423
|
] })] }));
|
|
399
424
|
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import TextInput from "ink-text-input";
|
|
4
|
+
import figures from "figures";
|
|
5
|
+
import { colors } from "../utils/theme.js";
|
|
6
|
+
/**
|
|
7
|
+
* Reusable search bar component for list views.
|
|
8
|
+
* Displays either an input mode or the active search with result count.
|
|
9
|
+
*/
|
|
10
|
+
export function SearchBar({ searchMode, searchQuery, submittedSearchQuery, resultCount, onSearchChange, onSearchSubmit, placeholder = "Type to search...", maxDisplayLength = 50, }) {
|
|
11
|
+
// Don't render if no search is active
|
|
12
|
+
if (!searchMode && !submittedSearchQuery) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
// Search input mode
|
|
16
|
+
if (searchMode) {
|
|
17
|
+
return (_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: colors.primary, children: [figures.pointerSmall, " Search: "] }), _jsx(TextInput, { value: searchQuery, onChange: onSearchChange, placeholder: placeholder, onSubmit: onSearchSubmit }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[Enter to search, Esc to cancel]"] })] }));
|
|
18
|
+
}
|
|
19
|
+
// Display active search with results
|
|
20
|
+
const displayQuery = submittedSearchQuery.length > maxDisplayLength
|
|
21
|
+
? submittedSearchQuery.substring(0, maxDisplayLength) + "..."
|
|
22
|
+
: submittedSearchQuery;
|
|
23
|
+
return (_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: colors.primary, children: [figures.info, " Searching for: "] }), _jsx(Text, { color: colors.warning, bold: true, children: displayQuery }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "(", resultCount, " results) [/ to edit, Esc to clear]"] })] }));
|
|
24
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Shared hook for managing search state in list views.
|
|
4
|
+
* Provides consistent search behavior across all resource lists.
|
|
5
|
+
*/
|
|
6
|
+
export function useListSearch(options = {}) {
|
|
7
|
+
const [searchMode, setSearchMode] = React.useState(false);
|
|
8
|
+
const [searchQuery, setSearchQuery] = React.useState("");
|
|
9
|
+
const [submittedSearchQuery, setSubmittedSearchQuery] = React.useState("");
|
|
10
|
+
const enterSearchMode = React.useCallback(() => {
|
|
11
|
+
setSearchMode(true);
|
|
12
|
+
}, []);
|
|
13
|
+
const cancelSearch = React.useCallback(() => {
|
|
14
|
+
setSearchMode(false);
|
|
15
|
+
setSearchQuery("");
|
|
16
|
+
}, []);
|
|
17
|
+
const submitSearch = React.useCallback(() => {
|
|
18
|
+
setSearchMode(false);
|
|
19
|
+
setSubmittedSearchQuery(searchQuery);
|
|
20
|
+
options.onSearchSubmit?.(searchQuery);
|
|
21
|
+
}, [searchQuery, options.onSearchSubmit]);
|
|
22
|
+
const clearSearch = React.useCallback(() => {
|
|
23
|
+
setSubmittedSearchQuery("");
|
|
24
|
+
setSearchQuery("");
|
|
25
|
+
options.onSearchClear?.();
|
|
26
|
+
}, [options.onSearchClear]);
|
|
27
|
+
const handleEscape = React.useCallback(() => {
|
|
28
|
+
if (searchMode) {
|
|
29
|
+
cancelSearch();
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
if (submittedSearchQuery) {
|
|
33
|
+
clearSearch();
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
return false;
|
|
37
|
+
}, [searchMode, submittedSearchQuery, cancelSearch, clearSearch]);
|
|
38
|
+
const getSearchOverhead = React.useCallback(() => {
|
|
39
|
+
// Search bar takes 2 lines when visible (1 line content + 1 marginBottom)
|
|
40
|
+
return searchMode || submittedSearchQuery ? 2 : 0;
|
|
41
|
+
}, [searchMode, submittedSearchQuery]);
|
|
42
|
+
return {
|
|
43
|
+
searchMode,
|
|
44
|
+
searchQuery,
|
|
45
|
+
submittedSearchQuery,
|
|
46
|
+
enterSearchMode,
|
|
47
|
+
cancelSearch,
|
|
48
|
+
setSearchQuery,
|
|
49
|
+
submitSearch,
|
|
50
|
+
clearSearch,
|
|
51
|
+
handleEscape,
|
|
52
|
+
getSearchOverhead,
|
|
53
|
+
};
|
|
54
|
+
}
|
package/dist/utils/commands.js
CHANGED
|
@@ -439,6 +439,53 @@ export function createProgram() {
|
|
|
439
439
|
const { deleteNetworkPolicy } = await import("../commands/network-policy/delete.js");
|
|
440
440
|
await deleteNetworkPolicy(id, options);
|
|
441
441
|
});
|
|
442
|
+
// Secret commands
|
|
443
|
+
const secret = program
|
|
444
|
+
.command("secret")
|
|
445
|
+
.description("Manage secrets")
|
|
446
|
+
.alias("s");
|
|
447
|
+
secret
|
|
448
|
+
.command("create <name>")
|
|
449
|
+
.description("Create a new secret. Value can be piped via stdin (e.g., echo 'val' | rli secret create name) or entered interactively with masked input for security.")
|
|
450
|
+
.option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
|
|
451
|
+
.action(async (name, options) => {
|
|
452
|
+
const { createSecret } = await import("../commands/secret/create.js");
|
|
453
|
+
await createSecret(name, options);
|
|
454
|
+
});
|
|
455
|
+
secret
|
|
456
|
+
.command("list")
|
|
457
|
+
.description("List all secrets")
|
|
458
|
+
.option("--limit <n>", "Max results", "20")
|
|
459
|
+
.option("-o, --output [format]", "Output format: text|json|yaml (default: json)")
|
|
460
|
+
.action(async (options) => {
|
|
461
|
+
const { listSecrets } = await import("../commands/secret/list.js");
|
|
462
|
+
await listSecrets(options);
|
|
463
|
+
});
|
|
464
|
+
secret
|
|
465
|
+
.command("get <name>")
|
|
466
|
+
.description("Get secret metadata by name")
|
|
467
|
+
.option("-o, --output [format]", "Output format: text|json|yaml (default: json)")
|
|
468
|
+
.action(async (name, options) => {
|
|
469
|
+
const { getSecret } = await import("../commands/secret/get.js");
|
|
470
|
+
await getSecret(name, options);
|
|
471
|
+
});
|
|
472
|
+
secret
|
|
473
|
+
.command("update <name>")
|
|
474
|
+
.description("Update a secret value (value from stdin or secure prompt)")
|
|
475
|
+
.option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
|
|
476
|
+
.action(async (name, options) => {
|
|
477
|
+
const { updateSecret } = await import("../commands/secret/update.js");
|
|
478
|
+
await updateSecret(name, options);
|
|
479
|
+
});
|
|
480
|
+
secret
|
|
481
|
+
.command("delete <name>")
|
|
482
|
+
.description("Delete a secret")
|
|
483
|
+
.option("-y, --yes", "Skip confirmation prompt")
|
|
484
|
+
.option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
|
|
485
|
+
.action(async (name, options) => {
|
|
486
|
+
const { deleteSecret } = await import("../commands/secret/delete.js");
|
|
487
|
+
await deleteSecret(name, options);
|
|
488
|
+
});
|
|
442
489
|
// MCP server commands
|
|
443
490
|
const mcp = program
|
|
444
491
|
.command("mcp")
|
package/dist/utils/output.js
CHANGED
|
@@ -117,7 +117,14 @@ export function output(data, options = {}) {
|
|
|
117
117
|
export function outputError(message, error) {
|
|
118
118
|
const errorMessage = error instanceof Error ? error.message : String(error || message);
|
|
119
119
|
console.error(`Error: ${message}`);
|
|
120
|
-
|
|
120
|
+
// Only print the error message if it adds new information
|
|
121
|
+
// Skip if: same as message, message contains it, or it contains the message
|
|
122
|
+
const messageLower = message.toLowerCase();
|
|
123
|
+
const errorLower = errorMessage.toLowerCase();
|
|
124
|
+
const isRedundant = errorMessage === message ||
|
|
125
|
+
messageLower.includes(errorLower) ||
|
|
126
|
+
errorLower.includes(messageLower);
|
|
127
|
+
if (error && !isRedundant) {
|
|
121
128
|
console.error(` ${errorMessage}`);
|
|
122
129
|
}
|
|
123
130
|
processUtils.exit(1);
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for reading secure input from stdin
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Prompt for a secret value with masked input (shows * for each character)
|
|
6
|
+
* Only works when stdin is a TTY (interactive terminal)
|
|
7
|
+
*/
|
|
8
|
+
export async function promptSecretValue(prompt = "Enter secret value: ") {
|
|
9
|
+
return new Promise((resolve) => {
|
|
10
|
+
process.stdout.write(prompt);
|
|
11
|
+
let value = "";
|
|
12
|
+
process.stdin.setRawMode(true);
|
|
13
|
+
process.stdin.resume();
|
|
14
|
+
process.stdin.setEncoding("utf8");
|
|
15
|
+
const onData = (char) => {
|
|
16
|
+
if (char === "\n" || char === "\r") {
|
|
17
|
+
process.stdin.setRawMode(false);
|
|
18
|
+
process.stdin.removeListener("data", onData);
|
|
19
|
+
process.stdin.pause();
|
|
20
|
+
process.stdout.write("\n");
|
|
21
|
+
resolve(value);
|
|
22
|
+
}
|
|
23
|
+
else if (char === "\u0003") {
|
|
24
|
+
// Ctrl+C
|
|
25
|
+
process.stdin.setRawMode(false);
|
|
26
|
+
process.stdout.write("\n");
|
|
27
|
+
process.exit(0);
|
|
28
|
+
}
|
|
29
|
+
else if (char === "\u007F" || char === "\b") {
|
|
30
|
+
// Backspace (0x7F) or Ctrl+H (0x08)
|
|
31
|
+
if (value.length > 0) {
|
|
32
|
+
value = value.slice(0, -1);
|
|
33
|
+
process.stdout.write("\b \b");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
else if (char >= " ") {
|
|
37
|
+
// Only add printable characters
|
|
38
|
+
value += char;
|
|
39
|
+
process.stdout.write("*");
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
process.stdin.on("data", onData);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Read all data from stdin (for piped input)
|
|
47
|
+
*/
|
|
48
|
+
export async function readStdin() {
|
|
49
|
+
const chunks = [];
|
|
50
|
+
for await (const chunk of process.stdin) {
|
|
51
|
+
chunks.push(Buffer.from(chunk));
|
|
52
|
+
}
|
|
53
|
+
return Buffer.concat(chunks).toString().trim();
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Get a secret value from either piped stdin or interactive prompt
|
|
57
|
+
* Automatically detects whether input is piped or interactive
|
|
58
|
+
*/
|
|
59
|
+
export async function getSecretValue(prompt = "Enter secret value: ") {
|
|
60
|
+
if (process.stdin.isTTY) {
|
|
61
|
+
return promptSecretValue(prompt);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
return readStdin();
|
|
65
|
+
}
|
|
66
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@runloop/rl-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Beautiful CLI for the Runloop platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -87,13 +87,13 @@
|
|
|
87
87
|
"zustand": "^5.0.2"
|
|
88
88
|
},
|
|
89
89
|
"devDependencies": {
|
|
90
|
-
"@anthropic-ai/mcpb": "^
|
|
90
|
+
"@anthropic-ai/mcpb": "^2.1.2",
|
|
91
91
|
"@types/jest": "^29.5.0",
|
|
92
92
|
"@types/node": "^22.7.9",
|
|
93
93
|
"@types/react": "^19.2.2",
|
|
94
94
|
"@typescript-eslint/eslint-plugin": "^8.46.0",
|
|
95
95
|
"@typescript-eslint/parser": "^8.46.0",
|
|
96
|
-
"esbuild": "^0.
|
|
96
|
+
"esbuild": "^0.27.2",
|
|
97
97
|
"eslint": "^9.37.0",
|
|
98
98
|
"eslint-plugin-react": "^7.37.5",
|
|
99
99
|
"eslint-plugin-react-hooks": "^6.1.1",
|