@fmode/studio 0.0.3 → 0.0.6
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 +8 -336
- package/package.json +1 -1
package/bin/fmode.js
CHANGED
|
@@ -1,350 +1,22 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
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
|
-
* - Update command for manual updates
|
|
11
|
-
* - Fallback to Node.js/Bun execution
|
|
7
|
+
* This is a thin router that delegates to the modular CLI structure.
|
|
8
|
+
* All command logic is now in cli/ subdirectories.
|
|
12
9
|
*
|
|
13
10
|
* Usage:
|
|
14
11
|
* npx @fmode/studio
|
|
15
|
-
* npx @fmode/studio
|
|
16
|
-
* npx @fmode/studio
|
|
17
|
-
* fmode
|
|
12
|
+
* npx @fmode/studio start
|
|
13
|
+
* npx @fmode/studio server start
|
|
18
14
|
*/
|
|
19
15
|
|
|
20
|
-
import {
|
|
21
|
-
import { fileURLToPath } from 'url';
|
|
22
|
-
import { dirname, join } from 'path';
|
|
23
|
-
import { existsSync, mkdirSync, chmodSync, renameSync } from 'fs';
|
|
24
|
-
import { createReadStream, createWriteStream } from 'fs';
|
|
25
|
-
import { pipeline } from 'stream/promises';
|
|
26
|
-
import { unlink, readFile } from 'fs/promises';
|
|
16
|
+
import { runCli } from '../cli/index.ts';
|
|
27
17
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const rootDir = dirname(__dirname);
|
|
31
|
-
|
|
32
|
-
// Configuration
|
|
33
|
-
const REPOS_BASE_URL = 'https://repos.fmode.cn/x/fmode-studio';
|
|
34
|
-
const MANIFEST_URL = `${REPOS_BASE_URL}/manifest.json`;
|
|
35
|
-
const CACHE_DIR = join(rootDir, '.cache', 'binaries');
|
|
36
|
-
|
|
37
|
-
// Platform detection
|
|
38
|
-
const platform = process.platform;
|
|
39
|
-
const arch = process.arch;
|
|
40
|
-
|
|
41
|
-
// Map platform/arch to executable name
|
|
42
|
-
function getExecutableName() {
|
|
43
|
-
const ext = platform === 'win32' ? '.exe' : '';
|
|
44
|
-
|
|
45
|
-
if (platform === 'win32' && arch === 'x64') {
|
|
46
|
-
return `fmode-win-x64${ext}`;
|
|
47
|
-
} else if (platform === 'win32' && arch === 'arm64') {
|
|
48
|
-
return `fmode-win-arm64${ext}`;
|
|
49
|
-
} else if (platform === 'linux' && arch === 'x64') {
|
|
50
|
-
return 'fmode-linux-x64';
|
|
51
|
-
} else if (platform === 'linux' && arch === 'arm64') {
|
|
52
|
-
return 'fmode-linux-arm64';
|
|
53
|
-
} else if (platform === 'darwin' && arch === 'x64') {
|
|
54
|
-
return 'fmode-macos-x64';
|
|
55
|
-
} else if (platform === 'darwin' && arch === 'arm64') {
|
|
56
|
-
return 'fmode-macos-arm64';
|
|
57
|
-
} else {
|
|
58
|
-
throw new Error(`Unsupported platform: ${platform} ${arch}`);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Get local package version
|
|
63
|
-
async function getLocalVersion() {
|
|
64
|
-
const packageJson = join(rootDir, 'package.json');
|
|
65
|
-
try {
|
|
66
|
-
const content = await readFile(packageJson, 'utf-8');
|
|
67
|
-
const pkg = JSON.parse(content);
|
|
68
|
-
return pkg.version;
|
|
69
|
-
} catch {
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Fetch remote manifest from repos.fmode.cn
|
|
75
|
-
async function fetchRemoteManifest() {
|
|
76
|
-
try {
|
|
77
|
-
const response = await fetch(MANIFEST_URL);
|
|
78
|
-
if (!response.ok) {
|
|
79
|
-
throw new Error(`HTTP ${response.status}`);
|
|
80
|
-
}
|
|
81
|
-
return await response.json();
|
|
82
|
-
} catch (error) {
|
|
83
|
-
console.warn(`Warning: Could not fetch remote manifest: ${error.message}`);
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Compare versions (returns positive if a > b, negative if a < b, 0 if equal)
|
|
89
|
-
function compareVersions(a, b) {
|
|
90
|
-
const partsA = a.split('.').map(Number);
|
|
91
|
-
const partsB = b.split('.').map(Number);
|
|
92
|
-
|
|
93
|
-
for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
|
|
94
|
-
const partA = partsA[i] || 0;
|
|
95
|
-
const partB = partsB[i] || 0;
|
|
96
|
-
if (partA !== partB) {
|
|
97
|
-
return partA - partB;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
return 0;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Download binary from repos.fmode.cn
|
|
104
|
-
async function downloadBinary(version, executableName) {
|
|
105
|
-
const url = `${REPOS_BASE_URL}/${version}/${executableName}`;
|
|
106
|
-
const tempPath = join(CACHE_DIR, `${executableName}.tmp`);
|
|
107
|
-
const finalPath = join(CACHE_DIR, executableName);
|
|
108
|
-
|
|
109
|
-
console.log(`Downloading ${url}...`);
|
|
110
|
-
|
|
111
|
-
try {
|
|
112
|
-
const response = await fetch(url);
|
|
113
|
-
if (!response.ok) {
|
|
114
|
-
throw new Error(`HTTP ${response.status}`);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Ensure cache directory exists
|
|
118
|
-
if (!existsSync(CACHE_DIR)) {
|
|
119
|
-
mkdirSync(CACHE_DIR, { recursive: true });
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Download to temp file
|
|
123
|
-
const fileStream = createWriteStream(tempPath);
|
|
124
|
-
await pipeline(response.body, fileStream);
|
|
125
|
-
|
|
126
|
-
// Make executable (Unix-like systems)
|
|
127
|
-
if (platform !== 'win32') {
|
|
128
|
-
chmodSync(tempPath, 0o755);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Move to final path
|
|
132
|
-
if (existsSync(finalPath)) {
|
|
133
|
-
await unlink(finalPath);
|
|
134
|
-
}
|
|
135
|
-
renameSync(tempPath, finalPath);
|
|
136
|
-
|
|
137
|
-
console.log(`Downloaded to ${finalPath}`);
|
|
138
|
-
return finalPath;
|
|
139
|
-
} catch (error) {
|
|
140
|
-
// Clean up temp file on error
|
|
141
|
-
if (existsSync(tempPath)) {
|
|
142
|
-
await unlink(tempPath).catch(() => {});
|
|
143
|
-
}
|
|
144
|
-
throw error;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Check for updates
|
|
149
|
-
async function checkForUpdates() {
|
|
150
|
-
const localVersion = await getLocalVersion();
|
|
151
|
-
const manifest = await fetchRemoteManifest();
|
|
152
|
-
|
|
153
|
-
if (!manifest || !manifest.version) {
|
|
154
|
-
console.log('Could not check for updates.');
|
|
155
|
-
return null;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
if (!localVersion) {
|
|
159
|
-
console.log(`Remote version: ${manifest.version}`);
|
|
160
|
-
console.log('Local version: unknown');
|
|
161
|
-
return manifest.version;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const comparison = compareVersions(manifest.version, localVersion);
|
|
165
|
-
|
|
166
|
-
if (comparison > 0) {
|
|
167
|
-
console.log(`Update available: ${localVersion} -> ${manifest.version}`);
|
|
168
|
-
return manifest.version;
|
|
169
|
-
} else if (comparison === 0) {
|
|
170
|
-
console.log(`Already up to date: ${localVersion}`);
|
|
171
|
-
return null;
|
|
172
|
-
} else {
|
|
173
|
-
console.log(`Local version is newer: ${localVersion} > ${manifest.version}`);
|
|
174
|
-
return null;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Run the executable
|
|
179
|
-
async function runExecutable(executablePath, args) {
|
|
180
|
-
const child = spawn(executablePath, args, {
|
|
181
|
-
stdio: 'inherit',
|
|
182
|
-
env: { ...process.env }
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
return new Promise((resolve, reject) => {
|
|
186
|
-
child.on('exit', (code) => {
|
|
187
|
-
resolve(code ?? 0);
|
|
188
|
-
process.exit(code ?? 0);
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
child.on('error', (err) => {
|
|
192
|
-
reject(err);
|
|
193
|
-
});
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// Main execution
|
|
198
|
-
async function main() {
|
|
199
|
-
const args = process.argv.slice(2);
|
|
200
|
-
|
|
201
|
-
// Handle -y flag (auto-confirm, no-op in our case)
|
|
202
|
-
const yesIndex = args.indexOf('-y');
|
|
203
|
-
if (yesIndex !== -1) {
|
|
204
|
-
args.splice(yesIndex, 1);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// Handle update command
|
|
208
|
-
if (args[0] === 'update') {
|
|
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')) {
|
|
228
|
-
const localVersion = await getLocalVersion();
|
|
229
|
-
if (localVersion) {
|
|
230
|
-
console.log(`@fmode/studio v${localVersion}`);
|
|
231
|
-
} else {
|
|
232
|
-
console.log('@fmode/studio (version unknown)');
|
|
233
|
-
}
|
|
234
|
-
process.exit(0);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// Handle --help flag
|
|
238
|
-
if (args.includes('--help') || (args.includes('-h') && !args.includes('--host'))) {
|
|
239
|
-
console.log(`
|
|
240
|
-
Fmode Code UI Server
|
|
241
|
-
|
|
242
|
-
USAGE:
|
|
243
|
-
fmode [OPTIONS]
|
|
244
|
-
npx @fmode/studio [OPTIONS]
|
|
245
|
-
fmode update Check and install updates
|
|
246
|
-
|
|
247
|
-
OPTIONS:
|
|
248
|
-
-p, --port <PORT> Set the server port (default: 6666)
|
|
249
|
-
-h, --host <HOST> Set the server host (default: 0.0.0.0)
|
|
250
|
-
--help Show this help message
|
|
251
|
-
--version, -v Show version information
|
|
252
|
-
-y Auto-confirm (no-op, for compatibility)
|
|
253
|
-
update Check and install updates
|
|
254
|
-
|
|
255
|
-
ENVIRONMENT VARIABLES:
|
|
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
|
|
259
|
-
|
|
260
|
-
EXAMPLES:
|
|
261
|
-
fmode # Start with default settings
|
|
262
|
-
fmode --port 8080 # Start on port 8080
|
|
263
|
-
npx @fmode/studio -y # Start with npx (auto-confirm)
|
|
264
|
-
npx @fmode/studio --port 3000 # Start on port 3000
|
|
265
|
-
fmode update # Check for updates
|
|
266
|
-
|
|
267
|
-
UPDATE MECHANISM:
|
|
268
|
-
The CLI automatically downloads the latest binary from repos.fmode.cn
|
|
269
|
-
and caches it locally. Use 'fmode update' to manually check for updates.
|
|
270
|
-
`);
|
|
271
|
-
process.exit(0);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
const executableName = getExecutableName();
|
|
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
|
-
}
|
|
291
|
-
|
|
292
|
-
// Priority 3: Download from repos.fmode.cn
|
|
293
|
-
if (!executablePath) {
|
|
294
|
-
console.log('No local binary found. Downloading...');
|
|
295
|
-
try {
|
|
296
|
-
const manifest = await fetchRemoteManifest();
|
|
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
|
-
}
|
|
304
|
-
|
|
305
|
-
// If we have an executable, run it
|
|
306
|
-
if (executablePath) {
|
|
307
|
-
try {
|
|
308
|
-
await runExecutable(executablePath, args);
|
|
309
|
-
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
|
-
|
|
316
|
-
// Priority 4: Fallback to Node.js/Bun
|
|
317
|
-
fallbackToNode(args);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
function fallbackToNode(args) {
|
|
321
|
-
const cliPath = join(rootDir, 'cli.ts');
|
|
322
|
-
|
|
323
|
-
if (existsSync(cliPath)) {
|
|
324
|
-
console.log('Running with bun...');
|
|
325
|
-
const child = spawn('bun', [cliPath, ...args], {
|
|
326
|
-
stdio: 'inherit',
|
|
327
|
-
env: { ...process.env }
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
child.on('exit', (code) => {
|
|
331
|
-
process.exit(code ?? 0);
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
child.on('error', (err) => {
|
|
335
|
-
console.error('Failed to start with bun:', err.message);
|
|
336
|
-
console.error('Please install bun: https://bun.sh');
|
|
337
|
-
process.exit(1);
|
|
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);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// Run main function
|
|
347
|
-
main().catch((error) => {
|
|
18
|
+
// Run the CLI
|
|
19
|
+
runCli().catch((error) => {
|
|
348
20
|
console.error('Fatal error:', error);
|
|
349
21
|
process.exit(1);
|
|
350
22
|
});
|