@runloop/rl-cli 0.1.1 → 0.2.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 (96) hide show
  1. package/README.md +54 -0
  2. package/dist/cli.js +73 -60
  3. package/dist/commands/auth.js +0 -1
  4. package/dist/commands/blueprint/create.js +31 -83
  5. package/dist/commands/blueprint/get.js +29 -34
  6. package/dist/commands/blueprint/list.js +215 -213
  7. package/dist/commands/blueprint/logs.js +133 -37
  8. package/dist/commands/blueprint/preview.js +42 -38
  9. package/dist/commands/config.js +117 -0
  10. package/dist/commands/devbox/create.js +120 -40
  11. package/dist/commands/devbox/delete.js +17 -33
  12. package/dist/commands/devbox/download.js +29 -43
  13. package/dist/commands/devbox/exec.js +22 -39
  14. package/dist/commands/devbox/execAsync.js +20 -37
  15. package/dist/commands/devbox/get.js +13 -35
  16. package/dist/commands/devbox/getAsync.js +12 -34
  17. package/dist/commands/devbox/list.js +241 -402
  18. package/dist/commands/devbox/logs.js +20 -38
  19. package/dist/commands/devbox/read.js +29 -43
  20. package/dist/commands/devbox/resume.js +13 -35
  21. package/dist/commands/devbox/rsync.js +26 -78
  22. package/dist/commands/devbox/scp.js +25 -79
  23. package/dist/commands/devbox/sendStdin.js +41 -0
  24. package/dist/commands/devbox/shutdown.js +13 -35
  25. package/dist/commands/devbox/ssh.js +45 -78
  26. package/dist/commands/devbox/suspend.js +13 -35
  27. package/dist/commands/devbox/tunnel.js +36 -88
  28. package/dist/commands/devbox/upload.js +28 -36
  29. package/dist/commands/devbox/write.js +29 -44
  30. package/dist/commands/mcp-install.js +4 -3
  31. package/dist/commands/menu.js +24 -66
  32. package/dist/commands/object/delete.js +12 -34
  33. package/dist/commands/object/download.js +26 -74
  34. package/dist/commands/object/get.js +12 -34
  35. package/dist/commands/object/list.js +15 -93
  36. package/dist/commands/object/upload.js +35 -96
  37. package/dist/commands/snapshot/create.js +23 -39
  38. package/dist/commands/snapshot/delete.js +17 -33
  39. package/dist/commands/snapshot/get.js +16 -0
  40. package/dist/commands/snapshot/list.js +309 -80
  41. package/dist/commands/snapshot/status.js +12 -34
  42. package/dist/components/ActionsPopup.js +63 -39
  43. package/dist/components/Breadcrumb.js +10 -52
  44. package/dist/components/DevboxActionsMenu.js +182 -110
  45. package/dist/components/DevboxCreatePage.js +12 -7
  46. package/dist/components/DevboxDetailPage.js +76 -28
  47. package/dist/components/ErrorBoundary.js +29 -0
  48. package/dist/components/ErrorMessage.js +10 -2
  49. package/dist/components/Header.js +12 -4
  50. package/dist/components/InteractiveSpawn.js +94 -0
  51. package/dist/components/MainMenu.js +36 -32
  52. package/dist/components/MetadataDisplay.js +4 -4
  53. package/dist/components/OperationsMenu.js +1 -1
  54. package/dist/components/ResourceActionsMenu.js +4 -4
  55. package/dist/components/ResourceListView.js +46 -34
  56. package/dist/components/Spinner.js +7 -2
  57. package/dist/components/StatusBadge.js +1 -1
  58. package/dist/components/SuccessMessage.js +12 -2
  59. package/dist/components/Table.js +16 -6
  60. package/dist/hooks/useCursorPagination.js +125 -85
  61. package/dist/hooks/useExitOnCtrlC.js +14 -0
  62. package/dist/hooks/useViewportHeight.js +47 -0
  63. package/dist/mcp/server.js +65 -6
  64. package/dist/router/Router.js +68 -0
  65. package/dist/router/types.js +1 -0
  66. package/dist/screens/BlueprintListScreen.js +7 -0
  67. package/dist/screens/DevboxActionsScreen.js +25 -0
  68. package/dist/screens/DevboxCreateScreen.js +11 -0
  69. package/dist/screens/DevboxDetailScreen.js +60 -0
  70. package/dist/screens/DevboxListScreen.js +23 -0
  71. package/dist/screens/LogsSessionScreen.js +49 -0
  72. package/dist/screens/MenuScreen.js +23 -0
  73. package/dist/screens/SSHSessionScreen.js +55 -0
  74. package/dist/screens/SnapshotListScreen.js +7 -0
  75. package/dist/services/blueprintService.js +105 -0
  76. package/dist/services/devboxService.js +215 -0
  77. package/dist/services/snapshotService.js +81 -0
  78. package/dist/store/blueprintStore.js +89 -0
  79. package/dist/store/devboxStore.js +105 -0
  80. package/dist/store/index.js +7 -0
  81. package/dist/store/navigationStore.js +101 -0
  82. package/dist/store/snapshotStore.js +87 -0
  83. package/dist/utils/CommandExecutor.js +53 -24
  84. package/dist/utils/client.js +0 -2
  85. package/dist/utils/config.js +20 -90
  86. package/dist/utils/interactiveCommand.js +3 -2
  87. package/dist/utils/logFormatter.js +162 -0
  88. package/dist/utils/memoryMonitor.js +85 -0
  89. package/dist/utils/output.js +150 -59
  90. package/dist/utils/screen.js +23 -0
  91. package/dist/utils/ssh.js +3 -1
  92. package/dist/utils/sshSession.js +5 -29
  93. package/dist/utils/terminalDetection.js +97 -0
  94. package/dist/utils/terminalSync.js +39 -0
  95. package/dist/utils/theme.js +147 -13
  96. package/package.json +16 -13
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Devbox Service - Handles all devbox API calls
3
+ * Returns plain data objects with no SDK reference retention
4
+ */
5
+ import { getClient } from "../utils/client.js";
6
+ /**
7
+ * Recursively truncate all strings in an object to prevent Yoga crashes
8
+ * CRITICAL: Must be applied to ALL data from API before storing/rendering
9
+ */
10
+ function truncateStrings(obj, maxLength = 200) {
11
+ if (obj === null || obj === undefined)
12
+ return obj;
13
+ if (typeof obj === "string") {
14
+ return (obj.length > maxLength ? obj.substring(0, maxLength) : obj);
15
+ }
16
+ if (Array.isArray(obj)) {
17
+ return obj.map((item) => truncateStrings(item, maxLength));
18
+ }
19
+ if (typeof obj === "object") {
20
+ const result = {};
21
+ for (const key in obj) {
22
+ if (Object.hasOwn(obj, key)) {
23
+ result[key] = truncateStrings(obj[key], maxLength);
24
+ }
25
+ }
26
+ return result;
27
+ }
28
+ return obj;
29
+ }
30
+ /**
31
+ * List devboxes with pagination
32
+ * CRITICAL: Creates defensive copies to break SDK reference chains
33
+ */
34
+ export async function listDevboxes(options) {
35
+ // Check if aborted before making request
36
+ if (options.signal?.aborted) {
37
+ throw new DOMException("Aborted", "AbortError");
38
+ }
39
+ const client = getClient();
40
+ const queryParams = {
41
+ limit: options.limit,
42
+ };
43
+ if (options.startingAfter) {
44
+ queryParams.starting_after = options.startingAfter;
45
+ }
46
+ if (options.status) {
47
+ queryParams.status = options.status;
48
+ }
49
+ if (options.search) {
50
+ queryParams.name = options.search;
51
+ }
52
+ // Fetch ONE page only - never iterate
53
+ const pagePromise = client.devboxes.list(queryParams);
54
+ // Wrap in Promise.race to support abort
55
+ let page;
56
+ if (options.signal) {
57
+ const abortPromise = new Promise((_, reject) => {
58
+ options.signal.addEventListener("abort", () => {
59
+ reject(new DOMException("Aborted", "AbortError"));
60
+ });
61
+ });
62
+ try {
63
+ page = (await Promise.race([
64
+ pagePromise,
65
+ abortPromise,
66
+ ]));
67
+ }
68
+ catch (err) {
69
+ // Re-throw abort errors, convert others
70
+ if (err?.name === "AbortError") {
71
+ throw err;
72
+ }
73
+ throw err;
74
+ }
75
+ }
76
+ else {
77
+ page = (await pagePromise);
78
+ }
79
+ // Check again after await (in case abort happened during request)
80
+ if (options.signal?.aborted) {
81
+ throw new DOMException("Aborted", "AbortError");
82
+ }
83
+ // Extract data and create defensive copies immediately
84
+ const devboxes = [];
85
+ if (page.devboxes && Array.isArray(page.devboxes)) {
86
+ page.devboxes.forEach((d) => {
87
+ // CRITICAL: Recursively truncate ALL strings in the object to prevent Yoga crashes
88
+ // This catches nested fields like launch_parameters.user_parameters.username
89
+ const truncated = truncateStrings(d, 200);
90
+ devboxes.push(truncated);
91
+ });
92
+ }
93
+ const result = {
94
+ devboxes,
95
+ totalCount: page.total_count || devboxes.length,
96
+ hasMore: page.has_more || false,
97
+ };
98
+ return result;
99
+ }
100
+ /**
101
+ * Get a single devbox by ID
102
+ */
103
+ export async function getDevbox(id) {
104
+ const client = getClient();
105
+ const devbox = await client.devboxes.retrieve(id);
106
+ // CRITICAL: Recursively truncate ALL strings in the object to prevent Yoga crashes
107
+ return truncateStrings(devbox, 200);
108
+ }
109
+ /**
110
+ * Delete a devbox (actually shuts it down)
111
+ */
112
+ export async function deleteDevbox(id) {
113
+ const client = getClient();
114
+ await client.devboxes.shutdown(id);
115
+ }
116
+ /**
117
+ * Shutdown a devbox
118
+ */
119
+ export async function shutdownDevbox(id) {
120
+ const client = getClient();
121
+ await client.devboxes.shutdown(id);
122
+ }
123
+ /**
124
+ * Suspend a devbox
125
+ */
126
+ export async function suspendDevbox(id) {
127
+ const client = getClient();
128
+ await client.devboxes.suspend(id);
129
+ }
130
+ /**
131
+ * Resume a devbox
132
+ */
133
+ export async function resumeDevbox(id) {
134
+ const client = getClient();
135
+ await client.devboxes.resume(id);
136
+ }
137
+ /**
138
+ * Upload file to devbox
139
+ */
140
+ export async function uploadFile(id, filepath, remotePath) {
141
+ const client = getClient();
142
+ const fs = await import("fs");
143
+ const fileStream = fs.createReadStream(filepath);
144
+ await client.devboxes.uploadFile(id, {
145
+ file: fileStream,
146
+ path: remotePath,
147
+ });
148
+ }
149
+ /**
150
+ * Create snapshot of devbox
151
+ */
152
+ export async function createSnapshot(id, name) {
153
+ const client = getClient();
154
+ const snapshot = await client.devboxes.snapshotDisk(id, { name });
155
+ return {
156
+ id: String(snapshot.id || "").substring(0, 100),
157
+ name: snapshot.name ? String(snapshot.name).substring(0, 200) : undefined,
158
+ };
159
+ }
160
+ /**
161
+ * Create SSH key for devbox
162
+ */
163
+ export async function createSSHKey(id) {
164
+ const client = getClient();
165
+ const result = await client.devboxes.createSSHKey(id);
166
+ // Truncate keys if they're unexpectedly long (shouldn't happen, but safety)
167
+ return {
168
+ ssh_private_key: String(result.ssh_private_key || "").substring(0, 10000),
169
+ url: String(result.url || "").substring(0, 500),
170
+ };
171
+ }
172
+ /**
173
+ * Create tunnel to devbox
174
+ */
175
+ export async function createTunnel(id, port) {
176
+ const client = getClient();
177
+ const tunnel = await client.devboxes.createTunnel(id, { port });
178
+ return {
179
+ url: String(tunnel.url || "").substring(0, 500),
180
+ };
181
+ }
182
+ /**
183
+ * Execute command in devbox
184
+ */
185
+ export async function execCommand(id, command) {
186
+ const client = getClient();
187
+ const result = await client.devboxes.executeSync(id, { command });
188
+ // CRITICAL: Truncate output to prevent Yoga crashes
189
+ const MAX_OUTPUT_LENGTH = 10000; // Allow more for command output
190
+ let stdout = String(result.stdout || "");
191
+ let stderr = String(result.stderr || "");
192
+ if (stdout.length > MAX_OUTPUT_LENGTH) {
193
+ stdout =
194
+ stdout.substring(0, MAX_OUTPUT_LENGTH) + "\n... (output truncated)";
195
+ }
196
+ if (stderr.length > MAX_OUTPUT_LENGTH) {
197
+ stderr =
198
+ stderr.substring(0, MAX_OUTPUT_LENGTH) + "\n... (output truncated)";
199
+ }
200
+ return {
201
+ stdout,
202
+ stderr,
203
+ exit_code: result.exit_code || 0,
204
+ };
205
+ }
206
+ /**
207
+ * Get devbox logs
208
+ * Returns the raw logs array from the API response
209
+ */
210
+ export async function getDevboxLogs(id) {
211
+ const client = getClient();
212
+ const response = await client.devboxes.logs.list(id);
213
+ // Return the logs array directly - formatting is handled by logFormatter
214
+ return response.logs || [];
215
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Snapshot Service - Handles all snapshot API calls
3
+ */
4
+ import { getClient } from "../utils/client.js";
5
+ /**
6
+ * List snapshots with pagination
7
+ */
8
+ export async function listSnapshots(options) {
9
+ const client = getClient();
10
+ const queryParams = {
11
+ limit: options.limit,
12
+ };
13
+ if (options.startingAfter) {
14
+ queryParams.starting_after = options.startingAfter;
15
+ }
16
+ if (options.devboxId) {
17
+ queryParams.devbox_id = options.devboxId;
18
+ }
19
+ const pagePromise = client.devboxes.listDiskSnapshots(queryParams);
20
+ const page = (await pagePromise);
21
+ const snapshots = [];
22
+ if (page.snapshots && Array.isArray(page.snapshots)) {
23
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
24
+ page.snapshots.forEach((s) => {
25
+ // CRITICAL: Truncate all strings to prevent Yoga crashes
26
+ const MAX_ID_LENGTH = 100;
27
+ const MAX_NAME_LENGTH = 200;
28
+ const MAX_STATUS_LENGTH = 50;
29
+ // Status is constructed/available in API response but not in type definition
30
+ const snapshotView = s;
31
+ snapshots.push({
32
+ id: String(snapshotView.id || "").substring(0, MAX_ID_LENGTH),
33
+ name: snapshotView.name
34
+ ? String(snapshotView.name).substring(0, MAX_NAME_LENGTH)
35
+ : undefined,
36
+ devbox_id: String(snapshotView.source_devbox_id || "").substring(0, MAX_ID_LENGTH),
37
+ status: snapshotView.status
38
+ ? String(snapshotView.status).substring(0, MAX_STATUS_LENGTH)
39
+ : "",
40
+ create_time_ms: snapshotView.create_time_ms,
41
+ });
42
+ });
43
+ }
44
+ const result = {
45
+ snapshots,
46
+ totalCount: page.total_count || snapshots.length,
47
+ hasMore: page.has_more || false,
48
+ };
49
+ return result;
50
+ }
51
+ /**
52
+ * Get snapshot status by ID
53
+ */
54
+ export async function getSnapshotStatus(id) {
55
+ const client = getClient();
56
+ const status = await client.devboxes.diskSnapshots.queryStatus(id);
57
+ return status;
58
+ }
59
+ /**
60
+ * Create a snapshot
61
+ */
62
+ export async function createSnapshot(devboxId, name) {
63
+ const client = getClient();
64
+ const snapshot = await client.devboxes.snapshotDisk(devboxId, {
65
+ name,
66
+ });
67
+ return {
68
+ id: snapshot.id,
69
+ name: snapshot.name || undefined,
70
+ devbox_id: snapshot.devbox_id || devboxId,
71
+ status: snapshot.status || "pending",
72
+ create_time_ms: snapshot.create_time_ms,
73
+ };
74
+ }
75
+ /**
76
+ * Delete a snapshot
77
+ */
78
+ export async function deleteSnapshot(id) {
79
+ const client = getClient();
80
+ await client.devboxes.diskSnapshots.delete(id);
81
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Blueprint Store - Manages blueprint list state, pagination, and caching
3
+ */
4
+ import { create } from "zustand";
5
+ const MAX_CACHE_SIZE = 10;
6
+ export const useBlueprintStore = create((set, get) => ({
7
+ blueprints: [],
8
+ loading: false,
9
+ initialLoading: true,
10
+ error: null,
11
+ currentPage: 0,
12
+ pageSize: 10,
13
+ totalCount: 0,
14
+ hasMore: false,
15
+ pageCache: new Map(),
16
+ lastIdCache: new Map(),
17
+ searchQuery: "",
18
+ selectedIndex: 0,
19
+ setBlueprints: (blueprints) => set({ blueprints }),
20
+ setLoading: (loading) => set({ loading }),
21
+ setInitialLoading: (loading) => set({ initialLoading: loading }),
22
+ setError: (error) => set({ error }),
23
+ setCurrentPage: (page) => set({ currentPage: page }),
24
+ setPageSize: (size) => set({ pageSize: size }),
25
+ setTotalCount: (count) => set({ totalCount: count }),
26
+ setHasMore: (hasMore) => set({ hasMore }),
27
+ setSearchQuery: (query) => set({ searchQuery: query }),
28
+ setSelectedIndex: (index) => set({ selectedIndex: index }),
29
+ cachePageData: (page, data, lastId) => {
30
+ const state = get();
31
+ const pageCache = state.pageCache;
32
+ const lastIdCache = state.lastIdCache;
33
+ // Aggressive LRU eviction
34
+ if (pageCache.size >= MAX_CACHE_SIZE) {
35
+ const oldestKey = pageCache.keys().next().value;
36
+ if (oldestKey !== undefined) {
37
+ pageCache.delete(oldestKey);
38
+ lastIdCache.delete(oldestKey);
39
+ }
40
+ }
41
+ // Create plain data objects to avoid SDK references
42
+ const plainData = data.map((b) => ({
43
+ id: b.id,
44
+ name: b.name,
45
+ status: b.status,
46
+ create_time_ms: b.create_time_ms,
47
+ build_status: b.build_status,
48
+ architecture: b.architecture,
49
+ resources: b.resources,
50
+ }));
51
+ pageCache.set(page, plainData);
52
+ lastIdCache.set(page, lastId);
53
+ set({});
54
+ },
55
+ getCachedPage: (page) => {
56
+ return get().pageCache.get(page);
57
+ },
58
+ clearCache: () => {
59
+ const state = get();
60
+ state.pageCache.clear();
61
+ state.lastIdCache.clear();
62
+ set({
63
+ pageCache: new Map(),
64
+ lastIdCache: new Map(),
65
+ });
66
+ },
67
+ clearAll: () => {
68
+ const state = get();
69
+ state.pageCache.clear();
70
+ state.lastIdCache.clear();
71
+ set({
72
+ blueprints: [],
73
+ loading: false,
74
+ initialLoading: true,
75
+ error: null,
76
+ currentPage: 0,
77
+ totalCount: 0,
78
+ hasMore: false,
79
+ pageCache: new Map(),
80
+ lastIdCache: new Map(),
81
+ searchQuery: "",
82
+ selectedIndex: 0,
83
+ });
84
+ },
85
+ getSelectedBlueprint: () => {
86
+ const state = get();
87
+ return state.blueprints[state.selectedIndex];
88
+ },
89
+ }));
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Devbox Store - Manages devbox list state, pagination, and caching
3
+ * Replaces useState/useRef from ListDevboxesUI
4
+ */
5
+ import { create } from "zustand";
6
+ const MAX_CACHE_SIZE = 10; // Limit cache to 10 pages
7
+ export const useDevboxStore = create((set, get) => ({
8
+ // Initial state
9
+ devboxes: [],
10
+ loading: false,
11
+ initialLoading: true,
12
+ error: null,
13
+ currentPage: 0,
14
+ pageSize: 10,
15
+ totalCount: 0,
16
+ hasMore: false,
17
+ pageCache: new Map(),
18
+ lastIdCache: new Map(),
19
+ searchQuery: "",
20
+ statusFilter: undefined,
21
+ selectedIndex: 0,
22
+ // Actions
23
+ setDevboxes: (devboxes) => {
24
+ const state = get();
25
+ const maxIndex = devboxes.length > 0 ? devboxes.length - 1 : 0;
26
+ const clampedIndex = Math.max(0, Math.min(state.selectedIndex, maxIndex));
27
+ set({ devboxes, selectedIndex: clampedIndex });
28
+ },
29
+ setLoading: (loading) => set({ loading }),
30
+ setInitialLoading: (loading) => set({ initialLoading: loading }),
31
+ setError: (error) => set({ error }),
32
+ setCurrentPage: (page) => set({ currentPage: page }),
33
+ setPageSize: (size) => set({ pageSize: size }),
34
+ setTotalCount: (count) => set({ totalCount: count }),
35
+ setHasMore: (hasMore) => set({ hasMore }),
36
+ setSearchQuery: (query) => set({ searchQuery: query }),
37
+ setStatusFilter: (status) => set({ statusFilter: status }),
38
+ setSelectedIndex: (index) => {
39
+ const state = get();
40
+ const maxIndex = state.devboxes.length > 0 ? state.devboxes.length - 1 : 0;
41
+ const clampedIndex = Math.max(0, Math.min(index, maxIndex));
42
+ set({ selectedIndex: clampedIndex });
43
+ },
44
+ // Cache management with LRU eviction - FIXED: No shallow copies
45
+ cachePageData: (page, data, lastId) => {
46
+ const state = get();
47
+ const pageCache = state.pageCache;
48
+ const lastIdCache = state.lastIdCache;
49
+ // Aggressive LRU eviction: Remove oldest entries if at limit
50
+ if (pageCache.size >= MAX_CACHE_SIZE) {
51
+ const oldestKey = pageCache.keys().next().value;
52
+ if (oldestKey !== undefined) {
53
+ pageCache.delete(oldestKey);
54
+ lastIdCache.delete(oldestKey);
55
+ }
56
+ }
57
+ // Deep copy all fields to avoid SDK references
58
+ const plainData = data.map((d) => {
59
+ // Create a deep copy using JSON serialization for safety
60
+ return JSON.parse(JSON.stringify(d));
61
+ });
62
+ pageCache.set(page, plainData);
63
+ lastIdCache.set(page, lastId);
64
+ // Trigger update without creating new Map
65
+ set({});
66
+ },
67
+ getCachedPage: (page) => {
68
+ return get().pageCache.get(page);
69
+ },
70
+ clearCache: () => {
71
+ const state = get();
72
+ // Explicitly clear all entries before reassigning
73
+ state.pageCache.clear();
74
+ state.lastIdCache.clear();
75
+ set({
76
+ pageCache: new Map(),
77
+ lastIdCache: new Map(),
78
+ });
79
+ },
80
+ // Aggressive memory cleanup
81
+ clearAll: () => {
82
+ const state = get();
83
+ // Clear existing structures first to release references
84
+ state.pageCache.clear();
85
+ state.lastIdCache.clear();
86
+ set({
87
+ devboxes: [],
88
+ loading: false,
89
+ initialLoading: true,
90
+ error: null,
91
+ currentPage: 0,
92
+ totalCount: 0,
93
+ hasMore: false,
94
+ pageCache: new Map(),
95
+ lastIdCache: new Map(),
96
+ searchQuery: "",
97
+ selectedIndex: 0,
98
+ });
99
+ },
100
+ // Getters
101
+ getSelectedDevbox: () => {
102
+ const state = get();
103
+ return state.devboxes[state.selectedIndex];
104
+ },
105
+ }));
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Root Store - Exports all stores for easy importing
3
+ */
4
+ export { useNavigation, useNavigationStore, NavigationProvider, } from "./navigationStore.js";
5
+ export { useDevboxStore } from "./devboxStore.js";
6
+ export { useBlueprintStore } from "./blueprintStore.js";
7
+ export { useSnapshotStore } from "./snapshotStore.js";
@@ -0,0 +1,101 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import React from "react";
3
+ const NavigationContext = React.createContext(null);
4
+ export function NavigationProvider({ initialScreen = "menu", initialParams = {}, children, }) {
5
+ // Use a single state object to avoid timing issues
6
+ const [state, setState] = React.useState({
7
+ currentScreen: initialScreen,
8
+ params: initialParams,
9
+ history: [],
10
+ });
11
+ const navigate = React.useCallback((screen, newParams = {}) => {
12
+ setState((prev) => ({
13
+ currentScreen: screen,
14
+ params: newParams,
15
+ history: [
16
+ ...prev.history,
17
+ { screen: prev.currentScreen, params: prev.params },
18
+ ],
19
+ }));
20
+ }, []);
21
+ const push = React.useCallback((screen, newParams = {}) => {
22
+ setState((prev) => ({
23
+ currentScreen: screen,
24
+ params: newParams,
25
+ history: [
26
+ ...prev.history,
27
+ { screen: prev.currentScreen, params: prev.params },
28
+ ],
29
+ }));
30
+ }, []);
31
+ const replace = React.useCallback((screen, newParams = {}) => {
32
+ setState((prev) => ({
33
+ ...prev,
34
+ currentScreen: screen,
35
+ params: newParams,
36
+ }));
37
+ }, []);
38
+ const goBack = React.useCallback(() => {
39
+ setState((prev) => {
40
+ if (prev.history.length > 0) {
41
+ const newHistory = [...prev.history];
42
+ const previousScreen = newHistory.pop();
43
+ return {
44
+ currentScreen: previousScreen.screen,
45
+ params: previousScreen.params,
46
+ history: newHistory,
47
+ };
48
+ }
49
+ else {
50
+ // If no history, go to menu
51
+ return {
52
+ currentScreen: "menu",
53
+ params: {},
54
+ history: [],
55
+ };
56
+ }
57
+ });
58
+ }, []);
59
+ const reset = React.useCallback(() => {
60
+ setState({
61
+ currentScreen: "menu",
62
+ params: {},
63
+ history: [],
64
+ });
65
+ }, []);
66
+ const canGoBack = React.useCallback(() => state.history.length > 0, [state.history.length]);
67
+ const value = React.useMemo(() => ({
68
+ currentScreen: state.currentScreen,
69
+ params: state.params,
70
+ navigate,
71
+ push,
72
+ replace,
73
+ goBack,
74
+ reset,
75
+ canGoBack,
76
+ }), [
77
+ state.currentScreen,
78
+ state.params,
79
+ navigate,
80
+ push,
81
+ replace,
82
+ goBack,
83
+ reset,
84
+ canGoBack,
85
+ ]);
86
+ return (_jsx(NavigationContext.Provider, { value: value, children: children }));
87
+ }
88
+ export function useNavigation() {
89
+ const context = React.useContext(NavigationContext);
90
+ if (!context) {
91
+ throw new Error("useNavigation must be used within NavigationProvider");
92
+ }
93
+ return context;
94
+ }
95
+ export function useNavigationStore(selector) {
96
+ const context = useNavigation();
97
+ if (!selector) {
98
+ return context;
99
+ }
100
+ return selector(context);
101
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Snapshot Store - Manages snapshot list state, pagination, and caching
3
+ */
4
+ import { create } from "zustand";
5
+ const MAX_CACHE_SIZE = 10;
6
+ export const useSnapshotStore = create((set, get) => ({
7
+ snapshots: [],
8
+ loading: false,
9
+ initialLoading: true,
10
+ error: null,
11
+ currentPage: 0,
12
+ pageSize: 10,
13
+ totalCount: 0,
14
+ hasMore: false,
15
+ pageCache: new Map(),
16
+ lastIdCache: new Map(),
17
+ devboxIdFilter: undefined,
18
+ selectedIndex: 0,
19
+ setSnapshots: (snapshots) => set({ snapshots }),
20
+ setLoading: (loading) => set({ loading }),
21
+ setInitialLoading: (loading) => set({ initialLoading: loading }),
22
+ setError: (error) => set({ error }),
23
+ setCurrentPage: (page) => set({ currentPage: page }),
24
+ setPageSize: (size) => set({ pageSize: size }),
25
+ setTotalCount: (count) => set({ totalCount: count }),
26
+ setHasMore: (hasMore) => set({ hasMore }),
27
+ setDevboxIdFilter: (devboxId) => set({ devboxIdFilter: devboxId }),
28
+ setSelectedIndex: (index) => set({ selectedIndex: index }),
29
+ cachePageData: (page, data, lastId) => {
30
+ const state = get();
31
+ const pageCache = state.pageCache;
32
+ const lastIdCache = state.lastIdCache;
33
+ // Aggressive LRU eviction
34
+ if (pageCache.size >= MAX_CACHE_SIZE) {
35
+ const oldestKey = pageCache.keys().next().value;
36
+ if (oldestKey !== undefined) {
37
+ pageCache.delete(oldestKey);
38
+ lastIdCache.delete(oldestKey);
39
+ }
40
+ }
41
+ // Create plain data objects to avoid SDK references
42
+ const plainData = data.map((s) => ({
43
+ id: s.id,
44
+ name: s.name,
45
+ devbox_id: s.devbox_id,
46
+ status: s.status,
47
+ create_time_ms: s.create_time_ms,
48
+ }));
49
+ pageCache.set(page, plainData);
50
+ lastIdCache.set(page, lastId);
51
+ set({});
52
+ },
53
+ getCachedPage: (page) => {
54
+ return get().pageCache.get(page);
55
+ },
56
+ clearCache: () => {
57
+ const state = get();
58
+ state.pageCache.clear();
59
+ state.lastIdCache.clear();
60
+ set({
61
+ pageCache: new Map(),
62
+ lastIdCache: new Map(),
63
+ });
64
+ },
65
+ clearAll: () => {
66
+ const state = get();
67
+ state.pageCache.clear();
68
+ state.lastIdCache.clear();
69
+ set({
70
+ snapshots: [],
71
+ loading: false,
72
+ initialLoading: true,
73
+ error: null,
74
+ currentPage: 0,
75
+ totalCount: 0,
76
+ hasMore: false,
77
+ pageCache: new Map(),
78
+ lastIdCache: new Map(),
79
+ devboxIdFilter: undefined,
80
+ selectedIndex: 0,
81
+ });
82
+ },
83
+ getSelectedSnapshot: () => {
84
+ const state = get();
85
+ return state.snapshots[state.selectedIndex];
86
+ },
87
+ }));