@fmode/studio 0.0.5 → 0.0.7
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 +6 -700
- package/package.json +1 -1
package/bin/fmode.js
CHANGED
|
@@ -4,713 +4,19 @@
|
|
|
4
4
|
* Fmode Studio CLI Entry Point
|
|
5
5
|
* npm/npx compatible entry point
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* - Automatic binary download and caching
|
|
10
|
-
* - Process management (start/stop)
|
|
11
|
-
* - Cache clearing
|
|
12
|
-
* - Smart upgrade with version detection
|
|
7
|
+
* This is a thin router that delegates to the modular CLI structure.
|
|
8
|
+
* All command logic is now in cli/ subdirectories.
|
|
13
9
|
*
|
|
14
10
|
* Usage:
|
|
15
11
|
* npx @fmode/studio
|
|
16
12
|
* npx @fmode/studio start
|
|
17
|
-
* npx @fmode/studio
|
|
18
|
-
* npx @fmode/studio clear
|
|
19
|
-
* npx @fmode/studio upgrade
|
|
13
|
+
* npx @fmode/studio server start
|
|
20
14
|
*/
|
|
21
15
|
|
|
22
|
-
import {
|
|
23
|
-
import { fileURLToPath } from 'url';
|
|
24
|
-
import { dirname, join } from 'path';
|
|
25
|
-
import { existsSync, mkdirSync, chmodSync, renameSync, rmSync } from 'fs';
|
|
26
|
-
import { createReadStream, createWriteStream } from 'fs';
|
|
27
|
-
import { pipeline } from 'stream/promises';
|
|
28
|
-
import { unlink, readFile, writeFile, readdir } from 'fs/promises';
|
|
29
|
-
import { promisify } from 'util';
|
|
16
|
+
import { runCli } from '../cli/index.ts';
|
|
30
17
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
34
|
-
const __dirname = dirname(__filename);
|
|
35
|
-
const rootDir = dirname(__dirname);
|
|
36
|
-
|
|
37
|
-
// Configuration
|
|
38
|
-
const REPOS_BASE_URL = 'https://repos.fmode.cn/x/fmode-studio';
|
|
39
|
-
const MANIFEST_URL = `${REPOS_BASE_URL}/manifest.json`;
|
|
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');
|
|
45
|
-
|
|
46
|
-
// Platform detection
|
|
47
|
-
const platform = process.platform;
|
|
48
|
-
const arch = process.arch;
|
|
49
|
-
|
|
50
|
-
// Map platform/arch to executable name
|
|
51
|
-
function getExecutableName() {
|
|
52
|
-
const ext = platform === 'win32' ? '.exe' : '';
|
|
53
|
-
|
|
54
|
-
if (platform === 'win32' && arch === 'x64') {
|
|
55
|
-
return `fmode-win-x64${ext}`;
|
|
56
|
-
} else if (platform === 'win32' && arch === 'arm64') {
|
|
57
|
-
return `fmode-win-arm64${ext}`;
|
|
58
|
-
} else if (platform === 'linux' && arch === 'x64') {
|
|
59
|
-
return 'fmode-linux-x64';
|
|
60
|
-
} else if (platform === 'linux' && arch === 'arm64') {
|
|
61
|
-
return 'fmode-linux-arm64';
|
|
62
|
-
} else if (platform === 'darwin' && arch === 'x64') {
|
|
63
|
-
return 'fmode-macos-x64';
|
|
64
|
-
} else if (platform === 'darwin' && arch === 'arm64') {
|
|
65
|
-
return 'fmode-macos-arm64';
|
|
66
|
-
} else {
|
|
67
|
-
throw new Error(`Unsupported platform: ${platform} ${arch}`);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Get local package version
|
|
72
|
-
async function getLocalVersion() {
|
|
73
|
-
const packageJson = join(rootDir, 'package.json');
|
|
74
|
-
try {
|
|
75
|
-
const content = await readFile(packageJson, 'utf-8');
|
|
76
|
-
const pkg = JSON.parse(content);
|
|
77
|
-
return pkg.version;
|
|
78
|
-
} catch {
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Fetch remote manifest from repos.fmode.cn
|
|
84
|
-
async function fetchRemoteManifest() {
|
|
85
|
-
try {
|
|
86
|
-
const response = await fetch(MANIFEST_URL);
|
|
87
|
-
if (!response.ok) {
|
|
88
|
-
throw new Error(`HTTP ${response.status}`);
|
|
89
|
-
}
|
|
90
|
-
return await response.json();
|
|
91
|
-
} catch (error) {
|
|
92
|
-
console.warn(`Warning: Could not fetch remote manifest: ${error.message}`);
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Compare versions (returns positive if a > b, negative if a < b, 0 if equal)
|
|
98
|
-
function compareVersions(a, b) {
|
|
99
|
-
const partsA = a.split('.').map(Number);
|
|
100
|
-
const partsB = b.split('.').map(Number);
|
|
101
|
-
|
|
102
|
-
for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
|
|
103
|
-
const partA = partsA[i] || 0;
|
|
104
|
-
const partB = partsB[i] || 0;
|
|
105
|
-
if (partA !== partB) {
|
|
106
|
-
return partA - partB;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
return 0;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Download binary from repos.fmode.cn
|
|
113
|
-
async function downloadBinary(version, executableName) {
|
|
114
|
-
const url = `${REPOS_BASE_URL}/${version}/${executableName}`;
|
|
115
|
-
const tempPath = join(CACHE_DIR, `${executableName}.tmp`);
|
|
116
|
-
const finalPath = join(CACHE_DIR, executableName);
|
|
117
|
-
|
|
118
|
-
console.log(`Downloading ${url}...`);
|
|
119
|
-
|
|
120
|
-
try {
|
|
121
|
-
const response = await fetch(url);
|
|
122
|
-
if (!response.ok) {
|
|
123
|
-
throw new Error(`HTTP ${response.status}`);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Ensure cache directory exists
|
|
127
|
-
if (!existsSync(CACHE_DIR)) {
|
|
128
|
-
mkdirSync(CACHE_DIR, { recursive: true });
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Download to temp file
|
|
132
|
-
const fileStream = createWriteStream(tempPath);
|
|
133
|
-
await pipeline(response.body, fileStream);
|
|
134
|
-
|
|
135
|
-
// Make executable (Unix-like systems)
|
|
136
|
-
if (platform !== 'win32') {
|
|
137
|
-
chmodSync(tempPath, 0o755);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Move to final path
|
|
141
|
-
if (existsSync(finalPath)) {
|
|
142
|
-
await unlink(finalPath);
|
|
143
|
-
}
|
|
144
|
-
renameSync(tempPath, finalPath);
|
|
145
|
-
|
|
146
|
-
console.log(`Downloaded to ${finalPath}`);
|
|
147
|
-
return finalPath;
|
|
148
|
-
} catch (error) {
|
|
149
|
-
// Clean up temp file on error
|
|
150
|
-
if (existsSync(tempPath)) {
|
|
151
|
-
await unlink(tempPath).catch(() => {});
|
|
152
|
-
}
|
|
153
|
-
throw error;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Get executable path (cached or bundled)
|
|
158
|
-
async function getExecutablePath() {
|
|
159
|
-
const executableName = getExecutableName();
|
|
160
|
-
|
|
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 {
|
|
231
|
-
return null;
|
|
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
|
-
}
|
|
251
|
-
|
|
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;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
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';
|
|
275
|
-
|
|
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
|
-
}
|
|
315
|
-
return null;
|
|
316
|
-
} catch {
|
|
317
|
-
return null;
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
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, {
|
|
385
|
-
stdio: 'inherit',
|
|
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);
|
|
393
|
-
});
|
|
394
|
-
}
|
|
395
|
-
|
|
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);
|
|
522
|
-
});
|
|
523
|
-
|
|
524
|
-
// 等待 tail 进程结束(用户按 Ctrl+C)
|
|
525
|
-
await new Promise((resolve) => {
|
|
526
|
-
tailCmd.on('exit', resolve);
|
|
527
|
-
});
|
|
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
|
-
}
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
// ============================================================
|
|
601
|
-
// MAIN ENTRY POINT
|
|
602
|
-
// ============================================================
|
|
603
|
-
async function main() {
|
|
604
|
-
const args = process.argv.slice(2);
|
|
605
|
-
|
|
606
|
-
// Handle -y flag (auto-confirm)
|
|
607
|
-
const yesIndex = args.indexOf('-y');
|
|
608
|
-
if (yesIndex !== -1) {
|
|
609
|
-
args.splice(yesIndex, 1);
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
// Handle --version flag FIRST (before switch)
|
|
613
|
-
if (args.includes('--version') || (args.includes('-v') && !args.includes('-vvv'))) {
|
|
614
|
-
const localVersion = await getLocalVersion();
|
|
615
|
-
if (localVersion) {
|
|
616
|
-
console.log(`@fmode/studio v${localVersion}`);
|
|
617
|
-
} else {
|
|
618
|
-
console.log('@fmode/studio (version unknown)');
|
|
619
|
-
}
|
|
620
|
-
process.exit(0);
|
|
621
|
-
return;
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
// Handle --help flag FIRST (before switch)
|
|
625
|
-
if (args.includes('--help') || (args.includes('-h') && !args.includes('--host'))) {
|
|
626
|
-
console.log(`
|
|
627
|
-
Fmode Code UI Server
|
|
628
|
-
|
|
629
|
-
USAGE:
|
|
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
|
|
639
|
-
|
|
640
|
-
OPTIONS:
|
|
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)
|
|
649
|
-
|
|
650
|
-
EXAMPLES:
|
|
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.
|
|
666
|
-
`);
|
|
667
|
-
process.exit(0);
|
|
668
|
-
return;
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
const command = args[0];
|
|
672
|
-
|
|
673
|
-
// Handle commands
|
|
674
|
-
switch (command) {
|
|
675
|
-
case 'start':
|
|
676
|
-
await cmdStart(args.slice(1));
|
|
677
|
-
return;
|
|
678
|
-
|
|
679
|
-
case 'stop':
|
|
680
|
-
await cmdStop(args.slice(1));
|
|
681
|
-
process.exit(0);
|
|
682
|
-
return;
|
|
683
|
-
|
|
684
|
-
case 'log':
|
|
685
|
-
await cmdLog(args.slice(1));
|
|
686
|
-
process.exit(0);
|
|
687
|
-
return;
|
|
688
|
-
|
|
689
|
-
case 'clear':
|
|
690
|
-
await cmdClear();
|
|
691
|
-
process.exit(0);
|
|
692
|
-
return;
|
|
693
|
-
|
|
694
|
-
case 'upgrade':
|
|
695
|
-
await cmdUpgrade();
|
|
696
|
-
process.exit(0);
|
|
697
|
-
return;
|
|
698
|
-
|
|
699
|
-
case 'update':
|
|
700
|
-
// Legacy command - same as upgrade
|
|
701
|
-
await cmdUpgrade();
|
|
702
|
-
process.exit(0);
|
|
703
|
-
return;
|
|
704
|
-
|
|
705
|
-
default:
|
|
706
|
-
// Default: start the server with all args (no command specified)
|
|
707
|
-
await cmdStart(args);
|
|
708
|
-
return;
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
// Run main function
|
|
713
|
-
main().catch((error) => {
|
|
18
|
+
// Run the CLI
|
|
19
|
+
runCli().catch((error) => {
|
|
714
20
|
console.error('Fatal error:', error);
|
|
715
21
|
process.exit(1);
|
|
716
22
|
});
|