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/dist/index.js +1650 -0
- package/dist/index.js.map +1 -0
- package/package.json +25 -32
- package/README.md +0 -52
- package/assets/RoportSyncPlugin.rbxmx +0 -1864
- package/bin/roport.js +0 -131
- package/src/builder.js +0 -563
- package/src/context.js +0 -76
- package/src/ignore.js +0 -120
- package/src/server.js +0 -393
- package/templates/default/README.md +0 -28
- package/templates/default/default.project.json +0 -23
- package/templates/default/src/client/init.client.lua +0 -1
- package/templates/default/src/server/init.server.lua +0 -1
- package/templates/default/src/shared/Hello.lua +0 -7
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!")
|