@runloop/rl-cli 1.2.0 → 1.3.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.
Files changed (55) hide show
  1. package/README.md +28 -8
  2. package/dist/commands/blueprint/list.js +97 -28
  3. package/dist/commands/blueprint/prune.js +7 -19
  4. package/dist/commands/devbox/create.js +3 -0
  5. package/dist/commands/devbox/list.js +44 -65
  6. package/dist/commands/menu.js +2 -1
  7. package/dist/commands/network-policy/create.js +27 -0
  8. package/dist/commands/network-policy/delete.js +21 -0
  9. package/dist/commands/network-policy/get.js +15 -0
  10. package/dist/commands/network-policy/list.js +494 -0
  11. package/dist/commands/object/list.js +516 -24
  12. package/dist/commands/snapshot/list.js +90 -29
  13. package/dist/components/Banner.js +109 -8
  14. package/dist/components/ConfirmationPrompt.js +45 -0
  15. package/dist/components/DevboxActionsMenu.js +42 -6
  16. package/dist/components/DevboxCard.js +1 -1
  17. package/dist/components/DevboxCreatePage.js +95 -81
  18. package/dist/components/DevboxDetailPage.js +218 -272
  19. package/dist/components/LogsViewer.js +8 -1
  20. package/dist/components/MainMenu.js +35 -4
  21. package/dist/components/NavigationTips.js +24 -0
  22. package/dist/components/NetworkPolicyCreatePage.js +264 -0
  23. package/dist/components/OperationsMenu.js +9 -1
  24. package/dist/components/ResourceActionsMenu.js +5 -1
  25. package/dist/components/ResourceDetailPage.js +204 -0
  26. package/dist/components/ResourceListView.js +19 -2
  27. package/dist/components/StatusBadge.js +2 -2
  28. package/dist/components/Table.js +6 -8
  29. package/dist/components/form/FormActionButton.js +7 -0
  30. package/dist/components/form/FormField.js +7 -0
  31. package/dist/components/form/FormListManager.js +112 -0
  32. package/dist/components/form/FormSelect.js +34 -0
  33. package/dist/components/form/FormTextInput.js +8 -0
  34. package/dist/components/form/index.js +8 -0
  35. package/dist/hooks/useViewportHeight.js +38 -20
  36. package/dist/router/Router.js +23 -1
  37. package/dist/screens/BlueprintDetailScreen.js +337 -0
  38. package/dist/screens/MenuScreen.js +6 -0
  39. package/dist/screens/NetworkPolicyCreateScreen.js +7 -0
  40. package/dist/screens/NetworkPolicyDetailScreen.js +247 -0
  41. package/dist/screens/NetworkPolicyListScreen.js +7 -0
  42. package/dist/screens/ObjectDetailScreen.js +377 -0
  43. package/dist/screens/ObjectListScreen.js +7 -0
  44. package/dist/screens/SnapshotDetailScreen.js +208 -0
  45. package/dist/services/blueprintService.js +30 -11
  46. package/dist/services/networkPolicyService.js +108 -0
  47. package/dist/services/objectService.js +101 -0
  48. package/dist/services/snapshotService.js +39 -3
  49. package/dist/store/blueprintStore.js +4 -10
  50. package/dist/store/index.js +1 -0
  51. package/dist/store/networkPolicyStore.js +83 -0
  52. package/dist/store/objectStore.js +92 -0
  53. package/dist/store/snapshotStore.js +4 -8
  54. package/dist/utils/commands.js +47 -0
  55. package/package.json +2 -2
