@fmode/studio 0.0.2 → 0.0.5
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/bin/fmode.js +511 -145
- package/package.json +2 -6
package/bin/fmode.js
CHANGED
|
@@ -7,23 +7,28 @@
|
|
|
7
7
|
* Features:
|
|
8
8
|
* - Version checking against repos.fmode.cn
|
|
9
9
|
* - Automatic binary download and caching
|
|
10
|
-
* -
|
|
11
|
-
* -
|
|
10
|
+
* - Process management (start/stop)
|
|
11
|
+
* - Cache clearing
|
|
12
|
+
* - Smart upgrade with version detection
|
|
12
13
|
*
|
|
13
14
|
* Usage:
|
|
14
15
|
* npx @fmode/studio
|
|
15
|
-
* npx @fmode/studio
|
|
16
|
-
* npx @fmode/studio
|
|
17
|
-
* fmode
|
|
16
|
+
* npx @fmode/studio start
|
|
17
|
+
* npx @fmode/studio stop
|
|
18
|
+
* npx @fmode/studio clear
|
|
19
|
+
* npx @fmode/studio upgrade
|
|
18
20
|
*/
|
|
19
21
|
|
|
20
|
-
import { spawn } from 'child_process';
|
|
22
|
+
import { spawn, exec } from 'child_process';
|
|
21
23
|
import { fileURLToPath } from 'url';
|
|
22
24
|
import { dirname, join } from 'path';
|
|
23
|
-
import { existsSync, mkdirSync, chmodSync, renameSync } from 'fs';
|
|
25
|
+
import { existsSync, mkdirSync, chmodSync, renameSync, rmSync } from 'fs';
|
|
24
26
|
import { createReadStream, createWriteStream } from 'fs';
|
|
25
27
|
import { pipeline } from 'stream/promises';
|
|
26
|
-
import { unlink, readFile } from 'fs/promises';
|
|
28
|
+
import { unlink, readFile, writeFile, readdir } from 'fs/promises';
|
|
29
|
+
import { promisify } from 'util';
|
|
30
|
+
|
|
31
|
+
const execAsync = promisify(exec);
|
|
27
32
|
|
|
28
33
|
const __filename = fileURLToPath(import.meta.url);
|
|
29
34
|
const __dirname = dirname(__filename);
|
|
@@ -33,6 +38,10 @@ const rootDir = dirname(__dirname);
|
|
|
33
38
|
const REPOS_BASE_URL = 'https://repos.fmode.cn/x/fmode-studio';
|
|
34
39
|
const MANIFEST_URL = `${REPOS_BASE_URL}/manifest.json`;
|
|
35
40
|
const CACHE_DIR = join(rootDir, '.cache', 'binaries');
|
|
41
|
+
const PID_DIR = join(rootDir, '.cache', 'pids');
|
|
42
|
+
const PID_FILE = join(PID_DIR, 'fmode.pid');
|
|
43
|
+
const LOG_DIR = join(rootDir, '.cache', 'logs');
|
|
44
|
+
const LOG_FILE = join(LOG_DIR, 'fmode.log');
|
|
36
45
|
|
|
37
46
|
// Platform detection
|
|
38
47
|
const platform = process.platform;
|
|
@@ -145,86 +154,463 @@ async function downloadBinary(version, executableName) {
|
|
|
145
154
|
}
|
|
146
155
|
}
|
|
147
156
|
|
|
148
|
-
//
|
|
149
|
-
async function
|
|
150
|
-
const
|
|
151
|
-
const manifest = await fetchRemoteManifest();
|
|
157
|
+
// Get executable path (cached or bundled)
|
|
158
|
+
async function getExecutablePath() {
|
|
159
|
+
const executableName = getExecutableName();
|
|
152
160
|
|
|
153
|
-
|
|
154
|
-
|
|
161
|
+
// Priority 1: Try cached binary
|
|
162
|
+
const cachedPath = join(CACHE_DIR, executableName);
|
|
163
|
+
if (existsSync(cachedPath)) {
|
|
164
|
+
return cachedPath;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Priority 2: Try bundled binary
|
|
168
|
+
const bundledPath = join(rootDir, 'dist', 'bin', executableName);
|
|
169
|
+
if (existsSync(bundledPath)) {
|
|
170
|
+
return bundledPath;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Get current binary version with timeout
|
|
177
|
+
async function getBinaryVersion(executablePath) {
|
|
178
|
+
return new Promise((resolve) => {
|
|
179
|
+
let output = '';
|
|
180
|
+
let timedOut = false;
|
|
181
|
+
|
|
182
|
+
const timeout = setTimeout(() => {
|
|
183
|
+
timedOut = true;
|
|
184
|
+
child.kill();
|
|
185
|
+
resolve(null); // Timeout = version unknown
|
|
186
|
+
}, 3000);
|
|
187
|
+
|
|
188
|
+
const child = spawn(executablePath, ['-vvv'], {
|
|
189
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
190
|
+
env: { ...process.env }
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
child.stdout.on('data', (data) => {
|
|
194
|
+
output += data.toString();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
child.on('exit', (code) => {
|
|
198
|
+
clearTimeout(timeout);
|
|
199
|
+
if (timedOut) return;
|
|
200
|
+
|
|
201
|
+
// Parse version from output: "@fmode/studio version x.x.x"
|
|
202
|
+
const match = output.match(/version\s+(\d+\.\d+\.\d+)/);
|
|
203
|
+
if (match) {
|
|
204
|
+
resolve(match[1]);
|
|
205
|
+
} else {
|
|
206
|
+
resolve(null);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
child.on('error', () => {
|
|
211
|
+
clearTimeout(timeout);
|
|
212
|
+
resolve(null);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Save PID to file
|
|
218
|
+
function savePid(pid) {
|
|
219
|
+
if (!existsSync(PID_DIR)) {
|
|
220
|
+
mkdirSync(PID_DIR, { recursive: true });
|
|
221
|
+
}
|
|
222
|
+
writeFile(PID_FILE, String(pid));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Read PID from file
|
|
226
|
+
async function readPid() {
|
|
227
|
+
try {
|
|
228
|
+
const content = await readFile(PID_FILE, 'utf-8');
|
|
229
|
+
return parseInt(content.trim(), 10);
|
|
230
|
+
} catch {
|
|
155
231
|
return null;
|
|
156
232
|
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Check if process is running
|
|
236
|
+
async function isProcessRunning(pid) {
|
|
237
|
+
try {
|
|
238
|
+
if (platform === 'win32') {
|
|
239
|
+
// Windows: 使用 tasklist 检查进程是否存在
|
|
240
|
+
const { stdout } = await execAsync(`tasklist /FI "PID eq ${pid}" /FO CSV`);
|
|
241
|
+
return stdout.includes(String(pid));
|
|
242
|
+
} else {
|
|
243
|
+
// Unix: 使用 process.kill(pid, 0) 检查
|
|
244
|
+
process.kill(pid, 0);
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
} catch {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
157
251
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
252
|
+
// Kill process by PID
|
|
253
|
+
async function killProcess(pid) {
|
|
254
|
+
try {
|
|
255
|
+
if (platform === 'win32') {
|
|
256
|
+
// Windows: 使用 taskkill 强制终止进程
|
|
257
|
+
await execAsync(`taskkill /F /PID ${pid}`);
|
|
258
|
+
return true;
|
|
259
|
+
} else {
|
|
260
|
+
// Unix: 使用 process.kill
|
|
261
|
+
process.kill(pid);
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
} catch {
|
|
265
|
+
return false;
|
|
162
266
|
}
|
|
267
|
+
}
|
|
163
268
|
|
|
164
|
-
|
|
269
|
+
// Find fmode processes
|
|
270
|
+
async function findFmodeProcesses() {
|
|
271
|
+
try {
|
|
272
|
+
const command = platform === 'win32'
|
|
273
|
+
? 'tasklist /FI "IMAGENAME eq fmode*" /FO CSV'
|
|
274
|
+
: 'ps aux | grep -E "fmode|fmode-win|fmode-linux|fmode-macos" | grep -v grep';
|
|
165
275
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
276
|
+
const { stdout } = await execAsync(command);
|
|
277
|
+
return stdout;
|
|
278
|
+
} catch {
|
|
279
|
+
return '';
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Find process by port and return PID
|
|
284
|
+
async function findProcessByPort(port) {
|
|
285
|
+
try {
|
|
286
|
+
let command;
|
|
287
|
+
if (platform === 'win32') {
|
|
288
|
+
// Windows: use netstat to find PID listening on port
|
|
289
|
+
command = `netstat -ano | findstr :${port} | findstr LISTENING`;
|
|
290
|
+
} else {
|
|
291
|
+
// Unix-like: use lsof
|
|
292
|
+
command = `lsof -ti:${port}`;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const { stdout } = await execAsync(command);
|
|
296
|
+
|
|
297
|
+
if (platform === 'win32') {
|
|
298
|
+
// Parse Windows netstat output: format is "protocol local_address foreign_address state pid"
|
|
299
|
+
// Example: TCP 0.0.0.0:16666 0.0.0.0:0 LISTENING 12345
|
|
300
|
+
const lines = stdout.trim().split('\n');
|
|
301
|
+
for (const line of lines) {
|
|
302
|
+
const parts = line.trim().split(/\s+/);
|
|
303
|
+
const pid = parseInt(parts[parts.length - 1], 10);
|
|
304
|
+
if (!isNaN(pid)) {
|
|
305
|
+
return pid;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
} else {
|
|
309
|
+
// lsof -ti returns just the PID
|
|
310
|
+
const pid = parseInt(stdout.trim(), 10);
|
|
311
|
+
if (!isNaN(pid)) {
|
|
312
|
+
return pid;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
171
315
|
return null;
|
|
172
|
-
}
|
|
173
|
-
console.log(`Local version is newer: ${localVersion} > ${manifest.version}`);
|
|
316
|
+
} catch {
|
|
174
317
|
return null;
|
|
175
318
|
}
|
|
176
319
|
}
|
|
177
320
|
|
|
178
|
-
//
|
|
179
|
-
|
|
180
|
-
const
|
|
321
|
+
// Get port from args (default 16666)
|
|
322
|
+
function getPortFromArgs(args) {
|
|
323
|
+
const portIndex = args.indexOf('-p');
|
|
324
|
+
if (portIndex !== -1 && args[portIndex + 1]) {
|
|
325
|
+
return parseInt(args[portIndex + 1], 10);
|
|
326
|
+
}
|
|
327
|
+
const portIndex2 = args.indexOf('--port');
|
|
328
|
+
if (portIndex2 !== -1 && args[portIndex2 + 1]) {
|
|
329
|
+
return parseInt(args[portIndex2 + 1], 10);
|
|
330
|
+
}
|
|
331
|
+
return 16666; // default port
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Save PID with port info
|
|
335
|
+
function savePidWithPort(pid, port) {
|
|
336
|
+
if (!existsSync(PID_DIR)) {
|
|
337
|
+
mkdirSync(PID_DIR, { recursive: true });
|
|
338
|
+
}
|
|
339
|
+
// Save as "pid:port" format
|
|
340
|
+
writeFile(PID_FILE, `${pid}:${port}`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Read PID and port from file
|
|
344
|
+
async function readPidWithPort() {
|
|
345
|
+
try {
|
|
346
|
+
const content = await readFile(PID_FILE, 'utf-8');
|
|
347
|
+
const parts = content.trim().split(':');
|
|
348
|
+
if (parts.length === 2) {
|
|
349
|
+
return { pid: parseInt(parts[0], 10), port: parseInt(parts[1], 10) };
|
|
350
|
+
}
|
|
351
|
+
return { pid: parseInt(content.trim(), 10), port: null };
|
|
352
|
+
} catch {
|
|
353
|
+
return { pid: null, port: null };
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// ============================================================
|
|
358
|
+
// COMMAND: START
|
|
359
|
+
// ============================================================
|
|
360
|
+
/**
|
|
361
|
+
* 启动 Fmode Studio 服务
|
|
362
|
+
* 直接启动二进制文件,输出直接显示在终端
|
|
363
|
+
*/
|
|
364
|
+
async function cmdStart(args) {
|
|
365
|
+
const executablePath = await getExecutablePath();
|
|
366
|
+
|
|
367
|
+
// If no executable, download first
|
|
368
|
+
let finalExecutablePath = executablePath;
|
|
369
|
+
if (!finalExecutablePath) {
|
|
370
|
+
console.log('No local binary found. Downloading...');
|
|
371
|
+
const manifest = await fetchRemoteManifest();
|
|
372
|
+
if (manifest && manifest.version) {
|
|
373
|
+
finalExecutablePath = await downloadBinary(manifest.version, getExecutableName());
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// 如果仍然没有可执行文件,退出
|
|
378
|
+
if (!finalExecutablePath) {
|
|
379
|
+
console.error('No executable found and could not download. Please run "fmode upgrade" first.');
|
|
380
|
+
process.exit(1);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// 直接启动二进制文件,不使用 detached,让输出直接显示在终端
|
|
384
|
+
spawn(finalExecutablePath, args, {
|
|
181
385
|
stdio: 'inherit',
|
|
182
|
-
env: {
|
|
386
|
+
env: {
|
|
387
|
+
...process.env,
|
|
388
|
+
FORCE_COLOR: process.env.FORCE_COLOR || '1',
|
|
389
|
+
TERM: process.env.TERM || 'xterm-256color'
|
|
390
|
+
}
|
|
391
|
+
}).on('exit', (code) => {
|
|
392
|
+
process.exit(code ?? 0);
|
|
183
393
|
});
|
|
394
|
+
}
|
|
184
395
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
396
|
+
// ============================================================
|
|
397
|
+
// COMMAND: STOP
|
|
398
|
+
// ============================================================
|
|
399
|
+
async function cmdStop(args) {
|
|
400
|
+
const port = getPortFromArgs(args);
|
|
401
|
+
const { pid: savedPid, port: savedPort } = await readPidWithPort();
|
|
402
|
+
|
|
403
|
+
// First try to find process by port
|
|
404
|
+
const portPid = await findProcessByPort(port);
|
|
405
|
+
|
|
406
|
+
if (portPid && await isProcessRunning(portPid)) {
|
|
407
|
+
console.log(`Stopping Fmode Studio on port ${port} (PID: ${portPid})...`);
|
|
408
|
+
if (await killProcess(portPid)) {
|
|
409
|
+
console.log('Fmode Studio stopped.');
|
|
410
|
+
await unlink(PID_FILE).catch(() => {});
|
|
411
|
+
return;
|
|
412
|
+
} else {
|
|
413
|
+
console.error('Failed to stop Fmode Studio.');
|
|
414
|
+
process.exit(1);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// If no process found on port, check saved PID
|
|
419
|
+
if (savedPid && await isProcessRunning(savedPid)) {
|
|
420
|
+
console.log(`Stopping Fmode Studio (PID: ${savedPid})...`);
|
|
421
|
+
if (await killProcess(savedPid)) {
|
|
422
|
+
console.log('Fmode Studio stopped.');
|
|
423
|
+
await unlink(PID_FILE).catch(() => {});
|
|
424
|
+
return;
|
|
425
|
+
} else {
|
|
426
|
+
console.error('Failed to stop Fmode Studio.');
|
|
427
|
+
process.exit(1);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Clean up stale PID file
|
|
432
|
+
if (savedPid || portPid) {
|
|
433
|
+
console.log('Process not running. Cleaning up PID file...');
|
|
434
|
+
await unlink(PID_FILE).catch(() => {});
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Try to find any fmode processes
|
|
438
|
+
console.log(`No process found on port ${port}. Searching for Fmode processes...`);
|
|
439
|
+
const processes = await findFmodeProcesses();
|
|
440
|
+
|
|
441
|
+
if (processes.trim()) {
|
|
442
|
+
console.log('\nFound Fmode processes:');
|
|
443
|
+
console.log(processes);
|
|
444
|
+
console.log('\nPlease manually kill the process or use:');
|
|
445
|
+
console.log(platform === 'win32' ? ' taskkill /F /PID <pid>' : ' kill -9 <pid>');
|
|
446
|
+
} else {
|
|
447
|
+
console.log('No Fmode Studio process found running.');
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// ============================================================
|
|
452
|
+
// COMMAND: CLEAR
|
|
453
|
+
// ============================================================
|
|
454
|
+
async function cmdClear() {
|
|
455
|
+
console.log('Clearing cached binaries...');
|
|
456
|
+
|
|
457
|
+
if (!existsSync(CACHE_DIR)) {
|
|
458
|
+
console.log('No cache directory found.');
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
try {
|
|
463
|
+
const files = await readdir(CACHE_DIR);
|
|
464
|
+
let removedCount = 0;
|
|
465
|
+
|
|
466
|
+
for (const file of files) {
|
|
467
|
+
const filePath = join(CACHE_DIR, file);
|
|
468
|
+
try {
|
|
469
|
+
await unlink(filePath);
|
|
470
|
+
console.log(` Removed: ${file}`);
|
|
471
|
+
removedCount++;
|
|
472
|
+
} catch (err) {
|
|
473
|
+
console.warn(` Skipped: ${file} (${err.message})`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
console.log(`\nCleared ${removedCount} file(s).`);
|
|
478
|
+
} catch (error) {
|
|
479
|
+
console.error(`Failed to clear cache: ${error.message}`);
|
|
480
|
+
process.exit(1);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// ============================================================
|
|
485
|
+
// COMMAND: LOG
|
|
486
|
+
// ============================================================
|
|
487
|
+
async function cmdLog(args) {
|
|
488
|
+
// Check for -f or --follow flag
|
|
489
|
+
const followMode = args.includes('-f') || args.includes('--follow');
|
|
490
|
+
const linesIndex = args.indexOf('-n') !== -1 ? args.indexOf('-n') : args.indexOf('--lines');
|
|
491
|
+
const numLines = linesIndex !== -1 ? parseInt(args[linesIndex + 1] || '50', 10) : 50;
|
|
492
|
+
|
|
493
|
+
if (!existsSync(LOG_FILE)) {
|
|
494
|
+
console.log('No log file found. Has the server been started?');
|
|
495
|
+
console.log(`Expected log file: ${LOG_FILE}`);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (followMode) {
|
|
500
|
+
// Follow mode (tail -f)
|
|
501
|
+
console.log(`Following log file: ${LOG_FILE} (Ctrl+C to exit)`);
|
|
502
|
+
console.log(`${'='.repeat(60)}\n`);
|
|
503
|
+
|
|
504
|
+
let tailCmd;
|
|
505
|
+
if (platform === 'win32') {
|
|
506
|
+
// Windows: use Get-Content with -Wait
|
|
507
|
+
tailCmd = spawn('powershell', ['-Command', `Get-Content -Path "${LOG_FILE}" -Wait -Tail 50`], {
|
|
508
|
+
stdio: 'inherit',
|
|
509
|
+
windowsHide: false // 需要窗口显示输出
|
|
510
|
+
});
|
|
511
|
+
} else {
|
|
512
|
+
// Unix: use tail -f
|
|
513
|
+
tailCmd = spawn('tail', ['-n', String(numLines), '-f', LOG_FILE], {
|
|
514
|
+
stdio: 'inherit'
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
tailCmd.on('error', (err) => {
|
|
519
|
+
console.log(`Follow mode not available: ${err.message}`);
|
|
520
|
+
console.log('Showing current log content:');
|
|
521
|
+
showLogContent(numLines);
|
|
189
522
|
});
|
|
190
523
|
|
|
191
|
-
|
|
192
|
-
|
|
524
|
+
// 等待 tail 进程结束(用户按 Ctrl+C)
|
|
525
|
+
await new Promise((resolve) => {
|
|
526
|
+
tailCmd.on('exit', resolve);
|
|
193
527
|
});
|
|
194
|
-
}
|
|
528
|
+
} else {
|
|
529
|
+
showLogContent(numLines);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
async function showLogContent(numLines) {
|
|
534
|
+
const content = await readFile(LOG_FILE, 'utf-8');
|
|
535
|
+
const lines = content.split('\n');
|
|
536
|
+
|
|
537
|
+
if (lines.length <= numLines) {
|
|
538
|
+
console.log(content);
|
|
539
|
+
} else {
|
|
540
|
+
console.log(lines.slice(-numLines).join('\n'));
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// ============================================================
|
|
545
|
+
// COMMAND: UPGRADE
|
|
546
|
+
// ============================================================
|
|
547
|
+
async function cmdUpgrade() {
|
|
548
|
+
console.log('Checking for updates...\n');
|
|
549
|
+
|
|
550
|
+
const manifest = await fetchRemoteManifest();
|
|
551
|
+
|
|
552
|
+
if (!manifest || !manifest.version) {
|
|
553
|
+
console.log('Could not fetch remote version.');
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const remoteVersion = manifest.version;
|
|
558
|
+
console.log(`Remote version: ${remoteVersion}`);
|
|
559
|
+
|
|
560
|
+
const executablePath = await getExecutablePath();
|
|
561
|
+
|
|
562
|
+
// Case 1: No binary found
|
|
563
|
+
if (!executablePath) {
|
|
564
|
+
console.log('No binary found. Downloading latest version...');
|
|
565
|
+
await downloadBinary(remoteVersion, getExecutableName());
|
|
566
|
+
console.log('\nUpgrade complete!');
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
console.log(`Local binary: ${executablePath}`);
|
|
571
|
+
|
|
572
|
+
// Case 2: Check current binary version
|
|
573
|
+
console.log('Checking local binary version...');
|
|
574
|
+
const localVersion = await getBinaryVersion(executablePath);
|
|
575
|
+
|
|
576
|
+
if (localVersion) {
|
|
577
|
+
console.log(`Local version: ${localVersion}`);
|
|
578
|
+
|
|
579
|
+
const comparison = compareVersions(remoteVersion, localVersion);
|
|
580
|
+
|
|
581
|
+
if (comparison > 0) {
|
|
582
|
+
console.log(`Update available: ${localVersion} -> ${remoteVersion}`);
|
|
583
|
+
console.log('Downloading...');
|
|
584
|
+
await downloadBinary(remoteVersion, getExecutableName());
|
|
585
|
+
console.log('\nUpgrade complete!');
|
|
586
|
+
} else if (comparison === 0) {
|
|
587
|
+
console.log('Already up to date.');
|
|
588
|
+
} else {
|
|
589
|
+
console.log('Local version is newer.');
|
|
590
|
+
}
|
|
591
|
+
} else {
|
|
592
|
+
// Timeout or error getting version - download latest
|
|
593
|
+
console.log('Could not determine local version (timeout or error)');
|
|
594
|
+
console.log('Downloading latest version to ensure compatibility...');
|
|
595
|
+
await downloadBinary(remoteVersion, getExecutableName());
|
|
596
|
+
console.log('\nUpgrade complete!');
|
|
597
|
+
}
|
|
195
598
|
}
|
|
196
599
|
|
|
197
|
-
//
|
|
600
|
+
// ============================================================
|
|
601
|
+
// MAIN ENTRY POINT
|
|
602
|
+
// ============================================================
|
|
198
603
|
async function main() {
|
|
199
604
|
const args = process.argv.slice(2);
|
|
200
605
|
|
|
201
|
-
// Handle -y flag (auto-confirm
|
|
606
|
+
// Handle -y flag (auto-confirm)
|
|
202
607
|
const yesIndex = args.indexOf('-y');
|
|
203
608
|
if (yesIndex !== -1) {
|
|
204
609
|
args.splice(yesIndex, 1);
|
|
205
610
|
}
|
|
206
611
|
|
|
207
|
-
// Handle
|
|
208
|
-
if (args
|
|
209
|
-
console.log('Checking for updates...');
|
|
210
|
-
const newVersion = await checkForUpdates();
|
|
211
|
-
|
|
212
|
-
if (newVersion) {
|
|
213
|
-
const executableName = getExecutableName();
|
|
214
|
-
console.log(`\nDownloading version ${newVersion}...`);
|
|
215
|
-
try {
|
|
216
|
-
await downloadBinary(newVersion, executableName);
|
|
217
|
-
console.log('\n✓ Update complete!');
|
|
218
|
-
} catch (error) {
|
|
219
|
-
console.error(`\n✗ Download failed: ${error.message}`);
|
|
220
|
-
process.exit(1);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
process.exit(0);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Handle --version flag
|
|
227
|
-
if (args.includes('--version') || args.includes('-v')) {
|
|
612
|
+
// Handle --version flag FIRST (before switch)
|
|
613
|
+
if (args.includes('--version') || (args.includes('-v') && !args.includes('-vvv'))) {
|
|
228
614
|
const localVersion = await getLocalVersion();
|
|
229
615
|
if (localVersion) {
|
|
230
616
|
console.log(`@fmode/studio v${localVersion}`);
|
|
@@ -232,114 +618,94 @@ async function main() {
|
|
|
232
618
|
console.log('@fmode/studio (version unknown)');
|
|
233
619
|
}
|
|
234
620
|
process.exit(0);
|
|
621
|
+
return;
|
|
235
622
|
}
|
|
236
623
|
|
|
237
|
-
// Handle --help flag
|
|
624
|
+
// Handle --help flag FIRST (before switch)
|
|
238
625
|
if (args.includes('--help') || (args.includes('-h') && !args.includes('--host'))) {
|
|
239
626
|
console.log(`
|
|
240
627
|
Fmode Code UI Server
|
|
241
628
|
|
|
242
629
|
USAGE:
|
|
243
|
-
fmode [OPTIONS]
|
|
244
|
-
npx @fmode/studio [OPTIONS]
|
|
245
|
-
|
|
630
|
+
fmode <COMMAND> [OPTIONS]
|
|
631
|
+
npx @fmode/studio <COMMAND> [OPTIONS]
|
|
632
|
+
|
|
633
|
+
COMMANDS:
|
|
634
|
+
start Start the Fmode Studio server (default)
|
|
635
|
+
stop Stop the running Fmode Studio server
|
|
636
|
+
log View server logs
|
|
637
|
+
clear Clear cached binary files
|
|
638
|
+
upgrade / update Upgrade to the latest version
|
|
246
639
|
|
|
247
640
|
OPTIONS:
|
|
248
|
-
-p, --port <PORT>
|
|
249
|
-
-h, --host <HOST>
|
|
250
|
-
--help
|
|
251
|
-
--version, -v
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
PORT Server port (default: 6666)
|
|
257
|
-
HOST Server host (default: 0.0.0.0)
|
|
258
|
-
VITE_IS_PLATFORM Set to 'true' for platform mode
|
|
641
|
+
-p, --port <PORT> Set the server port (default: 16666)
|
|
642
|
+
-h, --host <HOST> Set the server host (default: 0.0.0.0)
|
|
643
|
+
--help Show this help message
|
|
644
|
+
--version, -v Show version information
|
|
645
|
+
|
|
646
|
+
LOG OPTIONS:
|
|
647
|
+
-f, --follow Follow log output (like tail -f)
|
|
648
|
+
-n, --lines <NUM> Number of lines to show (default: 50)
|
|
259
649
|
|
|
260
650
|
EXAMPLES:
|
|
261
|
-
fmode
|
|
262
|
-
fmode
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
fmode
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
651
|
+
fmode # Start with default settings
|
|
652
|
+
fmode start # Explicitly start the server
|
|
653
|
+
fmode start --port 8080 # Start on port 8080
|
|
654
|
+
fmode --port 8080 # Start on port 8080 (same as above)
|
|
655
|
+
fmode stop # Stop the server
|
|
656
|
+
fmode stop --port 8080 # Stop server on port 8080
|
|
657
|
+
fmode log # Show recent logs
|
|
658
|
+
fmode log -f # Follow logs in real-time
|
|
659
|
+
fmode log -n 100 # Show last 100 log lines
|
|
660
|
+
fmode clear # Clear cached binaries
|
|
661
|
+
fmode upgrade # Upgrade to latest version
|
|
662
|
+
|
|
663
|
+
UPGRADE MECHANISM:
|
|
664
|
+
The CLI automatically downloads the latest binary from repos.fmode.cn.
|
|
665
|
+
Use 'fmode upgrade' to manually upgrade.
|
|
270
666
|
`);
|
|
271
667
|
process.exit(0);
|
|
668
|
+
return;
|
|
272
669
|
}
|
|
273
670
|
|
|
274
|
-
const
|
|
275
|
-
let executablePath = null;
|
|
276
|
-
|
|
277
|
-
// Priority 1: Try cached binary (most recent download)
|
|
278
|
-
const cachedPath = join(CACHE_DIR, executableName);
|
|
279
|
-
if (existsSync(cachedPath)) {
|
|
280
|
-
executablePath = cachedPath;
|
|
281
|
-
console.log(`Using cached binary: ${cachedPath}`);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// Priority 2: Try bundled binary (shipped with npm package)
|
|
285
|
-
if (!executablePath) {
|
|
286
|
-
const bundledPath = join(rootDir, 'dist', 'bin', executableName);
|
|
287
|
-
if (existsSync(bundledPath)) {
|
|
288
|
-
executablePath = bundledPath;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
671
|
+
const command = args[0];
|
|
291
672
|
|
|
292
|
-
//
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
if (manifest && manifest.version) {
|
|
298
|
-
executablePath = await downloadBinary(manifest.version, executableName);
|
|
299
|
-
}
|
|
300
|
-
} catch (error) {
|
|
301
|
-
console.warn(`Could not download binary: ${error.message}`);
|
|
302
|
-
}
|
|
303
|
-
}
|
|
673
|
+
// Handle commands
|
|
674
|
+
switch (command) {
|
|
675
|
+
case 'start':
|
|
676
|
+
await cmdStart(args.slice(1));
|
|
677
|
+
return;
|
|
304
678
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
await runExecutable(executablePath, args);
|
|
679
|
+
case 'stop':
|
|
680
|
+
await cmdStop(args.slice(1));
|
|
681
|
+
process.exit(0);
|
|
309
682
|
return;
|
|
310
|
-
} catch (err) {
|
|
311
|
-
console.error('Failed to start native executable:', err.message);
|
|
312
|
-
// Fall through to Node.js/Bun fallback
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
683
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
684
|
+
case 'log':
|
|
685
|
+
await cmdLog(args.slice(1));
|
|
686
|
+
process.exit(0);
|
|
687
|
+
return;
|
|
319
688
|
|
|
320
|
-
|
|
321
|
-
|
|
689
|
+
case 'clear':
|
|
690
|
+
await cmdClear();
|
|
691
|
+
process.exit(0);
|
|
692
|
+
return;
|
|
322
693
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
env: { ...process.env }
|
|
328
|
-
});
|
|
694
|
+
case 'upgrade':
|
|
695
|
+
await cmdUpgrade();
|
|
696
|
+
process.exit(0);
|
|
697
|
+
return;
|
|
329
698
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
699
|
+
case 'update':
|
|
700
|
+
// Legacy command - same as upgrade
|
|
701
|
+
await cmdUpgrade();
|
|
702
|
+
process.exit(0);
|
|
703
|
+
return;
|
|
333
704
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
});
|
|
339
|
-
} else {
|
|
340
|
-
console.error(`Error: Fmode Studio not found for platform ${platform} ${arch}`);
|
|
341
|
-
console.error('Please reinstall the package or report this issue.');
|
|
342
|
-
process.exit(1);
|
|
705
|
+
default:
|
|
706
|
+
// Default: start the server with all args (no command specified)
|
|
707
|
+
await cmdStart(args);
|
|
708
|
+
return;
|
|
343
709
|
}
|
|
344
710
|
}
|
|
345
711
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fmode/studio",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "AI PaaS IDE for Vibe Coding - Cross-platform CLI tool",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"author": "Fmode Studio Team",
|
|
@@ -28,9 +28,5 @@
|
|
|
28
28
|
"x64",
|
|
29
29
|
"arm64"
|
|
30
30
|
],
|
|
31
|
-
"dependencies": {
|
|
32
|
-
"@aws-sdk/client-s3": "^3.965.0",
|
|
33
|
-
"@siteboon/claude-code-ui": "^1.13.6",
|
|
34
|
-
"bun-pty": "^0.4.6"
|
|
35
|
-
}
|
|
31
|
+
"dependencies": {}
|
|
36
32
|
}
|