vzcode 2.3.0 → 2.4.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 (196) hide show
  1. package/package.json +3 -2
  2. package/src/cli.ts +208 -0
  3. package/src/client/AIAssist/AIAssistCodeMirrorKeyMap.ts +42 -0
  4. package/src/client/AIAssist/AIAssistWidget/index.tsx +135 -0
  5. package/src/client/AIAssist/AIAssistWidget/style.scss +47 -0
  6. package/src/client/AIAssist/Spinner.jsx +76 -0
  7. package/src/client/AIAssist/formatFile.ts +27 -0
  8. package/src/client/AIAssist/generateFilesContext.ts +25 -0
  9. package/src/client/AIAssist/index.ts +2 -0
  10. package/src/client/AIAssist/startAIAssist.ts +95 -0
  11. package/src/client/App/index.tsx +152 -0
  12. package/src/client/App/style.scss +82 -0
  13. package/src/client/App/usePending.ts +51 -0
  14. package/src/client/App/useShareDB.ts +161 -0
  15. package/src/client/CodeEditor/Copilot/index.ts +74 -0
  16. package/src/client/CodeEditor/InteractiveWidgets/boolToggler.ts +12 -0
  17. package/src/client/CodeEditor/InteractiveWidgets/colorPicker.ts +68 -0
  18. package/src/client/CodeEditor/InteractiveWidgets/colorsInTextPlugin.ts +174 -0
  19. package/src/client/CodeEditor/InteractiveWidgets/highlightWidgets.ts +79 -0
  20. package/src/client/CodeEditor/InteractiveWidgets/index.ts +44 -0
  21. package/src/client/CodeEditor/InteractiveWidgets/markdownCheckboxToggler.ts +12 -0
  22. package/src/client/CodeEditor/InteractiveWidgets/numberDragger.ts +20 -0
  23. package/src/client/CodeEditor/InteractiveWidgets/rotateWidget.ts +143 -0
  24. package/src/client/CodeEditor/InteractiveWidgets/urlClicker.test.ts +48 -0
  25. package/src/client/CodeEditor/InteractiveWidgets/urlClicker.ts +12 -0
  26. package/src/client/CodeEditor/InteractiveWidgets/vec2Slider.ts +25 -0
  27. package/src/client/CodeEditor/basicSetup.ts +73 -0
  28. package/src/client/CodeEditor/getOrCreateEditor.tsx +615 -0
  29. package/src/client/CodeEditor/index.tsx +202 -0
  30. package/src/client/CodeEditor/json1PresenceBroadcast.ts +41 -0
  31. package/src/client/CodeEditor/json1PresenceDisplay.ts +350 -0
  32. package/src/client/CodeEditor/rainbowBrackets.ts +122 -0
  33. package/src/client/CodeEditor/style.scss +156 -0
  34. package/src/client/CodeEditor/typescriptExtension/worker.ts +32 -0
  35. package/src/client/CodeErrorOverlay/index.tsx +77 -0
  36. package/src/client/CodeErrorOverlay/style.scss +33 -0
  37. package/src/client/Fonts/fonts.ts +8 -0
  38. package/src/client/Icons/AdjustmentSVG.tsx +19 -0
  39. package/src/client/Icons/BugSVG.tsx +20 -0
  40. package/src/client/Icons/CloseSVG.tsx +19 -0
  41. package/src/client/Icons/CssSVG.tsx +16 -0
  42. package/src/client/Icons/DirectoryArrowSVG.tsx +19 -0
  43. package/src/client/Icons/EditSVG.tsx +19 -0
  44. package/src/client/Icons/FileSVG.tsx +19 -0
  45. package/src/client/Icons/FolderSVG.tsx +20 -0
  46. package/src/client/Icons/GearSVG.tsx +16 -0
  47. package/src/client/Icons/HtmlSVG.tsx +16 -0
  48. package/src/client/Icons/JavaScriptSVG.tsx +16 -0
  49. package/src/client/Icons/JsonSVG.tsx +16 -0
  50. package/src/client/Icons/MarkdownSVG.tsx +16 -0
  51. package/src/client/Icons/MicOffSVG.tsx +35 -0
  52. package/src/client/Icons/MicSVG.tsx +25 -0
  53. package/src/client/Icons/NewSVG.tsx +19 -0
  54. package/src/client/Icons/PinSVG.tsx +14 -0
  55. package/src/client/Icons/PlaySVG.tsx +16 -0
  56. package/src/client/Icons/QuestionMarkSVG.tsx +18 -0
  57. package/src/client/Icons/ReactSVG.tsx +16 -0
  58. package/src/client/Icons/SearchSVG.tsx +20 -0
  59. package/src/client/Icons/SparklesSVG.tsx +23 -0
  60. package/src/client/Icons/SplitEditorSVG.tsx +34 -0
  61. package/src/client/Icons/StopSVG.tsx +15 -0
  62. package/src/client/Icons/SvelteSVG.tsx +16 -0
  63. package/src/client/Icons/TrashSVG.tsx +19 -0
  64. package/src/client/Icons/TypeScriptSVG.tsx +16 -0
  65. package/src/client/Icons/index.tsx +26 -0
  66. package/src/client/ImageViewer/index.tsx +91 -0
  67. package/src/client/ImageViewer/style.scss +41 -0
  68. package/src/client/PresenceNotifications/PresenceNotifications.test.ts +134 -0
  69. package/src/client/PresenceNotifications/index.tsx +158 -0
  70. package/src/client/PresenceNotifications/style.scss +17 -0
  71. package/src/client/RunCodeWidget/index.tsx +117 -0
  72. package/src/client/RunCodeWidget/style.scss +24 -0
  73. package/src/client/SplitPaneResizeContext.tsx +159 -0
  74. package/src/client/TabList/Tab.tsx +73 -0
  75. package/src/client/TabList/index.tsx +35 -0
  76. package/src/client/TabList/style.scss +51 -0
  77. package/src/client/VZCodeContext/index.tsx +29 -0
  78. package/src/client/VZCodeContext/types.ts +248 -0
  79. package/src/client/VZCodeContext/useVZCodeState.ts +602 -0
  80. package/src/client/VZKeyboardShortcutsDoc.tsx +148 -0
  81. package/src/client/VZLeft.tsx +28 -0
  82. package/src/client/VZMiddle.tsx +172 -0
  83. package/src/client/VZResizer/index.tsx +128 -0
  84. package/src/client/VZResizer/styles.scss +33 -0
  85. package/src/client/VZRight.tsx +113 -0
  86. package/src/client/VZSettings.tsx +275 -0
  87. package/src/client/VZSidebar/AIChat/ChatInput.tsx +243 -0
  88. package/src/client/VZSidebar/AIChat/DiffView.scss +136 -0
  89. package/src/client/VZSidebar/AIChat/DiffView.tsx +176 -0
  90. package/src/client/VZSidebar/AIChat/Message.tsx +79 -0
  91. package/src/client/VZSidebar/AIChat/MessageList.tsx +190 -0
  92. package/src/client/VZSidebar/AIChat/StreamingMessage.tsx +35 -0
  93. package/src/client/VZSidebar/AIChat/ThinkingScratchpad.tsx +156 -0
  94. package/src/client/VZSidebar/AIChat/TypingIndicator.tsx +19 -0
  95. package/src/client/VZSidebar/AIChat/index.tsx +357 -0
  96. package/src/client/VZSidebar/AIChat/styles.scss +646 -0
  97. package/src/client/VZSidebar/AIChat/useSpeechRecognition.ts +117 -0
  98. package/src/client/VZSidebar/CreateDirModal.tsx +111 -0
  99. package/src/client/VZSidebar/CreateFileModal.tsx +111 -0
  100. package/src/client/VZSidebar/DeleteConfirmationModal.tsx +76 -0
  101. package/src/client/VZSidebar/DirectoryListing.tsx +89 -0
  102. package/src/client/VZSidebar/EmptyState.tsx +11 -0
  103. package/src/client/VZSidebar/FileListing.tsx +91 -0
  104. package/src/client/VZSidebar/FileTypeIcon.tsx +49 -0
  105. package/src/client/VZSidebar/Item.tsx +237 -0
  106. package/src/client/VZSidebar/Listing.tsx +47 -0
  107. package/src/client/VZSidebar/Search.tsx +527 -0
  108. package/src/client/VZSidebar/VisualEditor.tsx +282 -0
  109. package/src/client/VZSidebar/VoiceChatModal.tsx +121 -0
  110. package/src/client/VZSidebar/aiCopyPaste.ts +199 -0
  111. package/src/client/VZSidebar/index.tsx +728 -0
  112. package/src/client/VZSidebar/styles.scss +612 -0
  113. package/src/client/VZSidebar/useDragAndDrop.tsx +408 -0
  114. package/src/client/bootstrap.ts +26 -0
  115. package/src/client/buildWorker.ts +3 -0
  116. package/src/client/featureFlags.ts +8 -0
  117. package/src/client/generateRequestId.ts +6 -0
  118. package/src/client/getFileTree.ts +53 -0
  119. package/src/client/index.ts +19 -0
  120. package/src/client/main.jsx +10 -0
  121. package/src/client/presenceColor.ts +8 -0
  122. package/src/client/shouldTriggerRun.ts +31 -0
  123. package/src/client/sortFileTree.ts +21 -0
  124. package/src/client/tabsSearchParameters.test.ts +79 -0
  125. package/src/client/tabsSearchParameters.ts +137 -0
  126. package/src/client/themes/index.ts +7 -0
  127. package/src/client/themes/themes.ts +77 -0
  128. package/src/client/themes/uiwImports.ts +32 -0
  129. package/src/client/themes/useDynamicTheme.ts +29 -0
  130. package/src/client/themes/vizhubTheme/colors.ts +80 -0
  131. package/src/client/themes/vizhubTheme/index.ts +193 -0
  132. package/src/client/useActions.ts +288 -0
  133. package/src/client/useESLint/index.ts +72 -0
  134. package/src/client/useESLint/worker.ts +113 -0
  135. package/src/client/useEditorCache.ts +35 -0
  136. package/src/client/useFileCRUD.ts +228 -0
  137. package/src/client/useKeyboardShortcuts.ts +451 -0
  138. package/src/client/useOpenDirectories.ts +99 -0
  139. package/src/client/usePresenceAutoFollow.ts +85 -0
  140. package/src/client/usePrettier/index.ts +178 -0
  141. package/src/client/usePrettier/worker.ts +109 -0
  142. package/src/client/useRunCode.tsx +15 -0
  143. package/src/client/useRuntimeError.ts +32 -0
  144. package/src/client/useSubmitOperation.ts +15 -0
  145. package/src/client/useURLSync.ts +137 -0
  146. package/src/client/usernameLocalStorage.ts +43 -0
  147. package/src/client/utils/fileExtension.ts +10 -0
  148. package/src/client/utils/isImageFile.ts +12 -0
  149. package/src/client/vzReducer/aiChatReducer.ts +44 -0
  150. package/src/client/vzReducer/closeTabReducer.test.ts +205 -0
  151. package/src/client/vzReducer/closeTabsReducer.ts +96 -0
  152. package/src/client/vzReducer/createInitialState.ts +38 -0
  153. package/src/client/vzReducer/editorNoLongerWantsFocusReducer.ts +9 -0
  154. package/src/client/vzReducer/findPane.ts +19 -0
  155. package/src/client/vzReducer/index.ts +250 -0
  156. package/src/client/vzReducer/openTabReducer.test.ts +127 -0
  157. package/src/client/vzReducer/openTabReducer.ts +74 -0
  158. package/src/client/vzReducer/searchReducer.test.ts +292 -0
  159. package/src/client/vzReducer/searchReducer.ts +181 -0
  160. package/src/client/vzReducer/setActiveFileIdReducer.ts +28 -0
  161. package/src/client/vzReducer/setActiveFileLeftRightReducer.ts +76 -0
  162. package/src/client/vzReducer/setIsDocOpenReducer.ts +9 -0
  163. package/src/client/vzReducer/setIsSettingsOpenReducer.ts +9 -0
  164. package/src/client/vzReducer/setThemeReducer.ts +9 -0
  165. package/src/client/vzReducer/setUsernameReducer.ts +9 -0
  166. package/src/client/vzReducer/splitCurrentPaneReducer.ts +66 -0
  167. package/src/client/vzReducer/toggleAutoFollowReducer.ts +12 -0
  168. package/src/client/vzReducer/updatePane.ts +40 -0
  169. package/src/client/vzReducer/updatePresenceIndicatorReducer.ts +30 -0
  170. package/src/client/vzReducer/visualEditorReducer.ts +9 -0
  171. package/src/ot.ts +24 -0
  172. package/src/randomId.ts +15 -0
  173. package/src/reactRouterExports.ts +15 -0
  174. package/src/runCode.ts +27 -0
  175. package/src/server/aiChatHandler/aiEditing.ts +151 -0
  176. package/src/server/aiChatHandler/chatOperations.ts +476 -0
  177. package/src/server/aiChatHandler/errorHandling.ts +66 -0
  178. package/src/server/aiChatHandler/index.ts +199 -0
  179. package/src/server/aiChatHandler/llmStreaming.ts +419 -0
  180. package/src/server/aiChatHandler/validation.ts +24 -0
  181. package/src/server/computeInitialDocument.ts +220 -0
  182. package/src/server/config.ts +24 -0
  183. package/src/server/featureFlags.ts +8 -0
  184. package/src/server/generateAIResponse.ts +188 -0
  185. package/src/server/handleAIAssist.ts +53 -0
  186. package/src/server/handleAIChatMessage.ts +2 -0
  187. package/src/server/handleAICopilot.ts +97 -0
  188. package/src/server/index.ts +379 -0
  189. package/src/server/isDirectory.ts +1 -0
  190. package/src/server/livekit.ts +24 -0
  191. package/src/server/prettier.ts +115 -0
  192. package/src/server/setupEnv.ts +8 -0
  193. package/src/submitOperation.ts +25 -0
  194. package/src/types.ts +205 -0
  195. package/src/utils/fileDiff.ts +117 -0
  196. package/src/vite-env.d.ts +10 -0
