syncrbx 1.1.0 → 1.1.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/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <div align="center">
2
- <img src="https://raw.githubusercontent.com/DevManyPB/Verde/main/Logo.png" alt="SyncRbx" width="200" />
2
+ <img src="https://www.syncrbx.xyz/Logo.png" alt="SyncRbx" width="200" />
3
3
  </div>
4
4
 
5
5
  # SyncRbx CLI
package/converters.js CHANGED
@@ -382,24 +382,44 @@ function filePathToInstancePath(filePath, projectRoot) {
382
382
  // ---------------------------------------------------------------------------
383
383
  // Phase 2 - #4: Project config loader
384
384
  // ---------------------------------------------------------------------------
385
+ const PROJECT_CONFIG_FILE = 'syncrbx.project.json';
386
+
387
+ // Keeps only known Roblox services, without duplicates. Returns null when the
388
+ // value is not a list, meaning "not chosen yet".
389
+ function sanitizeServices(services) {
390
+ if (!Array.isArray(services)) return null;
391
+ return [...new Set(services.filter(s => typeof s === 'string' && ROOT_SERVICES.includes(s)))];
392
+ }
393
+
394
+ function readProjectConfigFile(projectRoot) {
395
+ const configPath = path.join(projectRoot, PROJECT_CONFIG_FILE);
396
+ if (!fs.existsSync(configPath)) return {};
397
+ try {
398
+ return JSON.parse(fs.readFileSync(configPath, 'utf8'));
399
+ } catch (e) {
400
+ console.error(`[WARN] Error parsing ${PROJECT_CONFIG_FILE}: ${e.message}`);
401
+ return {};
402
+ }
403
+ }
404
+
385
405
  function loadProjectConfig(projectRoot) {
386
- const configPath = path.join(projectRoot, 'syncrbx.project.json');
387
406
  const defaults = {
388
407
  name: path.basename(projectRoot),
389
408
  port: 34872,
390
409
  ignore: ['*.conflict.bak', 'node_modules', '.git', '.vscode'],
391
410
  tree: null, // null = auto-detect from folder names
411
+ services: null, // null = the user has not chosen which services to sync
392
412
  };
413
+ const config = { ...defaults, ...readProjectConfigFile(projectRoot) };
414
+ config.services = sanitizeServices(config.services);
415
+ return config;
416
+ }
393
417
 
394
- if (fs.existsSync(configPath)) {
395
- try {
396
- const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
397
- return { ...defaults, ...config };
398
- } catch (e) {
399
- console.error(`[WARN] Error parsing syncrbx.project.json: ${e.message}`);
400
- }
401
- }
402
- return defaults;
418
+ // Saves the chosen services, keeping any other settings already in the file.
419
+ function saveProjectServices(projectRoot, services) {
420
+ const config = readProjectConfigFile(projectRoot);
421
+ config.services = services;
422
+ fs.writeFileSync(path.join(projectRoot, PROJECT_CONFIG_FILE), JSON.stringify(config, null, 2) + '\n', 'utf8');
403
423
  }
404
424
 
405
425
  module.exports = {
@@ -420,5 +440,8 @@ module.exports = {
420
440
  filePathToInstancePath,
421
441
  getMetaProperties,
422
442
  loadProjectConfig,
443
+ saveProjectServices,
444
+ sanitizeServices,
445
+ PROJECT_CONFIG_FILE,
423
446
  fileChecksum,
424
447
  };
package/local-sync.js CHANGED
@@ -6,6 +6,7 @@ const {
6
6
  processDirectory, convertFile, instanceToFile,
7
7
  filePathToInstancePath, stripExtension, ROOT_SERVICES,
8
8
  buildChecksums, loadProjectConfig, classFromFileName, isInitFile, ANY_CLASS,
9
+ saveProjectServices, sanitizeServices, STARTER_PLAYER_CHILDREN,
9
10
  } = require('./converters');
10
11
 
11
12
  const app = express();
@@ -17,6 +18,45 @@ const PROJECT_ROOT = process.cwd();
17
18
 
18
19
  const projectConfig = loadProjectConfig(PROJECT_ROOT);
19
20
  const PORT = projectConfig.port || 34872;
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Synced services — the user picks them in the Studio plugin the first time a
24
+ // project connects; they are saved in syncrbx.project.json. Nothing outside
25
+ // them is read, written or deleted, in either direction.
26
+ // ---------------------------------------------------------------------------
27
+ function isServiceSynced(serviceName) {
28
+ // Projects that have not chosen yet keep the previous behaviour (every
29
+ // known service), so older plugins still work.
30
+ const services = projectConfig.services || ROOT_SERVICES;
31
+ return services.includes(serviceName);
32
+ }
33
+
34
+ // "Workspace/Map/Door" → is Workspace synced?
35
+ function isInstancePathSynced(instancePath) {
36
+ return isServiceSynced(String(instancePath || '').split('/')[0]);
37
+ }
38
+
39
+ function isDiskPathSynced(filePath) {
40
+ const relative = path.relative(PROJECT_ROOT, filePath);
41
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) return false;
42
+ return isServiceSynced(relative.split(path.sep)[0]);
43
+ }
44
+
45
+ // Creates the folder of each synced service that does not exist yet.
46
+ function createServiceFolders(services) {
47
+ const folders = [...services];
48
+ if (services.includes('StarterPlayer')) {
49
+ folders.push(...STARTER_PLAYER_CHILDREN.map(child => `StarterPlayer/${child}`));
50
+ }
51
+ for (const folder of folders) {
52
+ const folderPath = path.join(PROJECT_ROOT, folder);
53
+ if (!fs.existsSync(folderPath)) {
54
+ markIgnored(folderPath);
55
+ fs.mkdirSync(folderPath, { recursive: true });
56
+ logDisk(`Folder created: ${folder}`);
57
+ }
58
+ }
59
+ }
20
60
  const HOST = '127.0.0.1';
21
61
  const ALLOWED_HOSTS = new Set([`localhost:${PORT}`, `127.0.0.1:${PORT}`, `[::1]:${PORT}`]);
22
62
 
@@ -257,30 +297,30 @@ const watcher = chokidar.watch(PROJECT_ROOT, {
257
297
 
258
298
  watcher
259
299
  .on('add', filePath => {
260
- if (isIgnored(filePath)) return;
300
+ if (isIgnored(filePath) || !isDiskPathSynced(filePath)) return;
261
301
  logDisk(`File created: ${path.relative(PROJECT_ROOT, filePath)}`);
262
302
  getTimestamps(filePath).disk = Date.now();
263
303
  pushChange(buildChangePayload('Added', filePath));
264
304
  })
265
305
  .on('change', filePath => {
266
- if (isIgnored(filePath)) return;
306
+ if (isIgnored(filePath) || !isDiskPathSynced(filePath)) return;
267
307
  logDisk(`File modified: ${path.relative(PROJECT_ROOT, filePath)}`);
268
308
  getTimestamps(filePath).disk = Date.now();
269
309
  pushChange(buildChangePayload('Changed', filePath));
270
310
  })
271
311
  .on('unlink', filePath => {
272
- if (isIgnored(filePath)) return;
312
+ if (isIgnored(filePath) || !isDiskPathSynced(filePath)) return;
273
313
  logDisk(`File deleted: ${path.relative(PROJECT_ROOT, filePath)}`);
274
314
  pushChange(buildChangePayload('Removed', filePath));
275
315
  lastWriteTimestamps.delete(path.resolve(filePath));
276
316
  })
277
317
  .on('addDir', dirPath => {
278
- if (isIgnored(dirPath)) return;
318
+ if (isIgnored(dirPath) || !isDiskPathSynced(dirPath)) return;
279
319
  logDisk(`Folder created: ${path.relative(PROJECT_ROOT, dirPath)}`);
280
320
  pushChange(buildChangePayload('Added', dirPath, true));
281
321
  })
282
322
  .on('unlinkDir', dirPath => {
283
- if (isIgnored(dirPath)) return;
323
+ if (isIgnored(dirPath) || !isDiskPathSynced(dirPath)) return;
284
324
  logDisk(`Folder deleted: ${path.relative(PROJECT_ROOT, dirPath)}`);
285
325
  pushChange(buildChangePayload('Removed', dirPath, true));
286
326
  });
@@ -294,9 +334,32 @@ app.get('/ping', (req, res) => {
294
334
  status: 'ok',
295
335
  project: projectConfig.name || path.basename(PROJECT_ROOT),
296
336
  version: require('./package.json').version,
337
+ // false until the user picks the services to sync in the plugin
338
+ configured: Array.isArray(projectConfig.services),
339
+ services: projectConfig.services,
340
+ availableServices: ROOT_SERVICES,
297
341
  });
298
342
  });
299
343
 
344
+ // POST /config — saves the services chosen in the plugin
345
+ app.post('/config', (req, res) => {
346
+ const requested = req.body && req.body.services;
347
+ const services = sanitizeServices(requested);
348
+ if (!services || services.length === 0 || services.length !== requested.length) {
349
+ return res.status(400).json({ error: 'services must be a non-empty list of Roblox services' });
350
+ }
351
+ try {
352
+ saveProjectServices(PROJECT_ROOT, services);
353
+ projectConfig.services = services;
354
+ createServiceFolders(services);
355
+ logServer(`Synced services: ${services.join(', ')}`);
356
+ res.json({ success: true, services });
357
+ } catch (e) {
358
+ logError(`Could not save the project config: ${e.message}`);
359
+ res.status(500).json({ error: e.message });
360
+ }
361
+ });
362
+
300
363
  app.post('/shutdown', (req, res) => {
301
364
  res.json({ success: true });
302
365
  logServer('Shutdown requested by VS Code extension.');
@@ -310,7 +373,7 @@ app.get('/tree', (req, res) => {
310
373
  const rootFolders = fs.readdirSync(PROJECT_ROOT, { withFileTypes: true });
311
374
  for (const folder of rootFolders) {
312
375
  // Only service folders are synced; skips node_modules, .git...
313
- if (folder.isDirectory() && ROOT_SERVICES.includes(folder.name)) {
376
+ if (folder.isDirectory() && ROOT_SERVICES.includes(folder.name) && isServiceSynced(folder.name)) {
314
377
  const subTree = processDirectory(path.join(PROJECT_ROOT, folder.name), true);
315
378
  tree.push(subTree);
316
379
  }
@@ -400,6 +463,11 @@ app.post('/studio-change', (req, res) => {
400
463
  continue;
401
464
  }
402
465
 
466
+ // Changes outside the synced services never touch the disk
467
+ if (!isInstancePathSynced(change.path) || (change.oldPath && !isInstancePathSynced(change.oldPath))) {
468
+ continue;
469
+ }
470
+
403
471
  logStudio(`${change.type} → ${change.path} (${change.className || '?'})`);
404
472
 
405
473
  const fileInfo = instanceToFile(change);
@@ -526,31 +594,24 @@ process.on('SIGTERM', () => shutdown('SIGTERM'));
526
594
  // Start
527
595
  // ---------------------------------------------------------------------------
528
596
  function startServer() {
529
- const defaultFolders = [
530
- ...ROOT_SERVICES,
531
- 'StarterPlayer/StarterPlayerScripts',
532
- 'StarterPlayer/StarterCharacterScripts'
533
- ];
534
- for (const folder of defaultFolders) {
535
- const folderPath = path.join(PROJECT_ROOT, folder);
536
- if (!fs.existsSync(folderPath)) {
537
- try {
538
- fs.mkdirSync(folderPath, { recursive: true });
539
- } catch (e) {
540
- // Ignore permissions/nested errors
541
- }
542
- }
597
+ if (projectConfig.services) {
598
+ createServiceFolders(projectConfig.services);
543
599
  }
544
600
 
545
601
  const server = app.listen(PORT, HOST, () => {
546
602
  console.log('');
547
603
  console.log(c.bold(c.green(' ╔══════════════════════════════════════════╗')));
548
- console.log(c.bold(c.green(' ║ 🌿 SYNCRBX v1.0 🌿 ║')));
604
+ console.log(c.bold(c.green(' ║ 🌿 SYNCRBX v' + require('./package.json').version + ' 🌿 ║')));
549
605
  console.log(c.bold(c.green(' ╚══════════════════════════════════════════╝')));
550
606
  console.log('');
551
607
  logServer(`Server at ${c.bold(`${HOST}:${PORT}`)}`);
552
608
  logServer(`Project: ${c.cyan(projectConfig.name)}`);
553
609
  logServer(`Monitoring: ${c.cyan(PROJECT_ROOT)}`);
610
+ if (projectConfig.services) {
611
+ logServer(`Syncing: ${c.cyan(projectConfig.services.join(', '))}`);
612
+ } else {
613
+ logServer(`No services chosen yet: pick them in the Studio plugin when you connect.`);
614
+ }
554
615
  logServer(`Waiting for connection from Roblox Studio...`);
555
616
  console.log('');
556
617
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "syncrbx",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "A free tool to sync VS Code with Roblox Studio. Instant bidirectional sync, SyncRbx Cloud, and Auto-Folders.",
5
5
  "main": "local-sync.js",
6
6
  "bin": {