create-stylus-ide 1.0.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 (135) hide show
  1. package/Readme.MD +1515 -0
  2. package/cli.js +28 -0
  3. package/frontend/.vscode/settings.json +9 -0
  4. package/frontend/app/api/chat/route.ts +101 -0
  5. package/frontend/app/api/check-setup/route.ts +93 -0
  6. package/frontend/app/api/cleanup/route.ts +14 -0
  7. package/frontend/app/api/compile/route.ts +95 -0
  8. package/frontend/app/api/compile-stream/route.ts +98 -0
  9. package/frontend/app/api/complete/route.ts +86 -0
  10. package/frontend/app/api/deploy/route.ts +118 -0
  11. package/frontend/app/api/export-abi/route.ts +58 -0
  12. package/frontend/app/favicon.ico +0 -0
  13. package/frontend/app/globals.css +177 -0
  14. package/frontend/app/layout.tsx +29 -0
  15. package/frontend/app/ml/page.tsx +694 -0
  16. package/frontend/app/page.tsx +1132 -0
  17. package/frontend/app/providers.tsx +18 -0
  18. package/frontend/app/qlearning/page.tsx +188 -0
  19. package/frontend/app/raytracing/page.tsx +268 -0
  20. package/frontend/components/abi/ABIDialog.tsx +132 -0
  21. package/frontend/components/ai/AICompletionPopup.tsx +76 -0
  22. package/frontend/components/ai/ChatPanel.tsx +292 -0
  23. package/frontend/components/ai/QuickActions.tsx +128 -0
  24. package/frontend/components/blockchain/BlockchainContractBanner.tsx +64 -0
  25. package/frontend/components/blockchain/BlockchainLoadingDialog.tsx +188 -0
  26. package/frontend/components/deploy/DeployDialog.tsx +334 -0
  27. package/frontend/components/editor/FileTabs.tsx +181 -0
  28. package/frontend/components/editor/MonacoEditor.tsx +306 -0
  29. package/frontend/components/file-tree/ContextMenu.tsx +110 -0
  30. package/frontend/components/file-tree/DeleteConfirmDialog.tsx +61 -0
  31. package/frontend/components/file-tree/FileInputDialog.tsx +97 -0
  32. package/frontend/components/file-tree/FileNode.tsx +60 -0
  33. package/frontend/components/file-tree/FileTree.tsx +259 -0
  34. package/frontend/components/file-tree/FileTreeSkeleton.tsx +26 -0
  35. package/frontend/components/file-tree/FolderNode.tsx +105 -0
  36. package/frontend/components/github/GitHubLoadingDialog.tsx +201 -0
  37. package/frontend/components/github/GitHubMetadataBanner.tsx +61 -0
  38. package/frontend/components/github/LoadFromGitHubDialog.tsx +125 -0
  39. package/frontend/components/github/URLCopyButton.tsx +60 -0
  40. package/frontend/components/interact/ContractInteraction.tsx +323 -0
  41. package/frontend/components/interact/ContractPlaceholder.tsx +41 -0
  42. package/frontend/components/orbit/BenchmarkDialog.tsx +342 -0
  43. package/frontend/components/orbit/OrbitExplorer.tsx +273 -0
  44. package/frontend/components/project/ProjectActions.tsx +176 -0
  45. package/frontend/components/q-learning/ContractConfig.tsx +172 -0
  46. package/frontend/components/q-learning/MazeGrid.tsx +346 -0
  47. package/frontend/components/q-learning/PathAnimation.tsx +384 -0
  48. package/frontend/components/q-learning/QTableHeatmap.tsx +300 -0
  49. package/frontend/components/q-learning/TrainingForm.tsx +349 -0
  50. package/frontend/components/ray-tracing/ContractConfig.tsx +245 -0
  51. package/frontend/components/ray-tracing/MintingForm.tsx +280 -0
  52. package/frontend/components/ray-tracing/RenderCanvas.tsx +228 -0
  53. package/frontend/components/ray-tracing/RenderingPanel.tsx +259 -0
  54. package/frontend/components/ray-tracing/StyleControls.tsx +217 -0
  55. package/frontend/components/setup/SetupGuide.tsx +290 -0
  56. package/frontend/components/ui/KeyboardShortcutHint.tsx +74 -0
  57. package/frontend/components/ui/alert-dialog.tsx +157 -0
  58. package/frontend/components/ui/alert.tsx +66 -0
  59. package/frontend/components/ui/badge.tsx +46 -0
  60. package/frontend/components/ui/button.tsx +62 -0
  61. package/frontend/components/ui/card.tsx +92 -0
  62. package/frontend/components/ui/context-menu.tsx +252 -0
  63. package/frontend/components/ui/dialog.tsx +143 -0
  64. package/frontend/components/ui/dropdown-menu.tsx +257 -0
  65. package/frontend/components/ui/input.tsx +21 -0
  66. package/frontend/components/ui/label.tsx +24 -0
  67. package/frontend/components/ui/progress.tsx +31 -0
  68. package/frontend/components/ui/scroll-area.tsx +58 -0
  69. package/frontend/components/ui/select.tsx +190 -0
  70. package/frontend/components/ui/separator.tsx +28 -0
  71. package/frontend/components/ui/sheet.tsx +139 -0
  72. package/frontend/components/ui/skeleton.tsx +13 -0
  73. package/frontend/components/ui/slider.tsx +63 -0
  74. package/frontend/components/ui/sonner.tsx +40 -0
  75. package/frontend/components/ui/tabs.tsx +66 -0
  76. package/frontend/components/ui/textarea.tsx +18 -0
  77. package/frontend/components/wallet/ConnectButton.tsx +167 -0
  78. package/frontend/components/wallet/FaucetButton.tsx +256 -0
  79. package/frontend/components.json +22 -0
  80. package/frontend/eslint.config.mjs +18 -0
  81. package/frontend/hooks/useAICompletion.ts +75 -0
  82. package/frontend/hooks/useBlockchainLoader.ts +58 -0
  83. package/frontend/hooks/useChats.ts +137 -0
  84. package/frontend/hooks/useCompilation.ts +173 -0
  85. package/frontend/hooks/useFileTabs.ts +178 -0
  86. package/frontend/hooks/useGitHubLoader.ts +50 -0
  87. package/frontend/hooks/useKeyboardShortcuts.ts +47 -0
  88. package/frontend/hooks/usePanelState.ts +115 -0
  89. package/frontend/hooks/useProjectState.ts +276 -0
  90. package/frontend/hooks/useResponsive.ts +29 -0
  91. package/frontend/lib/abi-parser.ts +58 -0
  92. package/frontend/lib/blockchain-api.ts +374 -0
  93. package/frontend/lib/blockchain-explorers.ts +75 -0
  94. package/frontend/lib/blockchain-loader.ts +112 -0
  95. package/frontend/lib/cargo-template.ts +64 -0
  96. package/frontend/lib/compilation.ts +529 -0
  97. package/frontend/lib/constants.ts +31 -0
  98. package/frontend/lib/deployment.ts +176 -0
  99. package/frontend/lib/file-utils.ts +83 -0
  100. package/frontend/lib/github-api.ts +246 -0
  101. package/frontend/lib/github-loader.ts +369 -0
  102. package/frontend/lib/ml-contract-template.txt +900 -0
  103. package/frontend/lib/orbit-chains.ts +181 -0
  104. package/frontend/lib/output-formatter.ts +68 -0
  105. package/frontend/lib/project-manager.ts +632 -0
  106. package/frontend/lib/ray-tracing-abi.ts +206 -0
  107. package/frontend/lib/storage.ts +189 -0
  108. package/frontend/lib/templates.ts +1662 -0
  109. package/frontend/lib/url-parser.ts +188 -0
  110. package/frontend/lib/utils.ts +6 -0
  111. package/frontend/lib/wagmi-config.ts +24 -0
  112. package/frontend/next.config.ts +7 -0
  113. package/frontend/package-lock.json +16259 -0
  114. package/frontend/package.json +60 -0
  115. package/frontend/postcss.config.mjs +7 -0
  116. package/frontend/public/file.svg +1 -0
  117. package/frontend/public/globe.svg +1 -0
  118. package/frontend/public/ml-weights/.gitkeep +0 -0
  119. package/frontend/public/ml-weights/model.pkl +0 -0
  120. package/frontend/public/ml-weights/model_weights.json +27102 -0
  121. package/frontend/public/ml-weights/test_samples.json +7888 -0
  122. package/frontend/public/next.svg +1 -0
  123. package/frontend/public/vercel.svg +1 -0
  124. package/frontend/public/window.svg +1 -0
  125. package/frontend/scripts/check-env.js +52 -0
  126. package/frontend/scripts/setup.js +285 -0
  127. package/frontend/tailwind.config.ts +64 -0
  128. package/frontend/tsconfig.json +34 -0
  129. package/frontend/types/blockchain.ts +63 -0
  130. package/frontend/types/github.ts +54 -0
  131. package/frontend/types/project.ts +106 -0
  132. package/ml-training/README.md +56 -0
  133. package/ml-training/train_tiny_model.py +325 -0
  134. package/ml-training/update_template.py +59 -0
  135. package/package.json +30 -0
