syncrbx 1.0.0 → 1.1.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.
- package/cli.js +459 -322
- package/converters.js +27 -1
- package/local-sync.js +570 -441
- package/package.json +26 -20
- package/build.js +0 -77
- package/make_rbxmx.js +0 -21
- package/test.js +0 -28
package/cli.js
CHANGED
|
@@ -1,322 +1,459 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
const { Command } = require('commander');
|
|
4
|
-
const fs = require('fs');
|
|
5
|
-
const path = require('path');
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const {
|
|
11
|
-
|
|
12
|
-
const program = new Command();
|
|
13
|
-
|
|
14
|
-
const API_BASE_URL = process.env.SYNCRBX_API_URL || '
|
|
15
|
-
const WEB_BASE_URL = process.env.SYNCRBX_WEB_URL || 'https://www.syncrbx.xyz';
|
|
16
|
-
|
|
17
|
-
const CONFIG_PATH = path.join(process.env.HOME || process.env.USERPROFILE, '.syncrbxconfig.json');
|
|
18
|
-
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { Command } = require('commander');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
const archiver = require('archiver');
|
|
8
|
+
const AdmZip = require('adm-zip');
|
|
9
|
+
const http = require('http');
|
|
10
|
+
const { spawn } = require('child_process');
|
|
11
|
+
|
|
12
|
+
const program = new Command();
|
|
13
|
+
|
|
14
|
+
const API_BASE_URL = process.env.SYNCRBX_API_URL || 'https://syncrbxbot.onrender.com/api/cli';
|
|
15
|
+
const WEB_BASE_URL = process.env.SYNCRBX_WEB_URL || 'https://www.syncrbx.xyz';
|
|
16
|
+
|
|
17
|
+
const CONFIG_PATH = path.join(process.env.HOME || process.env.USERPROFILE, '.syncrbxconfig.json');
|
|
18
|
+
|
|
19
|
+
// Refresh the access token when it expires in less than this many seconds.
|
|
20
|
+
const TOKEN_REFRESH_MARGIN_S = 60;
|
|
21
|
+
|
|
22
|
+
// --- Helper Functions ---
|
|
23
|
+
|
|
24
|
+
function forceExit(code) {
|
|
25
|
+
setTimeout(() => {
|
|
26
|
+
process.exit(code);
|
|
27
|
+
}, 50);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readCredentials() {
|
|
31
|
+
if (!fs.existsSync(CONFIG_PATH)) return null;
|
|
32
|
+
try {
|
|
33
|
+
return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
|
34
|
+
} catch (e) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function saveCredentials({ access_token, refresh_token }) {
|
|
40
|
+
const data = { access_token };
|
|
41
|
+
if (refresh_token) data.refresh_token = refresh_token;
|
|
42
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getTokenExpiry(token) {
|
|
46
|
+
try {
|
|
47
|
+
const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString('utf8'));
|
|
48
|
+
return typeof payload.exp === 'number' ? payload.exp : null;
|
|
49
|
+
} catch (e) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Returns a usable access token, refreshing it through the API when it is
|
|
55
|
+
// about to expire. Returns null when the user has to log in again.
|
|
56
|
+
async function getToken() {
|
|
57
|
+
const credentials = readCredentials();
|
|
58
|
+
if (!credentials || !credentials.access_token) return null;
|
|
59
|
+
|
|
60
|
+
const exp = getTokenExpiry(credentials.access_token);
|
|
61
|
+
const expiresSoon = exp !== null && exp - Date.now() / 1000 < TOKEN_REFRESH_MARGIN_S;
|
|
62
|
+
if (!expiresSoon) return credentials.access_token;
|
|
63
|
+
|
|
64
|
+
if (!credentials.refresh_token) {
|
|
65
|
+
console.error('❌ Your session has expired. Run "syncrbx login" again.');
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const res = await fetch(`${API_BASE_URL}/refresh`, {
|
|
71
|
+
method: 'POST',
|
|
72
|
+
headers: { 'Content-Type': 'application/json' },
|
|
73
|
+
body: JSON.stringify({ refresh_token: credentials.refresh_token }),
|
|
74
|
+
});
|
|
75
|
+
const data = await res.json().catch(() => ({}));
|
|
76
|
+
if (!res.ok || !data.access_token) {
|
|
77
|
+
console.error('❌ Your session has expired. Run "syncrbx login" again.');
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
saveCredentials(data);
|
|
81
|
+
return data.access_token;
|
|
82
|
+
} catch (err) {
|
|
83
|
+
console.error(`❌ Could not refresh your session: ${err.message}`);
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Trades the browser's access token for a session that belongs to the CLI, so
|
|
89
|
+
// both can refresh independently. Returns null if the API does not support it.
|
|
90
|
+
async function createCliSession(webAccessToken) {
|
|
91
|
+
try {
|
|
92
|
+
const res = await fetch(`${API_BASE_URL}/session`, {
|
|
93
|
+
method: 'POST',
|
|
94
|
+
headers: { 'Authorization': webAccessToken },
|
|
95
|
+
});
|
|
96
|
+
const data = await res.json().catch(() => ({}));
|
|
97
|
+
return res.ok && data.access_token ? data : null;
|
|
98
|
+
} catch (err) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function getProjectConfig() {
|
|
104
|
+
const localSyncRbx = path.join(process.cwd(), '.syncrbx', 'config.json');
|
|
105
|
+
if (fs.existsSync(localSyncRbx)) {
|
|
106
|
+
return JSON.parse(fs.readFileSync(localSyncRbx, 'utf8'));
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function openBrowser(url) {
|
|
112
|
+
// Spawn without a shell so the "&" in the query string is not treated as a
|
|
113
|
+
// command separator.
|
|
114
|
+
const [command, args] = process.platform === 'darwin' ? ['open', [url]]
|
|
115
|
+
: process.platform === 'win32' ? ['rundll32', ['url.dll,FileProtocolHandler', url]]
|
|
116
|
+
: ['xdg-open', [url]];
|
|
117
|
+
try {
|
|
118
|
+
const child = spawn(command, args, { stdio: 'ignore', detached: true });
|
|
119
|
+
child.on('error', () => { /* the URL is also printed to the terminal */ });
|
|
120
|
+
child.unref();
|
|
121
|
+
} catch (e) {
|
|
122
|
+
// the URL is also printed to the terminal
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function getAllowedWebOrigins() {
|
|
127
|
+
const origin = new URL(WEB_BASE_URL).origin;
|
|
128
|
+
const origins = new Set([origin]);
|
|
129
|
+
const url = new URL(origin);
|
|
130
|
+
url.hostname = url.hostname.startsWith('www.') ? url.hostname.slice(4) : `www.${url.hostname}`;
|
|
131
|
+
origins.add(url.origin);
|
|
132
|
+
return origins;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// --- Commands ---
|
|
136
|
+
|
|
137
|
+
program
|
|
138
|
+
.name('syncrbx')
|
|
139
|
+
.description('SyncRbx - A free tool to sync VS Code with Roblox Studio.')
|
|
140
|
+
.version(require('./package.json').version);
|
|
141
|
+
|
|
142
|
+
// Command: Login
|
|
143
|
+
program
|
|
144
|
+
.command('login')
|
|
145
|
+
.description('Authenticate with your SyncRbx account')
|
|
146
|
+
.option('-t, --token <token>', 'Provide the token directly')
|
|
147
|
+
.action((options) => {
|
|
148
|
+
if (options.token) {
|
|
149
|
+
saveCredentials({ access_token: options.token });
|
|
150
|
+
console.log('✅ Successfully logged in to SyncRbx.');
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const PORT = 14872;
|
|
155
|
+
// Random value that the web page must echo back, so other pages cannot
|
|
156
|
+
// push their own token into the CLI while it is waiting.
|
|
157
|
+
const state = crypto.randomBytes(24).toString('hex');
|
|
158
|
+
const loginUrl = `${WEB_BASE_URL}/cli-login?port=${PORT}&state=${state}`;
|
|
159
|
+
const allowedOrigins = getAllowedWebOrigins();
|
|
160
|
+
|
|
161
|
+
console.log(`🔗 Opening the browser to authenticate...`);
|
|
162
|
+
console.log(`If the browser does not open automatically, click here: ${loginUrl}`);
|
|
163
|
+
|
|
164
|
+
const server = http.createServer((req, res) => {
|
|
165
|
+
const url = new URL(req.url, `http://localhost:${PORT}`);
|
|
166
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
167
|
+
if (req.headers.origin && allowedOrigins.has(req.headers.origin)) {
|
|
168
|
+
headers['Access-Control-Allow-Origin'] = req.headers.origin;
|
|
169
|
+
headers['Vary'] = 'Origin';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (url.pathname !== '/callback') {
|
|
173
|
+
res.writeHead(404);
|
|
174
|
+
res.end();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const receivedState = url.searchParams.get('state') || '';
|
|
179
|
+
const stateMatches = receivedState.length === state.length
|
|
180
|
+
&& crypto.timingSafeEqual(Buffer.from(receivedState), Buffer.from(state));
|
|
181
|
+
if (!stateMatches) {
|
|
182
|
+
res.writeHead(403, headers);
|
|
183
|
+
res.end(JSON.stringify({ error: 'Invalid state' }));
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const token = url.searchParams.get('token');
|
|
188
|
+
if (!token) {
|
|
189
|
+
res.writeHead(400, headers);
|
|
190
|
+
res.end(JSON.stringify({ error: 'Token missing' }));
|
|
191
|
+
console.error('❌ Error: No token received.');
|
|
192
|
+
server.close(() => forceExit(1));
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
res.writeHead(200, headers);
|
|
197
|
+
res.end(JSON.stringify({ success: true }));
|
|
198
|
+
server.close();
|
|
199
|
+
|
|
200
|
+
createCliSession(token).then(session => {
|
|
201
|
+
saveCredentials(session || { access_token: token });
|
|
202
|
+
if (!session) {
|
|
203
|
+
console.log('⚠️ This login will expire in about an hour. Run "syncrbx login" again when it does.');
|
|
204
|
+
}
|
|
205
|
+
console.log('✅ Successfully logged in. You can now return to the terminal.');
|
|
206
|
+
forceExit(0);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
server.listen(PORT, '127.0.0.1', () => {
|
|
211
|
+
openBrowser(loginUrl);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Command: Serve
|
|
216
|
+
program
|
|
217
|
+
.command('serve')
|
|
218
|
+
.description('Start the local SyncRbx server')
|
|
219
|
+
.action(() => {
|
|
220
|
+
// Loaded lazily: requiring local-sync starts the file watcher.
|
|
221
|
+
require('./local-sync').startServer();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Command: Init
|
|
225
|
+
program
|
|
226
|
+
.command('init')
|
|
227
|
+
.description('Initialize a new SyncRbx repository in the cloud')
|
|
228
|
+
.option('--private', 'Make the repository private')
|
|
229
|
+
.option('--public', 'Make the repository public (default)')
|
|
230
|
+
.action(async (options) => {
|
|
231
|
+
const token = await getToken();
|
|
232
|
+
if (!token) {
|
|
233
|
+
console.error('❌ You are not authenticated. Use "syncrbx login" first.');
|
|
234
|
+
forceExit(1);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const srcPath = path.join(process.cwd(), 'src');
|
|
239
|
+
const rojoConfig = path.join(process.cwd(), 'default.project.json');
|
|
240
|
+
|
|
241
|
+
if (!fs.existsSync(srcPath) && !fs.existsSync(rojoConfig)) {
|
|
242
|
+
console.log('🚧 Roblox structure not detected. Creating scaffolding (src/ and config.json)...');
|
|
243
|
+
fs.mkdirSync(srcPath, { recursive: true });
|
|
244
|
+
|
|
245
|
+
const configJson = {
|
|
246
|
+
name: path.basename(process.cwd()),
|
|
247
|
+
version: "1.0.0",
|
|
248
|
+
engine: "roblox",
|
|
249
|
+
type: "syncrbx-project"
|
|
250
|
+
};
|
|
251
|
+
fs.writeFileSync(path.join(srcPath, 'config.json'), JSON.stringify(configJson, null, 2));
|
|
252
|
+
console.log('✅ Base structure created.');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const isPrivate = options.private ? true : false;
|
|
256
|
+
const projectName = path.basename(process.cwd());
|
|
257
|
+
|
|
258
|
+
console.log(`🚀 Initializing project "${projectName}" as ${isPrivate ? 'private' : 'public'}...`);
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
const res = await fetch(`${API_BASE_URL}/init`, {
|
|
262
|
+
method: 'POST',
|
|
263
|
+
headers: {
|
|
264
|
+
'Content-Type': 'application/json',
|
|
265
|
+
'Authorization': token
|
|
266
|
+
},
|
|
267
|
+
body: JSON.stringify({ projectName, isPrivate })
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const data = await res.json();
|
|
271
|
+
|
|
272
|
+
if (!res.ok) {
|
|
273
|
+
console.error(`❌ Error initializing: ${data.error}`);
|
|
274
|
+
forceExit(1);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const syncrbxFolder = path.join(process.cwd(), '.syncrbx');
|
|
279
|
+
if (!fs.existsSync(syncrbxFolder)) fs.mkdirSync(syncrbxFolder);
|
|
280
|
+
fs.writeFileSync(path.join(syncrbxFolder, 'config.json'), JSON.stringify({ project_id: data.id, owner_id: data.owner_id }, null, 2));
|
|
281
|
+
|
|
282
|
+
console.log('✅ Project initialized successfully and linked to the cloud.');
|
|
283
|
+
forceExit(0);
|
|
284
|
+
} catch (err) {
|
|
285
|
+
console.error(`❌ API connection failed: ${err.message}`);
|
|
286
|
+
forceExit(1);
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// Command: Push
|
|
291
|
+
program
|
|
292
|
+
.command('push')
|
|
293
|
+
.description('Package your code and push a commit to the cloud')
|
|
294
|
+
.argument('<message>', 'Commit message')
|
|
295
|
+
.action(async (message) => {
|
|
296
|
+
const token = await getToken();
|
|
297
|
+
if (!token) {
|
|
298
|
+
console.error('❌ You are not authenticated. Use "syncrbx login" first.');
|
|
299
|
+
forceExit(1);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const projectConfig = getProjectConfig();
|
|
304
|
+
if (!projectConfig || !projectConfig.project_id) {
|
|
305
|
+
console.error('❌ This directory is not linked to SyncRbx Cloud. Run "syncrbx init" first.');
|
|
306
|
+
forceExit(1);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
console.log(`📦 Packaging local code...`);
|
|
311
|
+
const zipPath = path.join(process.cwd(), '.syncrbx_build.zip');
|
|
312
|
+
const output = fs.createWriteStream(zipPath);
|
|
313
|
+
const archive = archiver('zip', { zlib: { level: 9 } });
|
|
314
|
+
|
|
315
|
+
output.on('close', async () => {
|
|
316
|
+
const stats = fs.statSync(zipPath);
|
|
317
|
+
const fileSizeInMB = stats.size / (1024 * 1024);
|
|
318
|
+
|
|
319
|
+
if (fileSizeInMB > 10) {
|
|
320
|
+
console.error(`❌ The packaged file is too large (${fileSizeInMB.toFixed(2)} MB). 10 MB limit exceeded.`);
|
|
321
|
+
fs.unlinkSync(zipPath);
|
|
322
|
+
forceExit(1);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
console.log(`✅ Compressed file (${stats.size} bytes).`);
|
|
327
|
+
console.log(`☁️ Pushing to SyncRbx Cloud...`);
|
|
328
|
+
|
|
329
|
+
let exitCode = 1;
|
|
330
|
+
try {
|
|
331
|
+
const zipBuffer = fs.readFileSync(zipPath);
|
|
332
|
+
const res = await fetch(`${API_BASE_URL}/push`, {
|
|
333
|
+
method: 'POST',
|
|
334
|
+
headers: {
|
|
335
|
+
'Authorization': token,
|
|
336
|
+
'Content-Type': 'application/zip',
|
|
337
|
+
'x-project-id': projectConfig.project_id,
|
|
338
|
+
// HTTP headers only carry Latin-1, so accents and emoji
|
|
339
|
+
// are percent-encoded.
|
|
340
|
+
'x-commit-message': encodeURIComponent(message),
|
|
341
|
+
'x-commit-message-encoding': 'uri'
|
|
342
|
+
},
|
|
343
|
+
body: zipBuffer
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
const data = await res.json().catch(() => ({ error: `HTTP ${res.status}` }));
|
|
347
|
+
if (!res.ok) {
|
|
348
|
+
console.error(`❌ Error pushing: ${data.error}`);
|
|
349
|
+
} else {
|
|
350
|
+
console.log(`🎉 Push successfully completed.`);
|
|
351
|
+
exitCode = 0;
|
|
352
|
+
}
|
|
353
|
+
} catch (err) {
|
|
354
|
+
console.error(`❌ API connection failed: ${err.message}`);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
fs.unlinkSync(zipPath);
|
|
358
|
+
process.exit(exitCode);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
archive.on('error', (err) => {
|
|
362
|
+
console.error(`❌ Error packaging files: ${err.message}`);
|
|
363
|
+
try { fs.unlinkSync(zipPath); } catch (e) { /* already gone */ }
|
|
364
|
+
forceExit(1);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
archive.pipe(output);
|
|
368
|
+
archive.glob('**/*.{lua,luau,json,txt,md,toml}', {
|
|
369
|
+
ignore: ['node_modules/**', '.git/**', '.syncrbx/**', '.syncrbx_build.zip']
|
|
370
|
+
});
|
|
371
|
+
archive.finalize();
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
// Command: Pull
|
|
375
|
+
program
|
|
376
|
+
.command('pull')
|
|
377
|
+
.description('Clone or update a repository from the cloud')
|
|
378
|
+
.argument('<project_id>', 'ID of the repository to clone')
|
|
379
|
+
.option('-f, --force', 'Overwrite local files that differ from the cloud version')
|
|
380
|
+
.action(async (projectId, options) => {
|
|
381
|
+
const token = await getToken();
|
|
382
|
+
if (!token) {
|
|
383
|
+
console.error('❌ You are not authenticated. Use "syncrbx login" first.');
|
|
384
|
+
forceExit(1);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
console.log(`🔍 Downloading repository with ID: ${projectId}...`);
|
|
389
|
+
|
|
390
|
+
try {
|
|
391
|
+
const res = await fetch(`${API_BASE_URL}/pull/${encodeURIComponent(projectId)}`, {
|
|
392
|
+
method: 'GET',
|
|
393
|
+
headers: {
|
|
394
|
+
'Authorization': token
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
if (!res.ok) {
|
|
399
|
+
const errorData = await res.json().catch(() => ({ error: 'Unknown error' }));
|
|
400
|
+
console.error(`❌ Error: ${errorData.error}`);
|
|
401
|
+
forceExit(1);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const arrayBuffer = await res.arrayBuffer();
|
|
406
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
407
|
+
const ownerId = res.headers.get('x-owner-id') || 'unknown';
|
|
408
|
+
|
|
409
|
+
let zip;
|
|
410
|
+
try {
|
|
411
|
+
zip = new AdmZip(buffer);
|
|
412
|
+
} catch (e) {
|
|
413
|
+
console.error(`❌ Error reading zip file: ${e.message}`);
|
|
414
|
+
forceExit(1);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (!options.force) {
|
|
419
|
+
const overwritten = zip.getEntries()
|
|
420
|
+
.filter(entry => !entry.isDirectory)
|
|
421
|
+
.filter(entry => {
|
|
422
|
+
const localPath = path.join(process.cwd(), entry.entryName);
|
|
423
|
+
if (!fs.existsSync(localPath)) return false;
|
|
424
|
+
return !fs.statSync(localPath).isFile() || !fs.readFileSync(localPath).equals(entry.getData());
|
|
425
|
+
})
|
|
426
|
+
.map(entry => entry.entryName);
|
|
427
|
+
|
|
428
|
+
if (overwritten.length > 0) {
|
|
429
|
+
console.error(`❌ Pull would overwrite ${overwritten.length} local file(s) with different content:`);
|
|
430
|
+
for (const name of overwritten.slice(0, 10)) console.error(` - ${name}`);
|
|
431
|
+
if (overwritten.length > 10) console.error(` ...and ${overwritten.length - 10} more`);
|
|
432
|
+
console.error('Run "syncrbx pull <project_id> --force" to overwrite them.');
|
|
433
|
+
forceExit(1);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
console.log(`📂 Extracting files...`);
|
|
439
|
+
try {
|
|
440
|
+
zip.extractAllTo(process.cwd(), true);
|
|
441
|
+
} catch (e) {
|
|
442
|
+
console.error(`❌ Error extracting zip file: ${e.message}`);
|
|
443
|
+
forceExit(1);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const syncrbxFolder = path.join(process.cwd(), '.syncrbx');
|
|
448
|
+
if (!fs.existsSync(syncrbxFolder)) fs.mkdirSync(syncrbxFolder);
|
|
449
|
+
fs.writeFileSync(path.join(syncrbxFolder, 'config.json'), JSON.stringify({ project_id: projectId, owner_id: ownerId }, null, 2));
|
|
450
|
+
|
|
451
|
+
console.log(`🎉 Pull completed successfully. The directory is now synchronized.`);
|
|
452
|
+
forceExit(0);
|
|
453
|
+
} catch (err) {
|
|
454
|
+
console.error(`❌ API connection failed: ${err.message}`);
|
|
455
|
+
forceExit(1);
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
program.parse(process.argv);
|