@@ -0,0 +1,337 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * BlueprintDetailScreen - Detail page for blueprints
4
+ * Uses the generic ResourceDetailPage component
5
+ */
6
+ import React from "react";
7
+ import { Text } from "ink";
8
+ import figures from "figures";
9
+ import { useNavigation } from "../store/navigationStore.js";
10
+ import { useBlueprintStore } from "../store/blueprintStore.js";
11
+ import { ResourceDetailPage, formatTimestamp, } from "../components/ResourceDetailPage.js";
12
+ import { getBlueprint } from "../services/blueprintService.js";
13
+ import { getClient } from "../utils/client.js";
14
+ import { SpinnerComponent } from "../components/Spinner.js";
15
+ import { ErrorMessage } from "../components/ErrorMessage.js";
16
+ import { Breadcrumb } from "../components/Breadcrumb.js";
17
+ import { ConfirmationPrompt } from "../components/ConfirmationPrompt.js";
18
+ import { colors } from "../utils/theme.js";
19
+ export function BlueprintDetailScreen({ blueprintId, }) {
20
+ const { goBack, navigate } = useNavigation();
21
+ const blueprints = useBlueprintStore((state) => state.blueprints);
22
+ const [loading, setLoading] = React.useState(false);
23
+ const [error, setError] = React.useState(null);
24
+ const [fetchedBlueprint, setFetchedBlueprint] = React.useState(null);
25
+ const [deleting, setDeleting] = React.useState(false);
26
+ const [showDeleteConfirm, setShowDeleteConfirm] = React.useState(false);
27
+ // Find blueprint in store first
28
+ const blueprintFromStore = blueprints.find((b) => b.id === blueprintId);
29
+ // Polling function - must be defined before any early returns (Rules of Hooks)
30
+ const pollBlueprint = React.useCallback(async () => {
31
+ if (!blueprintId)
32
+ return null;
33
+ return getBlueprint(blueprintId);
34
+ }, [blueprintId]);
35
+ // Fetch blueprint from API if not in store or missing full details
36
+ React.useEffect(() => {
37
+ if (blueprintId && !loading && !fetchedBlueprint) {
38
+ // Always fetch full details since store may only have basic info
39
+ setLoading(true);
40
+ setError(null);
41
+ getBlueprint(blueprintId)
42
+ .then((blueprint) => {
43
+ setFetchedBlueprint(blueprint);
44
+ setLoading(false);
45
+ })
46
+ .catch((err) => {
47
+ setError(err);
48
+ setLoading(false);
49
+ });
50
+ }
51
+ }, [blueprintId, loading, fetchedBlueprint]);
52
+ // Use fetched blueprint for full details, fall back to store for basic display
53
+ const blueprint = fetchedBlueprint || blueprintFromStore;
54
+ // Show loading state while fetching
55
+ if (loading && !blueprint) {
56
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
57
+ { label: "Blueprints" },
58
+ { label: "Loading...", active: true },
59
+ ] }), _jsx(SpinnerComponent, { message: "Loading blueprint details..." })] }));
60
+ }
61
+ // Show error state if fetch failed
62
+ if (error && !blueprint) {
63
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Blueprints" }, { label: "Error", active: true }] }), _jsx(ErrorMessage, { message: "Failed to load blueprint details", error: error })] }));
64
+ }
65
+ // Show error if no blueprint found
66
+ if (!blueprint) {
67
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
68
+ { label: "Blueprints" },
69
+ { label: "Not Found", active: true },
70
+ ] }), _jsx(ErrorMessage, { message: `Blueprint ${blueprintId || "unknown"} not found`, error: new Error("Blueprint not found") })] }));
71
+ }
72
+ // Build detail sections
73
+ const detailSections = [];
74
+ // Basic details section
75
+ const basicFields = [];
76
+ if (blueprint.create_time_ms) {
77
+ basicFields.push({
78
+ label: "Created",
79
+ value: formatTimestamp(blueprint.create_time_ms),
80
+ });
81
+ }
82
+ if (blueprint.architecture) {
83
+ basicFields.push({
84
+ label: "Architecture",
85
+ value: blueprint.architecture,
86
+ });
87
+ }
88
+ if (blueprint.resources) {
89
+ basicFields.push({
90
+ label: "Resources",
91
+ value: blueprint.resources,
92
+ });
93
+ }
94
+ if (basicFields.length > 0) {
95
+ detailSections.push({
96
+ title: "Details",
97
+ icon: figures.squareSmallFilled,
98
+ color: colors.warning,
99
+ fields: basicFields,
100
+ });
101
+ }
102
+ // Launch parameters section
103
+ const lp = blueprint.parameters?.launch_parameters;
104
+ if (lp) {
105
+ const lpFields = [];
106
+ if (lp.custom_cpu_cores) {
107
+ lpFields.push({
108
+ label: "CPU Cores",
109
+ value: String(lp.custom_cpu_cores),
110
+ });
111
+ }
112
+ if (lp.custom_gb_memory) {
113
+ lpFields.push({
114
+ label: "Memory",
115
+ value: `${lp.custom_gb_memory}GB`,
116
+ });
117
+ }
118
+ if (lp.custom_disk_size) {
119
+ lpFields.push({
120
+ label: "Disk Size",
121
+ value: `${lp.custom_disk_size}GB`,
122
+ });
123
+ }
124
+ if (lp.keep_alive_time_seconds) {
125
+ const minutes = Math.floor(lp.keep_alive_time_seconds / 60);
126
+ const hours = Math.floor(minutes / 60);
127
+ lpFields.push({
128
+ label: "Keep Alive",
129
+ value: hours > 0 ? `${hours}h ${minutes % 60}m` : `${minutes}m`,
130
+ });
131
+ }
132
+ if (lp.available_ports && lp.available_ports.length > 0) {
133
+ lpFields.push({
134
+ label: "Available Ports",
135
+ value: lp.available_ports.join(", "),
136
+ });
137
+ }
138
+ if (lp.required_services && lp.required_services.length > 0) {
139
+ lpFields.push({
140
+ label: "Required Services",
141
+ value: lp.required_services.join(", "),
142
+ });
143
+ }
144
+ if (lpFields.length > 0) {
145
+ detailSections.push({
146
+ title: "Launch Parameters",
147
+ icon: figures.arrowRight,
148
+ color: colors.secondary,
149
+ fields: lpFields,
150
+ });
151
+ }
152
+ }
153
+ // Setup section
154
+ const params = blueprint.parameters;
155
+ if (params) {
156
+ const setupFields = [];
157
+ if (params.dockerfile) {
158
+ const lineCount = params.dockerfile.split("\n").length;
159
+ setupFields.push({
160
+ label: "Dockerfile",
161
+ value: _jsxs(Text, { dimColor: true, children: [lineCount, " lines"] }),
162
+ });
163
+ }
164
+ if (params.system_setup_commands &&
165
+ params.system_setup_commands.length > 0) {
166
+ setupFields.push({
167
+ label: "Setup Commands",
168
+ value: `${params.system_setup_commands.length} commands`,
169
+ });
170
+ }
171
+ if (params.file_mounts && Object.keys(params.file_mounts).length > 0) {
172
+ setupFields.push({
173
+ label: "File Mounts",
174
+ value: `${Object.keys(params.file_mounts).length} mounts`,
175
+ });
176
+ }
177
+ if (setupFields.length > 0) {
178
+ detailSections.push({
179
+ title: "Build Configuration",
180
+ icon: figures.hamburger,
181
+ color: colors.info,
182
+ fields: setupFields,
183
+ });
184
+ }
185
+ }
186
+ // Operations available for blueprints
187
+ const operations = [
188
+ {
189
+ key: "logs",
190
+ label: "View Build Logs",
191
+ color: colors.info,
192
+ icon: figures.info,
193
+ shortcut: "l",
194
+ },
195
+ {
196
+ key: "create-devbox",
197
+ label: "Create Devbox from Blueprint",
198
+ color: colors.success,
199
+ icon: figures.play,
200
+ shortcut: "c",
201
+ },
202
+ {
203
+ key: "delete",
204
+ label: "Delete Blueprint",
205
+ color: colors.error,
206
+ icon: figures.cross,
207
+ shortcut: "d",
208
+ },
209
+ ];
210
+ // Handle operation selection
211
+ const handleOperation = async (operation, resource) => {
212
+ switch (operation) {
213
+ case "logs":
214
+ navigate("blueprint-logs", { blueprintId: resource.id });
215
+ break;
216
+ case "create-devbox":
217
+ navigate("devbox-create", { blueprintId: resource.id });
218
+ break;
219
+ case "delete":
220
+ // Show confirmation dialog
221
+ setShowDeleteConfirm(true);
222
+ break;
223
+ }
224
+ };
225
+ // Execute delete after confirmation
226
+ const executeDelete = async () => {
227
+ if (!blueprint)
228
+ return;
229
+ setShowDeleteConfirm(false);
230
+ setDeleting(true);
231
+ try {
232
+ const client = getClient();
233
+ await client.blueprints.delete(blueprint.id);
234
+ goBack();
235
+ }
236
+ catch (err) {
237
+ setError(err);
238
+ setDeleting(false);
239
+ }
240
+ };
241
+ // Show delete confirmation
242
+ if (showDeleteConfirm && blueprint) {
243
+ return (_jsx(ConfirmationPrompt, { title: "Delete Blueprint", message: `Are you sure you want to delete "${blueprint.name || blueprint.id}"?`, details: "This action cannot be undone.", breadcrumbItems: [
244
+ { label: "Blueprints" },
245
+ { label: blueprint.name || blueprint.id },
246
+ { label: "Delete", active: true },
247
+ ], onConfirm: executeDelete, onCancel: () => setShowDeleteConfirm(false) }));
248
+ }
249
+ // Show deleting state
250
+ if (deleting) {
251
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
252
+ { label: "Blueprints" },
253
+ { label: blueprint?.name || blueprint?.id || "Blueprint" },
254
+ { label: "Deleting...", active: true },
255
+ ] }), _jsx(SpinnerComponent, { message: "Deleting blueprint..." })] }));
256
+ }
257
+ // Build detailed info lines for full details view
258
+ const buildDetailLines = (bp) => {
259
+ const lines = [];
260
+ // Core Information
261
+ lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Blueprint Details" }, "core-title"));
262
+ lines.push(_jsxs(Text, { color: colors.idColor, children: [" ", "ID: ", bp.id] }, "core-id"));
263
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Name: ", bp.name || "(none)"] }, "core-name"));
264
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Status: ", bp.status] }, "core-status"));
265
+ if (bp.create_time_ms) {
266
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Created: ", new Date(bp.create_time_ms).toLocaleString()] }, "core-created"));
267
+ }
268
+ lines.push(_jsx(Text, { children: " " }, "core-space"));
269
+ // Launch Parameters
270
+ const lp = bp.parameters?.launch_parameters;
271
+ if (lp) {
272
+ lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Launch Parameters" }, "lp-title"));
273
+ if (lp.architecture) {
274
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Architecture: ", lp.architecture] }, "lp-arch"));
275
+ }
276
+ if (lp.resource_size_request) {
277
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Resource Size: ", lp.resource_size_request] }, "lp-resources"));
278
+ }
279
+ if (lp.custom_cpu_cores) {
280
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "CPU Cores: ", lp.custom_cpu_cores] }, "lp-cpu"));
281
+ }
282
+ if (lp.custom_gb_memory) {
283
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Memory: ", lp.custom_gb_memory, "GB"] }, "lp-memory"));
284
+ }
285
+ if (lp.custom_disk_size) {
286
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Disk Size: ", lp.custom_disk_size, "GB"] }, "lp-disk"));
287
+ }
288
+ if (lp.keep_alive_time_seconds) {
289
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Keep Alive: ", lp.keep_alive_time_seconds, "s"] }, "lp-keepalive"));
290
+ }
291
+ if (lp.available_ports && lp.available_ports.length > 0) {
292
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Available Ports: ", lp.available_ports.join(", ")] }, "lp-ports"));
293
+ }
294
+ if (lp.launch_commands && lp.launch_commands.length > 0) {
295
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Launch Commands:"] }, "lp-launch-cmds"));
296
+ lp.launch_commands.forEach((cmd, idx) => {
297
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", figures.pointer, " ", cmd] }, `lp-cmd-${idx}`));
298
+ });
299
+ }
300
+ lines.push(_jsx(Text, { children: " " }, "lp-space"));
301
+ }
302
+ // Build Configuration
303
+ const params = bp.parameters;
304
+ if (params) {
305
+ if (params.dockerfile) {
306
+ lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Dockerfile" }, "dockerfile-title"));
307
+ params.dockerfile.split("\n").forEach((line, idx) => {
308
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", line] }, `dockerfile-${idx}`));
309
+ });
310
+ lines.push(_jsx(Text, { children: " " }, "dockerfile-space"));
311
+ }
312
+ if (params.system_setup_commands &&
313
+ params.system_setup_commands.length > 0) {
314
+ lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "System Setup Commands" }, "setup-title"));
315
+ params.system_setup_commands.forEach((cmd, idx) => {
316
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", idx + 1, ". ", cmd] }, `setup-${idx}`));
317
+ });
318
+ lines.push(_jsx(Text, { children: " " }, "setup-space"));
319
+ }
320
+ if (params.file_mounts && Object.keys(params.file_mounts).length > 0) {
321
+ lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "File Mounts" }, "mounts-title"));
322
+ Object.entries(params.file_mounts).forEach(([path, _content], idx) => {
323
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", path] }, `mount-${idx}`));
324
+ });
325
+ lines.push(_jsx(Text, { children: " " }, "mounts-space"));
326
+ }
327
+ }
328
+ // Raw JSON
329
+ lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Raw JSON" }, "json-title"));
330
+ const jsonLines = JSON.stringify(bp, null, 2).split("\n");
331
+ jsonLines.forEach((line, idx) => {
332
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", line] }, `json-${idx}`));
333
+ });
334
+ return lines;
335
+ };
336
+ return (_jsx(ResourceDetailPage, { resource: blueprint, resourceType: "Blueprints", getDisplayName: (bp) => bp.name || bp.id, getId: (bp) => bp.id, getStatus: (bp) => bp.status, detailSections: detailSections, operations: operations, onOperation: handleOperation, onBack: goBack, buildDetailLines: buildDetailLines, pollResource: blueprint.status === "building" ? pollBlueprint : undefined }));
337
+ }
@@ -14,6 +14,12 @@ export function MenuScreen() {
14
14
  case "snapshots":
15
15
  navigate("snapshot-list");
16
16
  break;
17
+ case "network-policies":
18
+ navigate("network-policy-list");
19
+ break;
20
+ case "objects":
21
+ navigate("object-list");
22
+ break;
17
23
  default:
18
24
  // Fallback for any other screen names
19
25
  navigate(key);
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useNavigation } from "../store/navigationStore.js";
3
+ import { NetworkPolicyCreatePage } from "../components/NetworkPolicyCreatePage.js";
4
+ export function NetworkPolicyCreateScreen() {
5
+ const { goBack, navigate } = useNavigation();
6
+ return (_jsx(NetworkPolicyCreatePage, { onBack: goBack, onCreate: (policy) => navigate("network-policy-detail", { networkPolicyId: policy.id }) }));
7
+ }
@@ -0,0 +1,247 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * NetworkPolicyDetailScreen - Detail page for network policies
4
+ * Uses the generic ResourceDetailPage component
5
+ */
6
+ import React from "react";
7
+ import { Text } from "ink";
8
+ import figures from "figures";
9
+ import { useNavigation } from "../store/navigationStore.js";
10
+ import { useNetworkPolicyStore, } from "../store/networkPolicyStore.js";
11
+ import { ResourceDetailPage, formatTimestamp, } from "../components/ResourceDetailPage.js";
12
+ import { getNetworkPolicy, deleteNetworkPolicy, } from "../services/networkPolicyService.js";
13
+ import { SpinnerComponent } from "../components/Spinner.js";
14
+ import { ErrorMessage } from "../components/ErrorMessage.js";
15
+ import { Breadcrumb } from "../components/Breadcrumb.js";
16
+ import { ConfirmationPrompt } from "../components/ConfirmationPrompt.js";
17
+ import { NetworkPolicyCreatePage } from "../components/NetworkPolicyCreatePage.js";
18
+ import { colors } from "../utils/theme.js";
19
+ /**
20
+ * Get a display label for the egress policy type
21
+ */
22
+ function getEgressTypeLabel(egress) {
23
+ if (egress.allow_all) {
24
+ return "Allow All";
25
+ }
26
+ if (egress.allowed_hostnames.length === 0) {
27
+ return "Deny All";
28
+ }
29
+ return "Custom";
30
+ }
31
+ export function NetworkPolicyDetailScreen({ networkPolicyId, }) {
32
+ const { goBack } = useNavigation();
33
+ const networkPolicies = useNetworkPolicyStore((state) => state.networkPolicies);
34
+ const [loading, setLoading] = React.useState(false);
35
+ const [error, setError] = React.useState(null);
36
+ const [fetchedPolicy, setFetchedPolicy] = React.useState(null);
37
+ const [deleting, setDeleting] = React.useState(false);
38
+ const [showDeleteConfirm, setShowDeleteConfirm] = React.useState(false);
39
+ const [showEditForm, setShowEditForm] = React.useState(false);
40
+ // Find policy in store first
41
+ const policyFromStore = networkPolicies.find((p) => p.id === networkPolicyId);
42
+ // Fetch policy from API if not in store or missing full details
43
+ React.useEffect(() => {
44
+ if (networkPolicyId && !loading && !fetchedPolicy) {
45
+ // Always fetch full details since store may only have basic info
46
+ setLoading(true);
47
+ setError(null);
48
+ getNetworkPolicy(networkPolicyId)
49
+ .then((policy) => {
50
+ setFetchedPolicy(policy);
51
+ setLoading(false);
52
+ })
53
+ .catch((err) => {
54
+ setError(err);
55
+ setLoading(false);
56
+ });
57
+ }
58
+ }, [networkPolicyId, loading, fetchedPolicy]);
59
+ // Use fetched policy for full details, fall back to store for basic display
60
+ const policy = fetchedPolicy || policyFromStore;
61
+ // Show loading state while fetching
62
+ if (loading && !policy) {
63
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
64
+ { label: "Network Policies" },
65
+ { label: "Loading...", active: true },
66
+ ] }), _jsx(SpinnerComponent, { message: "Loading network policy details..." })] }));
67
+ }
68
+ // Show error state if fetch failed
69
+ if (error && !policy) {
70
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
71
+ { label: "Network Policies" },
72
+ { label: "Error", active: true },
73
+ ] }), _jsx(ErrorMessage, { message: "Failed to load network policy details", error: error })] }));
74
+ }
75
+ // Show error if no policy found
76
+ if (!policy) {
77
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
78
+ { label: "Network Policies" },
79
+ { label: "Not Found", active: true },
80
+ ] }), _jsx(ErrorMessage, { message: `Network policy ${networkPolicyId || "unknown"} not found`, error: new Error("Network policy not found") })] }));
81
+ }
82
+ // Build detail sections
83
+ const detailSections = [];
84
+ // Basic details section
85
+ const basicFields = [];
86
+ if (policy.description) {
87
+ basicFields.push({
88
+ label: "Description",
89
+ value: policy.description,
90
+ });
91
+ }
92
+ if (policy.create_time_ms) {
93
+ basicFields.push({
94
+ label: "Created",
95
+ value: formatTimestamp(policy.create_time_ms),
96
+ });
97
+ }
98
+ if (policy.update_time_ms) {
99
+ basicFields.push({
100
+ label: "Last Updated",
101
+ value: formatTimestamp(policy.update_time_ms),
102
+ });
103
+ }
104
+ if (basicFields.length > 0) {
105
+ detailSections.push({
106
+ title: "Details",
107
+ icon: figures.squareSmallFilled,
108
+ color: colors.warning,
109
+ fields: basicFields,
110
+ });
111
+ }
112
+ // Egress rules section
113
+ const egressFields = [];
114
+ egressFields.push({
115
+ label: "Policy Type",
116
+ value: (_jsx(Text, { color: policy.egress.allow_all
117
+ ? colors.success
118
+ : policy.egress.allowed_hostnames.length === 0
119
+ ? colors.error
120
+ : colors.warning, bold: true, children: getEgressTypeLabel(policy.egress) })),
121
+ });
122
+ egressFields.push({
123
+ label: "Allow Devbox-to-Devbox",
124
+ value: policy.egress.allow_devbox_to_devbox ? "Yes" : "No",
125
+ });
126
+ if (policy.egress.allowed_hostnames &&
127
+ policy.egress.allowed_hostnames.length > 0) {
128
+ egressFields.push({
129
+ label: "Allowed Hostnames",
130
+ value: `${policy.egress.allowed_hostnames.length} hostname(s)`,
131
+ });
132
+ }
133
+ detailSections.push({
134
+ title: "Egress Rules",
135
+ icon: figures.arrowRight,
136
+ color: colors.info,
137
+ fields: egressFields,
138
+ });
139
+ // Operations available for network policies
140
+ const operations = [
141
+ {
142
+ key: "edit",
143
+ label: "Edit Network Policy",
144
+ color: colors.warning,
145
+ icon: figures.pointer,
146
+ shortcut: "e",
147
+ },
148
+ {
149
+ key: "delete",
150
+ label: "Delete Network Policy",
151
+ color: colors.error,
152
+ icon: figures.cross,
153
+ shortcut: "d",
154
+ },
155
+ ];
156
+ // Handle operation selection
157
+ const handleOperation = async (operation, _resource) => {
158
+ switch (operation) {
159
+ case "edit":
160
+ setShowEditForm(true);
161
+ break;
162
+ case "delete":
163
+ // Show confirmation dialog
164
+ setShowDeleteConfirm(true);
165
+ break;
166
+ }
167
+ };
168
+ // Execute delete after confirmation
169
+ const executeDelete = async () => {
170
+ if (!policy)
171
+ return;
172
+ setShowDeleteConfirm(false);
173
+ setDeleting(true);
174
+ try {
175
+ await deleteNetworkPolicy(policy.id);
176
+ goBack();
177
+ }
178
+ catch (err) {
179
+ setError(err);
180
+ setDeleting(false);
181
+ }
182
+ };
183
+ // Build detailed info lines for full details view
184
+ const buildDetailLines = (np) => {
185
+ const lines = [];
186
+ // Core Information
187
+ lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Network Policy Details" }, "core-title"));
188
+ lines.push(_jsxs(Text, { color: colors.idColor, children: [" ", "ID: ", np.id] }, "core-id"));
189
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Name: ", np.name] }, "core-name"));
190
+ if (np.description) {
191
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Description: ", np.description] }, "core-desc"));
192
+ }
193
+ if (np.create_time_ms) {
194
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Created: ", new Date(np.create_time_ms).toLocaleString()] }, "core-created"));
195
+ }
196
+ if (np.update_time_ms) {
197
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Last Updated: ", new Date(np.update_time_ms).toLocaleString()] }, "core-updated"));
198
+ }
199
+ lines.push(_jsx(Text, { children: " " }, "core-space"));
200
+ // Egress Rules
201
+ lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Egress Rules" }, "egress-title"));
202
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Policy Type: ", getEgressTypeLabel(np.egress)] }, "egress-type"));
203
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Allow All: ", np.egress.allow_all ? "Yes" : "No"] }, "egress-allow-all"));
204
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Allow Devbox-to-Devbox:", " ", np.egress.allow_devbox_to_devbox ? "Yes" : "No"] }, "egress-devbox"));
205
+ lines.push(_jsx(Text, { children: " " }, "egress-space"));
206
+ // Allowed Hostnames
207
+ if (np.egress.allowed_hostnames && np.egress.allowed_hostnames.length > 0) {
208
+ lines.push(_jsxs(Text, { color: colors.warning, bold: true, children: ["Allowed Hostnames (", np.egress.allowed_hostnames.length, ")"] }, "hostnames-title"));
209
+ np.egress.allowed_hostnames.forEach((hostname, idx) => {
210
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", figures.pointer, " ", hostname] }, `hostname-${idx}`));
211
+ });
212
+ lines.push(_jsx(Text, { children: " " }, "hostnames-space"));
213
+ }
214
+ // Raw JSON
215
+ lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Raw JSON" }, "json-title"));
216
+ const jsonLines = JSON.stringify(np, null, 2).split("\n");
217
+ jsonLines.forEach((line, idx) => {
218
+ lines.push(_jsxs(Text, { dimColor: true, children: [" ", line] }, `json-${idx}`));
219
+ });
220
+ return lines;
221
+ };
222
+ // Show edit form
223
+ if (showEditForm && policy) {
224
+ return (_jsx(NetworkPolicyCreatePage, { onBack: () => setShowEditForm(false), onCreate: (updatedPolicy) => {
225
+ // Update the fetched policy with the new data
226
+ setFetchedPolicy(updatedPolicy);
227
+ setShowEditForm(false);
228
+ }, initialPolicy: policy }));
229
+ }
230
+ // Show delete confirmation
231
+ if (showDeleteConfirm && policy) {
232
+ return (_jsx(ConfirmationPrompt, { title: "Delete Network Policy", message: `Are you sure you want to delete "${policy.name || policy.id}"?`, details: "This action cannot be undone. Any devboxes using this policy will lose their network restrictions.", breadcrumbItems: [
233
+ { label: "Network Policies" },
234
+ { label: policy.name || policy.id },
235
+ { label: "Delete", active: true },
236
+ ], onConfirm: executeDelete, onCancel: () => setShowDeleteConfirm(false) }));
237
+ }
238
+ // Show deleting state
239
+ if (deleting) {
240
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
241
+ { label: "Network Policies" },
242
+ { label: policy.name || policy.id },
243
+ { label: "Deleting...", active: true },
244
+ ] }), _jsx(SpinnerComponent, { message: "Deleting network policy..." })] }));
245
+ }
246
+ return (_jsx(ResourceDetailPage, { resource: policy, resourceType: "Network Policies", getDisplayName: (np) => np.name || np.id, getId: (np) => np.id, getStatus: () => "active", detailSections: detailSections, operations: operations, onOperation: handleOperation, onBack: goBack, buildDetailLines: buildDetailLines }));
247
+ }
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useNavigation } from "../store/navigationStore.js";
3
+ import { ListNetworkPoliciesUI } from "../commands/network-policy/list.js";
4
+ export function NetworkPolicyListScreen() {
5
+ const { goBack } = useNavigation();
6
+ return _jsx(ListNetworkPoliciesUI, { onBack: goBack });
7
+ }