roport 1.4.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/ignore.js DELETED
@@ -1,120 +0,0 @@
1
- const fs = require('fs-extra');
2
- const path = require('path');
3
-
4
- class IgnoreManager {
5
- constructor(projectRoot) {
6
- this.projectRoot = projectRoot;
7
- this.rules = [];
8
- this.cache = new Map(); // Cache for scalability
9
- this.loadRules();
10
- }
11
-
12
- loadRules() {
13
- this.rules = [];
14
- this.cache.clear();
15
- // Default ignores
16
- this.addRule('.git');
17
- this.addRule('node_modules');
18
- this.addRule('cli');
19
- this.addRule('Roport.rbxmx');
20
- this.addRule('RoportSyncPlugin.rbxmx');
21
- this.addRule('.DS_Store');
22
- this.addRule('package.json');
23
- this.addRule('package-lock.json');
24
-
25
- // Load from files
26
- const ignoreFiles = ['.rojoignore', '.gitignore'];
27
- for (const file of ignoreFiles) {
28
- const ignorePath = path.join(this.projectRoot, file);
29
- if (fs.existsSync(ignorePath)) {
30
- try {
31
- const content = fs.readFileSync(ignorePath, 'utf8');
32
- const lines = content.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#'));
33
- lines.forEach(line => this.addRule(line));
34
- } catch (e) {
35
- // Ignore read errors
36
- }
37
- }
38
- }
39
- }
40
-
41
- addRule(rule) {
42
- this.rules.push(rule);
43
- }
44
-
45
- isIgnored(filePath) {
46
- if (this.cache.has(filePath)) {
47
- return this.cache.get(filePath);
48
- }
49
-
50
- // If filePath is absolute, make it relative
51
- let relativePath = filePath;
52
- if (path.isAbsolute(filePath)) {
53
- relativePath = path.relative(this.projectRoot, filePath);
54
- }
55
-
56
- // Normalize to forward slashes
57
- relativePath = relativePath.replace(/\\/g, '/');
58
-
59
- if (relativePath.startsWith('../')) {
60
- this.cache.set(filePath, true);
61
- return true; // Outside project
62
- }
63
- if (relativePath === '' || relativePath === '.') {
64
- this.cache.set(filePath, false);
65
- return false; // Root
66
- }
67
-
68
- for (const rule of this.rules) {
69
- // 1. Exact match
70
- if (relativePath === rule) {
71
- this.cache.set(filePath, true);
72
- return true;
73
- }
74
-
75
- // 2. Directory match (rule ends with /)
76
- if (rule.endsWith('/') && relativePath.startsWith(rule)) {
77
- this.cache.set(filePath, true);
78
- return true;
79
- }
80
-
81
- // 3. Extension match (*.lua)
82
- if (rule.startsWith('*.') && relativePath.endsWith(rule.slice(1))) {
83
- this.cache.set(filePath, true);
84
- return true;
85
- }
86
-
87
- // 4. Folder match (node_modules) - check if it's a segment
88
- if (!rule.includes('/') && !rule.includes('*')) {
89
- const segments = relativePath.split('/');
90
- if (segments.includes(rule)) {
91
- this.cache.set(filePath, true);
92
- return true;
93
- }
94
- }
95
-
96
- // 5. Basic wildcard (src/*)
97
- if (rule.endsWith('/*')) {
98
- const base = rule.slice(0, -2);
99
- if (relativePath.startsWith(base + '/') && relativePath.split('/').length === base.split('/').length + 1) {
100
- this.cache.set(filePath, true);
101
- return true;
102
- }
103
- }
104
-
105
- // 6. Recursive wildcard (src/**)
106
- if (rule.endsWith('/**')) {
107
- const base = rule.slice(0, -3);
108
- if (relativePath.startsWith(base + '/')) {
109
- this.cache.set(filePath, true);
110
- return true;
111
- }
112
- }
113
- }
114
-
115
- this.cache.set(filePath, false);
116
- return false;
117
- }
118
- }
119
-
120
- module.exports = IgnoreManager;
package/src/server.js DELETED
@@ -1,393 +0,0 @@
1
- const express = require('express');
2
- const fs = require('fs-extra');
3
- const path = require('path');
4
- const chalk = require('chalk');
5
-
6
- function createApp(projectRoot) {
7
- const app = express();
8
-
9
- // Determine project root
10
- if (!projectRoot) {
11
- projectRoot = process.cwd();
12
- if (fs.existsSync(path.join(projectRoot, 'src'))) {
13
- // We are in the root
14
- } else if (fs.existsSync(path.join(projectRoot, '../src'))) {
15
- // We are in cli/ or similar
16
- projectRoot = path.resolve(projectRoot, '..');
17
- }
18
- }
19
- console.log(chalk.cyan(`Project Root: ${projectRoot}`));
20
-
21
- // State for two-way sync
22
- let changedFiles = new Set();
23
- let commandQueue = []; // Queue for commands to be sent to Roblox
24
- let executionResults = new Map(); // Store results of executed scripts
25
- let isWriting = false; // Lock to prevent loops (Roblox -> File -> Watcher -> Roblox)
26
-
27
- // Watch for file changes locally
28
- let watcher;
29
- try {
30
- console.log(chalk.gray(`Starting file watcher on ${projectRoot}...`));
31
- watcher = fs.watch(projectRoot, { recursive: true }, (eventType, filename) => {
32
- if (filename && !isWriting) {
33
- // Normalize path to forward slashes
34
- const relativePath = filename.replace(/\\/g, '/');
35
-
36
- // Check ignore rules
37
- if (isIgnored(relativePath)) return;
38
-
39
- // Debounce/Deduplicate slightly
40
- if (!changedFiles.has(relativePath)) {
41
- changedFiles.add(relativePath);
42
- console.log(chalk.yellow(`File changed: ${relativePath} (${eventType})`));
43
- }
44
- }
45
- });
46
- } catch (e) {
47
- console.warn(chalk.red("File watching failed (might not be supported on this OS):"), e);
48
- }
49
-
50
- app.close = () => {
51
- if (watcher) watcher.close();
52
- };
53
-
54
- // Increase limit for large file batches
55
- app.use(express.json({ limit: '50mb' }));
56
-
57
- // Ignore Logic
58
- const IgnoreManager = require('./ignore');
59
- const ignoreManager = new IgnoreManager(projectRoot);
60
-
61
- function isIgnored(filePath) {
62
- return ignoreManager.isIgnored(filePath);
63
- }
64
-
65
- // Project Configuration Parsing
66
- function getProjectConfig() {
67
- const configPath = path.join(projectRoot, 'default.project.json');
68
- if (fs.existsSync(configPath)) {
69
- try {
70
- const config = fs.readJsonSync(configPath);
71
- const mounts = [];
72
-
73
- function traverse(node, currentPath) {
74
- const info = {
75
- robloxPath: currentPath,
76
- properties: node.$properties,
77
- ignoreUnknownInstances: node.$ignoreUnknownInstances,
78
- className: node.$className
79
- };
80
-
81
- if (node.$path) {
82
- info.filePath = node.$path;
83
- }
84
-
85
- // Push if it's a mount point (has path) or has configuration
86
- if (info.filePath || info.properties || info.className || info.ignoreUnknownInstances) {
87
- mounts.push(info);
88
- }
89
-
90
- for (const key in node) {
91
- if (!key.startsWith('$') && typeof node[key] === 'object') {
92
- traverse(node[key], [...currentPath, key]);
93
- }
94
- }
95
- }
96
-
97
- if (config.tree) {
98
- traverse(config.tree, []);
99
- }
100
-
101
- return { name: config.name, mounts };
102
- } catch (e) {
103
- console.error(chalk.red("Failed to parse default.project.json:"), e);
104
- }
105
- }
106
- return null;
107
- }
108
-
109
- app.get('/config', (req, res) => {
110
- const config = getProjectConfig();
111
- if (config) {
112
- res.send(config);
113
- } else {
114
- // Fallback for legacy projects without config
115
- res.send({
116
- mounts: [
117
- { robloxPath: ["ServerScriptService"], filePath: "src/server" },
118
- { robloxPath: ["ReplicatedStorage"], filePath: "src/shared" },
119
- { robloxPath: ["StarterPlayer", "StarterPlayerScripts"], filePath: "src/client" },
120
- { robloxPath: ["Workspace"], filePath: "src/workspace" },
121
- { robloxPath: ["StarterGui"], filePath: "src/interface" },
122
- { robloxPath: ["StarterPack"], filePath: "src/tools" },
123
- { robloxPath: ["Lighting"], filePath: "src/lighting" },
124
- { robloxPath: ["ReplicatedFirst"], filePath: "src/first" },
125
- { robloxPath: ["SoundService"], filePath: "src/sounds" }
126
- ]
127
- });
128
- }
129
- });
130
-
131
- app.get('/ping', (req, res) => {
132
- res.send('pong');
133
- });
134
-
135
- // AI Helper: Get Project Tree
136
- app.get('/tree', async (req, res) => {
137
- try {
138
- const contextGen = require('./context');
139
- const tree = await contextGen.generate(projectRoot, { maxDepth: 5, includeSource: false });
140
- res.send({ tree });
141
- } catch (e) {
142
- res.status(500).send({ error: e.message });
143
- }
144
- });
145
-
146
- // New endpoint for Roblox to poll for changes
147
- app.get('/poll', async (req, res) => {
148
- const response = { changes: [], deletions: [], commands: [] };
149
- const MAX_CHANGES_PER_POLL = 50; // Scalability: Batch size limit
150
-
151
- // Handle Commands
152
- if (commandQueue.length > 0) {
153
- response.commands = [...commandQueue];
154
- commandQueue = []; // Clear queue
155
- console.log(chalk.magenta(`Sending ${response.commands.length} commands to Roblox`));
156
- }
157
-
158
- // Handle File Changes & Deletions
159
- if (changedFiles.size > 0) {
160
- // Convert to array to slice, but we need to be careful about concurrent updates
161
- const allFiles = Array.from(changedFiles);
162
- const batch = allFiles.slice(0, MAX_CHANGES_PER_POLL);
163
-
164
- // Remove these from the set immediately so we don't process them again
165
- // If processing fails, we might lose them, but for now this is acceptable for scalability
166
- batch.forEach(f => changedFiles.delete(f));
167
-
168
- for (const filePath of batch) {
169
- try {
170
- const fullPath = path.resolve(projectRoot, filePath);
171
-
172
- if (await fs.pathExists(fullPath)) {
173
- // File Exists -> Update
174
- const stat = await fs.stat(fullPath);
175
- if (stat.isFile()) {
176
- // Check for binary files
177
- if (filePath.endsWith('.rbxm') || filePath.endsWith('.png') || filePath.endsWith('.jpg')) {
178
- // For binary files, we don't send content (or we could send base64)
179
- // Sync.lua currently uses absolutePath for .rbxm, so content is ignored.
180
- // Sending empty content prevents utf8 corruption errors.
181
- response.changes.push({ filePath, absolutePath: fullPath, content: "" });
182
- } else {
183
- const content = await fs.readFile(fullPath, 'utf8');
184
- response.changes.push({ filePath, absolutePath: fullPath, content });
185
- }
186
- } else if (stat.isDirectory()) {
187
- response.changes.push({ filePath, absolutePath: fullPath, isDirectory: true });
188
- }
189
- } else {
190
- // File Does Not Exist -> Deletion
191
- response.deletions.push(filePath);
192
- }
193
- } catch (e) {
194
- console.error(chalk.red(`Error reading changed file ${filePath}:`), e);
195
- }
196
- }
197
-
198
- if (response.changes.length > 0) {
199
- console.log(chalk.magenta(`Sending ${response.changes.length} updates to Roblox (Batch of ${batch.length})`));
200
- }
201
- if (response.deletions.length > 0) {
202
- console.log(chalk.magenta(`Sending ${response.deletions.length} deletions to Roblox`));
203
- }
204
-
205
- // If there are more changes pending, we could hint the client to poll again immediately
206
- if (changedFiles.size > 0) {
207
- response.morePending = true;
208
- }
209
- }
210
-
211
- res.send(response);
212
- });
213
-
214
- app.post('/move', async (req, res) => {
215
- const { oldPath, newPath } = req.body;
216
-
217
- if (!oldPath || !newPath) {
218
- return res.status(400).send('Missing paths');
219
- }
220
-
221
- const safeOld = path.resolve(projectRoot, oldPath);
222
- const safeNew = path.resolve(projectRoot, newPath);
223
-
224
- if (!safeOld.startsWith(projectRoot) || !safeNew.startsWith(projectRoot)) {
225
- return res.status(400).send('Unsafe path');
226
- }
227
-
228
- console.log(chalk.blue(`Moving ${oldPath} -> ${newPath}`));
229
- isWriting = true;
230
-
231
- try {
232
- if (await fs.pathExists(safeOld)) {
233
- await fs.move(safeOld, safeNew, { overwrite: true });
234
- console.log(chalk.green(`Moved successfully`));
235
- res.send({ success: true });
236
- } else {
237
- res.status(404).send('Old path not found');
238
- }
239
- } catch (err) {
240
- console.error(chalk.red('Move failed:'), err);
241
- res.status(500).send({ error: err.message });
242
- } finally {
243
- setTimeout(() => { isWriting = false; }, 500);
244
- }
245
- });
246
-
247
- app.post('/log', (req, res) => {
248
- const { message, type, timestamp } = req.body;
249
- const timeStr = new Date((timestamp || Date.now() / 1000) * 1000).toLocaleTimeString();
250
-
251
- let color = chalk.white;
252
- if (type === 2 || type === 'Error') color = chalk.red; // Error
253
- else if (type === 1 || type === 'Warning') color = chalk.yellow; // Warning
254
-
255
- console.log(chalk.gray(`[STUDIO ${timeStr}]`) + " " + color(message));
256
- res.send({ success: true });
257
- });
258
-
259
- // AI Interface: Remote Execution
260
- app.post('/execute', (req, res) => {
261
- const { code } = req.body;
262
- if (!code) return res.status(400).send('Missing code');
263
-
264
- const id = Date.now().toString();
265
- commandQueue.push({ type: 'EXECUTE', id, code });
266
- console.log(chalk.blue(`Queued execution ${id}`));
267
-
268
- // Wait for result (long polling implementation could be better, but simple polling works for now)
269
- res.send({ id, status: 'queued' });
270
- });
271
-
272
- app.post('/execute-result', (req, res) => {
273
- const { id, success, result, error } = req.body;
274
- executionResults.set(id, { success, result, error });
275
- console.log(chalk.blue(`Received result for ${id}: ${success ? 'Success' : 'Failed'}`));
276
- res.send({ success: true });
277
- });
278
-
279
- app.get('/execute/:id', (req, res) => {
280
- const { id } = req.params;
281
- if (executionResults.has(id)) {
282
- const result = executionResults.get(id);
283
- executionResults.delete(id); // Consume result
284
- res.send({ status: 'completed', ...result });
285
- } else {
286
- res.send({ status: 'pending' });
287
- }
288
- });
289
-
290
- app.post('/command', (req, res) => {
291
- const { type, path } = req.body;
292
- if (!type) return res.status(400).send('Missing command type');
293
-
294
- commandQueue.push({ type, path });
295
- console.log(chalk.blue(`Queued command: ${type}`));
296
- res.send({ success: true });
297
- });
298
-
299
- app.post('/batch', async (req, res) => {
300
- const { files } = req.body;
301
-
302
- if (!files || !Array.isArray(files)) {
303
- return res.status(400).send('Invalid body');
304
- }
305
-
306
- console.log(chalk.blue(`Received batch of ${files.length} files`));
307
-
308
- isWriting = true; // Lock watcher
309
-
310
- try {
311
- for (const file of files) {
312
- const { filePath, content } = file;
313
- // Prevent directory traversal
314
- const safePath = path.resolve(projectRoot, filePath);
315
- if (!safePath.startsWith(projectRoot)) {
316
- console.warn(chalk.yellow(`Skipping unsafe path: ${filePath}`));
317
- continue;
318
- }
319
-
320
- if (isIgnored(filePath)) {
321
- console.log(chalk.gray(`Skipping ignored file: ${filePath}`));
322
- continue;
323
- }
324
-
325
- if (filePath.endsWith('.json')) {
326
- try {
327
- const json = JSON.parse(content);
328
- await fs.outputFile(safePath, JSON.stringify(json, null, 2));
329
- } catch (e) {
330
- await fs.outputFile(safePath, content);
331
- }
332
- } else {
333
- await fs.outputFile(safePath, content);
334
- }
335
- console.log(chalk.green(`Synced: ${filePath}`));
336
- }
337
- res.send({ success: true });
338
- } catch (err) {
339
- console.error(chalk.red('Error syncing files:'), err);
340
- res.status(500).send({ error: err.message });
341
- } finally {
342
- // Release lock after a short delay to let FS settle
343
- setTimeout(() => { isWriting = false; }, 500);
344
- }
345
- });
346
-
347
- app.post('/delete', async (req, res) => {
348
- const { files } = req.body;
349
-
350
- if (!files || !Array.isArray(files)) {
351
- return res.status(400).send('Invalid body');
352
- }
353
-
354
- console.log(chalk.blue(`Received delete request for ${files.length} files`));
355
-
356
- try {
357
- for (const filePath of files) {
358
- // Prevent directory traversal
359
- const safePath = path.resolve(projectRoot, filePath);
360
- if (!safePath.startsWith(projectRoot)) {
361
- console.warn(chalk.yellow(`Skipping unsafe delete path: ${filePath}`));
362
- continue;
363
- }
364
-
365
- if (isIgnored(filePath)) {
366
- console.log(chalk.gray(`Skipping ignored file deletion: ${filePath}`));
367
- continue;
368
- }
369
-
370
- if (await fs.pathExists(safePath)) {
371
- await fs.remove(safePath);
372
- console.log(chalk.magenta(`Deleted: ${filePath}`));
373
- }
374
- }
375
- res.send({ success: true });
376
- } catch (err) {
377
- console.error(chalk.red('Error deleting files:'), err);
378
- res.status(500).send({ error: err.message });
379
- }
380
- });
381
-
382
- return app;
383
- }
384
-
385
- function startServer(port) {
386
- const app = createApp();
387
- return app.listen(port, () => {
388
- console.log(chalk.cyan(`Roport server running on http://127.0.0.1:${port}`));
389
- console.log(chalk.gray('Waiting for requests...'));
390
- });
391
- }
392
-
393
- module.exports = { startServer, createApp };
@@ -1,28 +0,0 @@
1
- # Roport Project
2
-
3
- This project was initialized with [Roport](https://github.com/Rydaguy101/RojoExportPlugin).
4
-
5
- ## Getting Started
6
-
7
- 1. **Start the Sync Server:**
8
- ```bash
9
- roport serve
10
- ```
11
-
12
- 2. **Connect in Roblox Studio:**
13
- - Install the **Roport Sync Plugin**.
14
- - Click **Connect** in the plugin toolbar.
15
-
16
- ## Project Structure
17
-
18
- - `src/server`: Server-side scripts (ServerScriptService)
19
- - `src/client`: Client-side scripts (StarterPlayerScripts)
20
- - `src/shared`: Shared modules (ReplicatedStorage)
21
- - `src/workspace`: Workspace objects
22
- - `default.project.json`: Project configuration (Rojo-compatible)
23
-
24
- ## Commands
25
-
26
- - `roport serve`: Start sync server
27
- - `roport build -o MyGame.rbxmx`: Build project to model file
28
- - `roport context`: Generate AI context summary
@@ -1,23 +0,0 @@
1
- {
2
- "name": "RoportProject",
3
- "tree": {
4
- "$className": "DataModel",
5
- "ServerScriptService": {
6
- "$path": "src/server"
7
- },
8
- "ReplicatedStorage": {
9
- "$path": "src/shared"
10
- },
11
- "StarterPlayer": {
12
- "StarterPlayerScripts": {
13
- "$path": "src/client"
14
- }
15
- },
16
- "Workspace": {
17
- "$path": "src/workspace"
18
- },
19
- "Lighting": {
20
- "$path": "src/lighting"
21
- }
22
- }
23
- }
@@ -1 +0,0 @@
1
- print("Hello from Roport Client!")
@@ -1 +0,0 @@
1
- print("Hello from Roport Server!")
@@ -1,7 +0,0 @@
1
- local module = {}
2
-
3
- function module.world()
4
- print("Hello World from Shared!")
5
- end
6
-
7
- return module