@@ -0,0 +1,379 @@
1
+ #!/usr/bin/env node
2
+ import WebSocketJSONStream from '@teamwork/websocket-json-stream';
3
+ import bodyParser from 'body-parser';
4
+ import express from 'express';
5
+ import fs from 'fs';
6
+ import http from 'http';
7
+ import open from 'open';
8
+ import path from 'path';
9
+ import ShareDB from 'sharedb';
10
+ import { fileURLToPath } from 'url';
11
+ import { WebSocketServer } from 'ws';
12
+ import { json1Presence } from '../ot.js';
13
+ import { computeInitialDocument } from './computeInitialDocument.js';
14
+ import { handleAIAssist } from './handleAIAssist.js';
15
+ import { handleAICopilot } from './handleAICopilot.js';
16
+ import { handleAIChatMessage } from './handleAIChatMessage.js';
17
+
18
+ import { isDirectory } from './isDirectory.js';
19
+ import { createToken } from './livekit.js';
20
+ import './setupEnv.js';
21
+
22
+ // Import the image file utility
23
+ const isImageFile = (fileName) => {
24
+ return (
25
+ fileName.match(
26
+ /\.(png|jpg|jpeg|gif|bmp|svg|webp)$/i,
27
+ ) !== null
28
+ );
29
+ };
30
+
31
+ // The time in milliseconds by which auto-saving is debounced.
32
+ const autoSaveDebounceTimeMS = 800;
33
+
34
+ // The time in milliseconds by which auto-saving is throttled
35
+ // when the user is interacting with the widgets in the editor.
36
+ const throttleTimeMS = 100;
37
+
38
+ // Helper function to get the port from command line arguments or use default.
39
+ function getPortFromArgs(defaultPort = 3030) {
40
+ const portArg = process.argv.find((arg) =>
41
+ arg.startsWith('--port='),
42
+ );
43
+ if (portArg) {
44
+ return parseInt(portArg.split('=')[1], 10);
45
+ }
46
+ return defaultPort;
47
+ }
48
+
49
+ // Server port.
50
+ const port = getPortFromArgs();
51
+
52
+ const initialDocument = computeInitialDocument({
53
+ // Use the current working directory to look for files.
54
+ fullPath: process.cwd(),
55
+ });
56
+
57
+ // Register our custom OT type,
58
+ // because it does not ship with ShareDB.
59
+ ShareDB.types.register(json1Presence.type);
60
+
61
+ const app = express();
62
+
63
+ // Use ShareDB over WebSocket
64
+ const shareDBBackend = new ShareDB({
65
+ // Enable presence
66
+ // See https://github.com/share/sharedb/blob/master/examples/rich-text-presence/server.js#L9
67
+ presence: true,
68
+ doNotForwardSendPresenceErrorsToClient: true,
69
+ });
70
+ const server = http.createServer(app);
71
+ const wss = new WebSocketServer({ server });
72
+ wss.on('connection', (ws) => {
73
+ const clientStream = new WebSocketJSONStream(ws);
74
+ shareDBBackend.listen(clientStream);
75
+
76
+ // Prevent server crashes on errors.
77
+ clientStream.on('error', (error) => {
78
+ console.log('clientStream error: ' + error.message);
79
+ });
80
+
81
+ // Handle errors
82
+ ws.on('error', (error) => {
83
+ console.log('ws error: ' + error.message);
84
+ });
85
+
86
+ // Handle disconnections
87
+ ws.on('close', () => {
88
+ clientStream.end();
89
+ });
90
+ });
91
+
92
+ // Serve static files
93
+ const filename = fileURLToPath(import.meta.url);
94
+ const dirname = path.dirname(filename);
95
+ const dir = path.join(dirname, '..', '..', 'dist');
96
+ app.use(express.static(dir));
97
+
98
+ // Create the initial "document",
99
+ // which is a representation of files on disk.
100
+ const shareDBConnection = shareDBBackend.connect();
101
+ const shareDBDoc = shareDBConnection.get('documents', '1');
102
+
103
+ // Set up presence for AI editing following the same pattern as useShareDB.ts
104
+ const docPresence = shareDBConnection.getDocPresence(
105
+ 'documents',
106
+ '1',
107
+ );
108
+
109
+ // Create local presence for AI editing with a unique ID
110
+ const generateAIEditId = () => {
111
+ const timestamp = Date.now().toString(36);
112
+ return `ai-edit-${timestamp}`;
113
+ };
114
+
115
+ const createAIEditLocalPresence = () =>
116
+ docPresence.create(generateAIEditId());
117
+
118
+ shareDBDoc.create(initialDocument, json1Presence.type.uri);
119
+
120
+ // Handle AI Assist requests.
121
+ app.post(
122
+ '/ai-assist',
123
+ bodyParser.json(),
124
+ handleAIAssist(shareDBDoc),
125
+ );
126
+
127
+ // Handle AI Copilot requests.
128
+ app.post(
129
+ '/ai-copilot',
130
+ bodyParser.json(),
131
+ handleAICopilot(),
132
+ );
133
+
134
+ // Handle AI Chat Message requests.
135
+ app.post(
136
+ '/ai-chat-message',
137
+ bodyParser.json(),
138
+ handleAIChatMessage({
139
+ shareDBDoc,
140
+ createAIEditLocalPresence,
141
+ onCreditDeduction: undefined,
142
+ }),
143
+ );
144
+
145
+ // Livekit Token Generator
146
+ app.get('/livekit-token', async (req, res) => {
147
+ const { room, username } = req.query;
148
+ res.send(await createToken(room, username));
149
+ });
150
+
151
+ // The state of the document when files were last auto-saved.
152
+ let previousDocument = initialDocument;
153
+
154
+ // Saves the files that changed.
155
+ const save = () => {
156
+ const currentDocument = shareDBDoc.data;
157
+
158
+ // Take a look at each file (key) previously and currently.
159
+ const allKeys = Object.keys({
160
+ ...currentDocument.files,
161
+ ...previousDocument.files,
162
+ });
163
+
164
+ const directoriesToDelete = [];
165
+
166
+ for (const key of allKeys) {
167
+ const previous = previousDocument.files[key];
168
+ const current = currentDocument.files[key];
169
+
170
+ // If this file was neither created nor deleted...
171
+ if (previous && current) {
172
+ // Handle changing of text content.
173
+ if (previous.text !== current.text) {
174
+ if (isImageFile(current.name)) {
175
+ // Write image files as binary from base64
176
+ const buffer = Buffer.from(
177
+ current.text,
178
+ 'base64',
179
+ );
180
+ fs.writeFileSync(current.name, buffer);
181
+ } else {
182
+ // Write non-image files as text
183
+ fs.writeFileSync(current.name, current.text);
184
+ }
185
+ }
186
+
187
+ // // Handle renaming files.
188
+ // if (previous.name !== current.name) {
189
+
190
+ // const previousPath = previous.name.split('/');
191
+ // const currentPath = current.name.split('/');
192
+
193
+ // // - If the path is length 1 for previous and current,
194
+ // // then this should be true.
195
+ // // - If the path length is the sane for previous and current,
196
+ // // AND all the entries leading up to the last one are the same
197
+ // // between previous and current, AND the last entry in the arrays
198
+ // // (the file name itself) is different between previous and current
199
+ // const onlyFileNameChanged =
200
+ // (previousPath.length === 1 &&
201
+ // currentPath.length === 1) ||
202
+ // (previousPath.length === currentPath.length &&
203
+ // previousPath
204
+ // .slice(0, -1)
205
+ // .every(
206
+ // (val, index) => val === currentPath[index],
207
+ // ) &&
208
+ // previousPath.at(-1) !== currentPath.at(-1));
209
+
210
+ // // This logic works if only the file name itself changes.
211
+ // if (onlyFileNameChanged) {
212
+ // fs.renameSync(previous.name, current.name);
213
+ // }
214
+ // {
215
+ // // If we get here, then the directory may have been renamed,
216
+ // // or the file has been moved between directories.
217
+ // // If we get here, then the directory may have been renamed,
218
+ // // or the file has been moved between directories.
219
+ // const newDir = path.dirname(current.name);
220
+
221
+ // // Check if the new directory exists, if not, create it
222
+ // if (!fs.existsSync(newDir)) {
223
+ // fs.mkdirSync(newDir, { recursive: true });
224
+ // }
225
+
226
+ // // Move the file to the new directory
227
+ // fs.renameSync(previous.name, current.name);
228
+ // }
229
+ // }
230
+ // Handle renaming files.
231
+ if (previous.name !== current.name) {
232
+ if (isDirectory(previous.name)) {
233
+ // If the directory itself is being renamed,
234
+ // Phase I: Ignore it, and get the files moved
235
+ // Phase II: Keep track of these, and delete them after
236
+ // all the files moved.
237
+ // directoriesToDelete.push(previous.name)
238
+ directoriesToDelete.push(previous.name);
239
+ } else {
240
+ const newDir = path.dirname(current.name);
241
+
242
+ // Check if the new directory exists, if not, create it
243
+ if (!fs.existsSync(newDir)) {
244
+ fs.mkdirSync(newDir, { recursive: true });
245
+ }
246
+
247
+ // Move the file to the new directory
248
+ fs.renameSync(previous.name, current.name);
249
+ }
250
+ }
251
+ }
252
+
253
+ // handle deleting files and directories.
254
+ if (previous && !current) {
255
+ let stats = fs.statSync(previous.name);
256
+ //Check if the file path we are trying to delete is a directory
257
+ if (!stats.isDirectory()) {
258
+ fs.unlinkSync(previous.name);
259
+ } else {
260
+ //Performs directory deletion.
261
+ fs.rm(
262
+ previous.name,
263
+ {
264
+ recursive: true, //Makes sure that all files in directory are deleted.
265
+ },
266
+ (error) => {
267
+ if (error) {
268
+ console.log(error);
269
+ }
270
+ },
271
+ );
272
+ }
273
+ }
274
+
275
+ // Handle creating files and Directories.
276
+ if (!previous && current) {
277
+ //File Creation
278
+ if (!isDirectory(current.name)) {
279
+ if (isImageFile(current.name)) {
280
+ // Write image files as binary from base64
281
+ const buffer = Buffer.from(
282
+ current.text,
283
+ 'base64',
284
+ );
285
+ fs.writeFileSync(current.name, buffer);
286
+ } else {
287
+ // Write non-image files as text
288
+ fs.writeFileSync(current.name, current.text);
289
+ }
290
+ } else {
291
+ fs.mkdirSync(current.name, { recursive: true });
292
+ }
293
+ }
294
+ }
295
+ // TODO deleted all directories under directoriesToDelete
296
+
297
+ for (const dir of directoriesToDelete) {
298
+ //Performs directory deletion.
299
+ fs.rm(
300
+ dir,
301
+ {
302
+ recursive: true, //Makes sure that all files in directory are deleted.
303
+ },
304
+ (error) => {
305
+ if (error) {
306
+ console.log(error);
307
+ }
308
+ },
309
+ );
310
+ }
311
+
312
+ previousDocument = currentDocument;
313
+ };
314
+
315
+ // // Listen for when users modify files.
316
+ // // Files get written to disk after `autoSaveDebounceTimeMS`
317
+ // // milliseconds of inactivity.
318
+ // let timeout;
319
+ // shareDBDoc.subscribe(() => {
320
+ // shareDBDoc.on('op', () => {
321
+ // // console.log(shareDBDoc.data.isInteracting);
322
+ // clearTimeout(timeout);
323
+ // timeout = setTimeout(save, autoSaveDebounceTimeMS);
324
+ // });
325
+ // });
326
+
327
+ let lastExecutedTime = Date.now();
328
+ let lastChangeTimeout; // This timeout ensures the last change is saved
329
+
330
+ // Function to throttle the saving
331
+ function throttleSave() {
332
+ const now = Date.now();
333
+ const timeSinceLastExecution = now - lastExecutedTime;
334
+
335
+ if (timeSinceLastExecution > throttleTimeMS) {
336
+ save();
337
+ lastExecutedTime = now;
338
+ } else {
339
+ // Clear the previous timeout and set a new one to save the last change
340
+ clearTimeout(lastChangeTimeout);
341
+ lastChangeTimeout = setTimeout(() => {
342
+ save();
343
+ lastExecutedTime = now;
344
+ }, throttleTimeMS - timeSinceLastExecution);
345
+ }
346
+ }
347
+
348
+ // Function to debounce the saving
349
+ let debounceTimeout;
350
+ function debounceSave() {
351
+ clearTimeout(debounceTimeout);
352
+ debounceTimeout = setTimeout(
353
+ save,
354
+ autoSaveDebounceTimeMS,
355
+ );
356
+ }
357
+
358
+ // Subscribe to listen for modifications
359
+ shareDBDoc.subscribe(() => {
360
+ shareDBDoc.on('op', () => {
361
+ if (shareDBDoc.data.isInteracting) {
362
+ throttleSave();
363
+ } else {
364
+ debounceSave();
365
+ }
366
+ });
367
+ });
368
+
369
+ server.listen(port, async () => {
370
+ // Note: ngrok support can be added when the package is properly installed
371
+ // Sets the port to the one specified in the environment
372
+ // variable (for development) or the default port.
373
+ let livePort = process.env.EDITOR_PORT || port;
374
+ console.log(`EDITOR_PORT: ${process.env.EDITOR_PORT}`);
375
+ console.log(
376
+ `Editor is live at http://localhost:${livePort}`,
377
+ );
378
+ open(`http://localhost:${livePort}`);
379
+ });
@@ -0,0 +1 @@
1
+ export const isDirectory = (file) => file.endsWith('/');
@@ -0,0 +1,24 @@
1
+ import { AccessToken } from 'livekit-server-sdk';
2
+
3
+ const { LIVEKIT_API_KEY, LIVEKIT_API_SECRET } = process.env;
4
+
5
+ export const createToken = async (roomName, username) => {
6
+ // If this room doesn't exist, it'll be automatically created when the first
7
+ // client joins
8
+ // Identifier to be used for participant.
9
+ // It's available as LocalParticipant.identity with livekit-client SDK
10
+ // console.log(username);
11
+ const participantName = `${username}`;
12
+
13
+ const at = new AccessToken(
14
+ LIVEKIT_API_KEY,
15
+ LIVEKIT_API_SECRET,
16
+ {
17
+ identity: participantName,
18
+ // Token to expire after 10 minutes
19
+ ttl: '10m',
20
+ },
21
+ );
22
+ at.addGrant({ roomJoin: true, room: roomName });
23
+ return await at.toJwt();
24
+ };
@@ -0,0 +1,115 @@
1
+ import { format } from 'prettier/standalone';
2
+ import * as prettierPluginBabel from 'prettier/plugins/babel';
3
+ import * as prettierPluginEstree from 'prettier/plugins/estree';
4
+ import * as prettierPluginHtml from 'prettier/plugins/html';
5
+ import * as prettierPluginMarkdown from 'prettier/plugins/markdown';
6
+ import * as prettierPluginCSS from 'prettier/plugins/postcss';
7
+ import * as prettierPluginTypescript from 'prettier/plugins/typescript';
8
+ import { VizFiles, VizFileId } from '@vizhub/viz-types';
9
+
10
+ // Parser mappings - matches client-side implementation
11
+ const parsers = {
12
+ js: 'babel',
13
+ jsx: 'babel',
14
+ mjs: 'babel',
15
+ cjs: 'babel',
16
+ ts: 'typescript',
17
+ tsx: 'typescript',
18
+ css: 'css',
19
+ html: 'html',
20
+ json: 'json',
21
+ json5: 'json5',
22
+ md: 'markdown',
23
+ markdown: 'markdown',
24
+ } as const;
25
+
26
+ // Prettier plugins - matches client-side implementation
27
+ const plugins = [
28
+ prettierPluginBabel,
29
+ prettierPluginEstree,
30
+ prettierPluginHtml,
31
+ prettierPluginMarkdown,
32
+ prettierPluginTypescript,
33
+ prettierPluginCSS,
34
+ ] as any;
35
+
36
+ // Prettier options - matches client-side implementation and .prettierrc
37
+ const prettierOptions = {
38
+ proseWrap: 'always' as const,
39
+ singleQuote: true,
40
+ printWidth: 60,
41
+ };
42
+
43
+ /**
44
+ * Extracts file extension from filename
45
+ * Example: 'foo.js' => 'js'
46
+ */
47
+ const getFileExtension = (fileName: string): string => {
48
+ const match = fileName.match(/\.([^.]+)$/);
49
+ return match ? match[1] : '';
50
+ };
51
+
52
+ /**
53
+ * Formats a single file using Prettier
54
+ */
55
+ export const formatFile = async (
56
+ fileText: string,
57
+ fileName: string,
58
+ ): Promise<string | null> => {
59
+ const fileExtension = getFileExtension(fileName);
60
+ const parser =
61
+ parsers[fileExtension as keyof typeof parsers];
62
+
63
+ // If no parser is found, return null (no formatting)
64
+ if (!parser) {
65
+ return null;
66
+ }
67
+
68
+ try {
69
+ const formatted = await format(fileText, {
70
+ parser,
71
+ plugins,
72
+ ...prettierOptions,
73
+ });
74
+ return formatted;
75
+ } catch (error) {
76
+ // Log error but don't throw - return original text
77
+ console.error(
78
+ `Prettier formatting error for ${fileName}:`,
79
+ error,
80
+ );
81
+ return null;
82
+ }
83
+ };
84
+
85
+ /**
86
+ * Formats multiple files using Prettier
87
+ * Only formats files that have supported extensions
88
+ * Returns a map of fileId -> formatted text for successfully formatted files
89
+ */
90
+ export const formatFiles = async (
91
+ files: VizFiles,
92
+ fileIds?: VizFileId[],
93
+ ): Promise<{ [fileId: VizFileId]: string }> => {
94
+ const results: { [fileId: VizFileId]: string } = {};
95
+ const targetFileIds = fileIds || Object.keys(files);
96
+
97
+ // Process files in parallel for better performance
98
+ const formatPromises = targetFileIds.map(
99
+ async (fileId) => {
100
+ const file = files[fileId];
101
+ if (!file) return;
102
+
103
+ const formatted = await formatFile(
104
+ file.text,
105
+ file.name,
106
+ );
107
+ if (formatted !== null && formatted !== file.text) {
108
+ results[fileId] = formatted;
109
+ }
110
+ },
111
+ );
112
+
113
+ await Promise.all(formatPromises);
114
+ return results;
115
+ };
@@ -0,0 +1,8 @@
1
+ import dotenv from 'dotenv';
2
+ import { fileURLToPath } from 'url';
3
+ import { join } from 'node:path';
4
+
5
+ const dir = fileURLToPath(import.meta.url);
6
+ dotenv.config({
7
+ path: join(dir, '../../../.env'),
8
+ });
@@ -0,0 +1,25 @@
1
+ import { diff } from './ot.js';
2
+
3
+ /**
4
+ * Creates a submitOperation function that can be used to submit diff-based operations to ShareDB.
5
+ * This is the core logic extracted from useSubmitOperation for reuse across client and server.
6
+ */
7
+ export const createSubmitOperation = <T>(shareDBDoc: {
8
+ data: T;
9
+ submitOp: (op: any) => void;
10
+ }): ((next: (data: T) => T) => void) => {
11
+ return (next) => {
12
+ const data: T = shareDBDoc.data;
13
+ const op = diff(data, next(data));
14
+ if (op && shareDBDoc) {
15
+ shareDBDoc.submitOp(op);
16
+ }
17
+ };
18
+ };
19
+
20
+ /**
21
+ * Type definition for the submitOperation function
22
+ */
23
+ export type SubmitOperationFunction<T> = (
24
+ next: (data: T) => T,
25
+ ) => void;