@@ -0,0 +1,632 @@
1
+ /**
2
+ * Project Manager
3
+ * Core logic for managing multi-file projects
4
+ */
5
+
6
+ import { v4 as uuidv4 } from "uuid";
7
+ import {
8
+ ProjectFile,
9
+ FileNode,
10
+ ProjectState,
11
+ CreateFileOptions,
12
+ CreateFolderOptions,
13
+ MoveFileOptions,
14
+ RenameOptions,
15
+ FileLanguage,
16
+ } from "@/types/project";
17
+
18
+ /**
19
+ * Create a new empty project
20
+ */
21
+ export function createProject(
22
+ name: string,
23
+ source: "local" | "github" = "local"
24
+ ): ProjectState {
25
+ const now = new Date();
26
+
27
+ return {
28
+ id: uuidv4(),
29
+ name,
30
+ type: "stylus-contract", // ✅ ADD THIS LINE
31
+ files: [
32
+ // Default starter files
33
+ createDefaultFile("src/lib.rs", "rust", getDefaultRustContent()),
34
+ createDefaultFile("Cargo.toml", "toml", getDefaultCargoToml(name)),
35
+ ],
36
+ structure: [
37
+ {
38
+ id: uuidv4(),
39
+ name: "src",
40
+ path: "src",
41
+ type: "folder",
42
+ expanded: true,
43
+ children: [
44
+ {
45
+ id: uuidv4(),
46
+ name: "lib.rs",
47
+ path: "src/lib.rs",
48
+ type: "file",
49
+ },
50
+ ],
51
+ },
52
+ {
53
+ id: uuidv4(),
54
+ name: "Cargo.toml",
55
+ path: "Cargo.toml",
56
+ type: "file",
57
+ },
58
+ ],
59
+ activeFilePath: "src/lib.rs",
60
+ source: source,
61
+ createdAt: now,
62
+ updatedAt: now,
63
+ metadata: {
64
+ source,
65
+ },
66
+ };
67
+ }
68
+
69
+ /**
70
+ * Add a new file to the project
71
+ */
72
+ export function addFile(
73
+ project: ProjectState,
74
+ options: CreateFileOptions
75
+ ): ProjectState {
76
+ const { path, content = "", language } = options;
77
+
78
+ // Check if file already exists
79
+ if (project.files.find((f) => f.path === path)) {
80
+ throw new Error(`File already exists: ${path}`);
81
+ }
82
+
83
+ const fileName = path.split("/").pop() || path;
84
+ const detectedLanguage = language || detectLanguage(fileName);
85
+
86
+ const newFile: ProjectFile = {
87
+ id: uuidv4(),
88
+ path,
89
+ name: fileName,
90
+ content,
91
+ language: detectedLanguage,
92
+ modified: false,
93
+ isOpen: false,
94
+ createdAt: new Date(),
95
+ updatedAt: new Date(),
96
+ };
97
+
98
+ // Add file to files array
99
+ const updatedFiles = [...project.files, newFile];
100
+
101
+ // Update file tree structure
102
+ const updatedStructure = insertFileIntoTree(project.structure, path);
103
+
104
+ return {
105
+ ...project,
106
+ files: updatedFiles,
107
+ structure: updatedStructure,
108
+ updatedAt: new Date(),
109
+ };
110
+ }
111
+
112
+ /**
113
+ * Add a new folder to the project
114
+ */
115
+ export function addFolder(
116
+ project: ProjectState,
117
+ options: CreateFolderOptions
118
+ ): ProjectState {
119
+ const { path } = options;
120
+
121
+ // Check if folder already exists
122
+ if (findNodeByPath(project.structure, path)) {
123
+ throw new Error(`Folder already exists: ${path}`);
124
+ }
125
+
126
+ const updatedStructure = insertFolderIntoTree(project.structure, path);
127
+
128
+ return {
129
+ ...project,
130
+ structure: updatedStructure,
131
+ updatedAt: new Date(),
132
+ };
133
+ }
134
+
135
+ /**
136
+ * Delete a file from the project
137
+ */
138
+ export function deleteFile(
139
+ project: ProjectState,
140
+ filePath: string
141
+ ): ProjectState {
142
+ // Remove from files array
143
+ const updatedFiles = project.files.filter((f) => f.path !== filePath);
144
+
145
+ // Remove from tree structure
146
+ const updatedStructure = removeNodeFromTree(project.structure, filePath);
147
+
148
+ // If active file was deleted, clear active file
149
+ const activeFilePath =
150
+ project.activeFilePath === filePath ? null : project.activeFilePath;
151
+
152
+ return {
153
+ ...project,
154
+ files: updatedFiles,
155
+ structure: updatedStructure,
156
+ activeFilePath,
157
+ updatedAt: new Date(),
158
+ };
159
+ }
160
+
161
+ /**
162
+ * Delete a folder and all its contents
163
+ */
164
+ export function deleteFolder(
165
+ project: ProjectState,
166
+ folderPath: string
167
+ ): ProjectState {
168
+ // Remove all files in folder
169
+ const updatedFiles = project.files.filter(
170
+ (f) => !f.path.startsWith(folderPath + "/")
171
+ );
172
+
173
+ // Remove folder from tree
174
+ const updatedStructure = removeNodeFromTree(project.structure, folderPath);
175
+
176
+ return {
177
+ ...project,
178
+ files: updatedFiles,
179
+ structure: updatedStructure,
180
+ updatedAt: new Date(),
181
+ };
182
+ }
183
+
184
+ /**
185
+ * Rename a file or folder
186
+ */
187
+ export function renameFile(
188
+ project: ProjectState,
189
+ options: RenameOptions
190
+ ): ProjectState {
191
+ const { oldPath, newPath } = options;
192
+
193
+ // Check if new path already exists
194
+ if (project.files.find((f) => f.path === newPath)) {
195
+ throw new Error(`File already exists: ${newPath}`);
196
+ }
197
+
198
+ // Update file in files array
199
+ const updatedFiles = project.files.map((file) => {
200
+ if (file.path === oldPath) {
201
+ const newName = newPath.split("/").pop() || newPath;
202
+ return {
203
+ ...file,
204
+ path: newPath,
205
+ name: newName,
206
+ updatedAt: new Date(),
207
+ };
208
+ }
209
+ return file;
210
+ });
211
+
212
+ // Update tree structure
213
+ const updatedStructure = renameNodeInTree(
214
+ project.structure,
215
+ oldPath,
216
+ newPath
217
+ );
218
+
219
+ // Update active file path if it was renamed
220
+ const activeFilePath =
221
+ project.activeFilePath === oldPath ? newPath : project.activeFilePath;
222
+
223
+ return {
224
+ ...project,
225
+ files: updatedFiles,
226
+ structure: updatedStructure,
227
+ activeFilePath,
228
+ updatedAt: new Date(),
229
+ };
230
+ }
231
+
232
+ /**
233
+ * Move a file to a different location
234
+ */
235
+ export function moveFile(
236
+ project: ProjectState,
237
+ options: MoveFileOptions
238
+ ): ProjectState {
239
+ const { fromPath, toPath } = options;
240
+
241
+ // This is similar to rename, but handles moving between folders
242
+ return renameFile(project, { oldPath: fromPath, newPath: toPath });
243
+ }
244
+
245
+ /**
246
+ * Get a file by its path
247
+ */
248
+ export function getFileByPath(
249
+ project: ProjectState,
250
+ path: string
251
+ ): ProjectFile | undefined {
252
+ return project.files.find((f) => f.path === path);
253
+ }
254
+
255
+ /**
256
+ * Update file content
257
+ */
258
+ export function updateFileContent(
259
+ project: ProjectState,
260
+ filePath: string,
261
+ content: string
262
+ ): ProjectState {
263
+ const updatedFiles = project.files.map((file) => {
264
+ if (file.path === filePath) {
265
+ return {
266
+ ...file,
267
+ content,
268
+ modified: true,
269
+ updatedAt: new Date(),
270
+ };
271
+ }
272
+ return file;
273
+ });
274
+
275
+ return {
276
+ ...project,
277
+ files: updatedFiles,
278
+ updatedAt: new Date(),
279
+ };
280
+ }
281
+
282
+ /**
283
+ * Mark file as saved (not modified)
284
+ */
285
+ export function markFileSaved(
286
+ project: ProjectState,
287
+ filePath: string
288
+ ): ProjectState {
289
+ const updatedFiles = project.files.map((file) => {
290
+ if (file.path === filePath) {
291
+ return {
292
+ ...file,
293
+ modified: false,
294
+ };
295
+ }
296
+ return file;
297
+ });
298
+
299
+ return {
300
+ ...project,
301
+ files: updatedFiles,
302
+ };
303
+ }
304
+
305
+ /**
306
+ * Set active file
307
+ */
308
+ export function setActiveFile(
309
+ project: ProjectState,
310
+ filePath: string | null
311
+ ): ProjectState {
312
+ return {
313
+ ...project,
314
+ activeFilePath: filePath,
315
+ };
316
+ }
317
+
318
+ /**
319
+ * Toggle file open state
320
+ */
321
+ export function toggleFileOpen(
322
+ project: ProjectState,
323
+ filePath: string,
324
+ isOpen: boolean
325
+ ): ProjectState {
326
+ const updatedFiles = project.files.map((file) => {
327
+ if (file.path === filePath) {
328
+ return {
329
+ ...file,
330
+ isOpen,
331
+ };
332
+ }
333
+ return file;
334
+ });
335
+
336
+ return {
337
+ ...project,
338
+ files: updatedFiles,
339
+ };
340
+ }
341
+
342
+ /**
343
+ * Build file tree structure from flat file list
344
+ */
345
+ export function buildFileTree(files: ProjectFile[]): FileNode[] {
346
+ const root: FileNode[] = [];
347
+
348
+ files.forEach((file) => {
349
+ const pathParts = file.path.split("/");
350
+ let currentLevel = root;
351
+
352
+ pathParts.forEach((part, index) => {
353
+ const isFile = index === pathParts.length - 1;
354
+ const currentPath = pathParts.slice(0, index + 1).join("/");
355
+
356
+ let existingNode = currentLevel.find((node) => node.name === part);
357
+
358
+ if (!existingNode) {
359
+ const newNode: FileNode = {
360
+ id: uuidv4(),
361
+ name: part,
362
+ path: currentPath,
363
+ type: isFile ? "file" : "folder",
364
+ ...(isFile ? {} : { children: [], expanded: false }),
365
+ };
366
+
367
+ currentLevel.push(newNode);
368
+ existingNode = newNode;
369
+ }
370
+
371
+ if (!isFile && existingNode.children) {
372
+ currentLevel = existingNode.children;
373
+ }
374
+ });
375
+ });
376
+
377
+ return sortTree(root);
378
+ }
379
+
380
+ // ============================================================================
381
+ // HELPER FUNCTIONS
382
+ // ============================================================================
383
+
384
+ function createDefaultFile(
385
+ path: string,
386
+ language: FileLanguage,
387
+ content: string
388
+ ): ProjectFile {
389
+ const name = path.split("/").pop() || path;
390
+ const now = new Date();
391
+
392
+ return {
393
+ id: uuidv4(),
394
+ path,
395
+ name,
396
+ content,
397
+ language,
398
+ modified: false,
399
+ isOpen: path === "src/lib.rs", // Open lib.rs by default
400
+ createdAt: now,
401
+ updatedAt: now,
402
+ };
403
+ }
404
+
405
+ function detectLanguage(fileName: string): FileLanguage {
406
+ if (fileName.endsWith(".rs")) return "rust";
407
+ if (fileName.endsWith(".toml")) return "toml";
408
+ if (fileName.endsWith(".md")) return "markdown";
409
+ if (fileName === ".gitignore") return "gitignore";
410
+ return "text";
411
+ }
412
+
413
+ function getDefaultRustContent(): string {
414
+ return `// Welcome to Stylus IDE - Multi-File Project
415
+
416
+ #![cfg_attr(not(feature = "export-abi"), no_main)]
417
+ extern crate alloc;
418
+
419
+ use stylus_sdk::prelude::*;
420
+ use stylus_sdk::alloy_primitives::U256;
421
+
422
+ sol_storage! {
423
+ #[entrypoint]
424
+ pub struct Counter {
425
+ uint256 count;
426
+ }
427
+ }
428
+
429
+ #[public]
430
+ impl Counter {
431
+ pub fn get(&self) -> U256 {
432
+ self.count.get()
433
+ }
434
+
435
+ pub fn increment(&mut self) {
436
+ let count = self.count.get();
437
+ self.count.set(count + U256::from(1));
438
+ }
439
+ }
440
+ `;
441
+ }
442
+
443
+ function getDefaultCargoToml(projectName: string): string {
444
+ return `[package]
445
+ name = "${projectName.toLowerCase().replace(/[^a-z0-9_-]/g, "_")}"
446
+ version = "0.1.0"
447
+ edition = "2021"
448
+
449
+ [dependencies]
450
+ alloy-primitives = "=0.7.6"
451
+ alloy-sol-types = "=0.7.6"
452
+ stylus-sdk = "0.6.0"
453
+
454
+ [dev-dependencies]
455
+ tokio = { version = "1.12.0", features = ["full"] }
456
+ ethers = "2.0"
457
+ eyre = "0.6.8"
458
+
459
+ [features]
460
+ export-abi = ["stylus-sdk/export-abi"]
461
+
462
+ [lib]
463
+ crate-type = ["lib", "cdylib"]
464
+
465
+ [profile.release]
466
+ codegen-units = 1
467
+ strip = true
468
+ lto = true
469
+ panic = "abort"
470
+ opt-level = "s"
471
+ `;
472
+ }
473
+
474
+ function insertFileIntoTree(tree: FileNode[], filePath: string): FileNode[] {
475
+ const pathParts = filePath.split("/");
476
+ const fileName = pathParts.pop()!;
477
+
478
+ if (pathParts.length === 0) {
479
+ // Root level file
480
+ return [
481
+ ...tree,
482
+ {
483
+ id: uuidv4(),
484
+ name: fileName,
485
+ path: filePath,
486
+ type: "file",
487
+ },
488
+ ];
489
+ }
490
+
491
+ // Nested file - find or create folders
492
+ return tree.map((node) => {
493
+ if (node.name === pathParts[0] && node.type === "folder") {
494
+ const remainingPath = pathParts.slice(1).join("/") + "/" + fileName;
495
+ return {
496
+ ...node,
497
+ children: insertFileIntoTree(node.children || [], remainingPath),
498
+ };
499
+ }
500
+ return node;
501
+ });
502
+ }
503
+
504
+ function insertFolderIntoTree(
505
+ tree: FileNode[],
506
+ folderPath: string
507
+ ): FileNode[] {
508
+ const pathParts = folderPath.split("/");
509
+ const folderName = pathParts[0];
510
+
511
+ const existingFolder = tree.find((node) => node.name === folderName);
512
+
513
+ if (pathParts.length === 1) {
514
+ if (existingFolder) {
515
+ return tree;
516
+ }
517
+ return [
518
+ ...tree,
519
+ {
520
+ id: uuidv4(),
521
+ name: folderName,
522
+ path: folderPath,
523
+ type: "folder",
524
+ children: [],
525
+ expanded: false,
526
+ },
527
+ ];
528
+ }
529
+
530
+ // Nested folder
531
+ const remainingPath = pathParts.slice(1).join("/");
532
+
533
+ if (existingFolder && existingFolder.type === "folder") {
534
+ return tree.map((node) => {
535
+ if (node.name === folderName) {
536
+ return {
537
+ ...node,
538
+ children: insertFolderIntoTree(node.children || [], remainingPath),
539
+ };
540
+ }
541
+ return node;
542
+ });
543
+ }
544
+
545
+ // Create parent folder if doesn't exist
546
+ return [
547
+ ...tree,
548
+ {
549
+ id: uuidv4(),
550
+ name: folderName,
551
+ path: pathParts[0],
552
+ type: "folder",
553
+ children: insertFolderIntoTree([], remainingPath),
554
+ expanded: false,
555
+ },
556
+ ];
557
+ }
558
+
559
+ function removeNodeFromTree(tree: FileNode[], path: string): FileNode[] {
560
+ return tree
561
+ .filter((node) => node.path !== path)
562
+ .map((node) => {
563
+ if (node.type === "folder" && node.children) {
564
+ return {
565
+ ...node,
566
+ children: removeNodeFromTree(node.children, path),
567
+ };
568
+ }
569
+ return node;
570
+ });
571
+ }
572
+
573
+ function renameNodeInTree(
574
+ tree: FileNode[],
575
+ oldPath: string,
576
+ newPath: string
577
+ ): FileNode[] {
578
+ return tree.map((node) => {
579
+ if (node.path === oldPath) {
580
+ const newName = newPath.split("/").pop()!;
581
+ return {
582
+ ...node,
583
+ name: newName,
584
+ path: newPath,
585
+ };
586
+ }
587
+
588
+ if (node.type === "folder" && node.children) {
589
+ return {
590
+ ...node,
591
+ children: renameNodeInTree(node.children, oldPath, newPath),
592
+ };
593
+ }
594
+
595
+ return node;
596
+ });
597
+ }
598
+
599
+ function findNodeByPath(tree: FileNode[], path: string): FileNode | undefined {
600
+ for (const node of tree) {
601
+ if (node.path === path) {
602
+ return node;
603
+ }
604
+
605
+ if (node.type === "folder" && node.children) {
606
+ const found = findNodeByPath(node.children, path);
607
+ if (found) return found;
608
+ }
609
+ }
610
+
611
+ return undefined;
612
+ }
613
+
614
+ function sortTree(tree: FileNode[]): FileNode[] {
615
+ return tree
616
+ .sort((a, b) => {
617
+ // Folders first
618
+ if (a.type === "folder" && b.type === "file") return -1;
619
+ if (a.type === "file" && b.type === "folder") return 1;
620
+ // Then alphabetically
621
+ return a.name.localeCompare(b.name);
622
+ })
623
+ .map((node) => {
624
+ if (node.type === "folder" && node.children) {
625
+ return {
626
+ ...node,
627
+ children: sortTree(node.children),
628
+ };
629
+ }
630
+ return node;
631
+ });
632
+ }