create-yeow 0.5.1 → 0.5.3

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.
@@ -1,544 +1,544 @@
1
- import { existsSync, mkdirSync, copyFileSync, writeFileSync, readFileSync, createWriteStream, statSync, watch, readdirSync, rmSync } from 'fs';
2
- import { resolve, dirname, basename } from 'path';
3
- import { spawn, execSync } from 'child_process';
4
- import { fileURLToPath } from 'url';
5
- import { createInterface } from 'readline';
6
- import https from 'https';
7
- import { createServer } from 'http';
8
- import { WebSocketServer } from 'ws';
9
- import { SourceMapConsumer } from 'source-map';
10
-
11
- const __dirname = dirname(fileURLToPath(import.meta.url));
12
- const ROOT = resolve(__dirname, '..');
13
- const DEVDIR = resolve(ROOT, '.yeow', 'dev');
14
- const CACHE = resolve(DEVDIR, 'cache');
15
- const SERVER = resolve(DEVDIR, 'server');
16
- const WS_PORT = 17368;
17
-
18
- const YES = process.argv.includes('-y') || process.env.CI === 'true';
19
- const EULA = process.argv.includes('--eula') || YES;
20
- const STOP = (() => { const a = process.argv.find(a => a.startsWith('--stop=')); if (!a) return null; const m = a.split('=')[1].match(/^(\d+)(s|m|h)?$/); return m ? parseInt(m[1]) * (m[2] === 'm' ? 60 : m[2] === 'h' ? 3600 : 1) : null; })();
21
-
22
- // ── AI 工作流参数(headless 模式)─────────────────────────────
23
- function parseDur(flag, def) {
24
- const a = process.argv.find(a => a.startsWith(flag));
25
- if (!a) return def;
26
- const m = a.split('=')[1].match(/^(\d+)(s|m|h)?$/);
27
- return m ? parseInt(m[1]) * (m[2] === 'm' ? 60 : m[2] === 'h' ? 3600 : 1) : def;
28
- }
29
- const TIMEOUT = parseDur('--timeout=', 120); // 服务器加载超时(秒,默认 2m)
30
- const WAIT = parseDur('--wait=', 30); // 加载成功后等待(秒,默认 30s)
31
- const OUTFILE = process.argv.find(a => a.startsWith('--outfile='))?.split('=').slice(1).join('=') || null;
32
- const KEEP = process.argv.includes('--keep');
33
- const HEADLESS = process.argv.includes('--eula') || process.argv.includes('--timeout')
34
- || process.argv.includes('--wait') || process.argv.includes('--outfile') || KEEP;
35
-
36
- const cfg = JSON.parse(readFileSync(resolve(ROOT, 'yeow.config.json'), 'utf-8'));
37
- const RUNTIME = resolve(ROOT, '.yeow', 'assets', 'yeow-runtime-0.5.0.jar');
38
-
39
- // Dev server config (optional, from yeow.config.json)
40
- const devCfg = cfg.dev || {};
41
- const PAPER_VERSION = devCfg.paperVersion || '1.21.4';
42
- const PAPER_URL = devCfg.paperJar || null;
43
- let PAPER_PATH = null;
44
- let PAPER_JAR = null;
45
-
46
- if (PAPER_URL) {
47
- if (PAPER_URL.startsWith('http://') || PAPER_URL.startsWith('https://')) {
48
- PAPER_PATH = resolve(CACHE, basename(new URL(PAPER_URL).pathname));
49
- PAPER_JAR = basename(new URL(PAPER_URL).pathname);
50
- } else {
51
- PAPER_PATH = PAPER_URL;
52
- PAPER_JAR = basename(PAPER_URL);
53
- }
54
- } else {
55
- PAPER_JAR = `paper-${PAPER_VERSION}.jar`;
56
- PAPER_PATH = resolve(CACHE, PAPER_JAR);
57
- }
58
-
59
- // Config hash — detect changes and recreate dev server
60
- const CONFIG_HASH_FILE = resolve(DEVDIR, '.config-hash');
61
- function configHash() { return JSON.stringify({ paperUrl: PAPER_URL, paperVersion: PAPER_VERSION }); }
62
-
63
- function checkConfigChanged() {
64
- if (!existsSync(CONFIG_HASH_FILE)) return true;
65
- try {
66
- return readFileSync(CONFIG_HASH_FILE, 'utf-8').trim() !== configHash();
67
- } catch { return true; }
68
- }
69
-
70
- function saveConfigHash() {
71
- mkdirSync(DEVDIR, { recursive: true });
72
- writeFileSync(CONFIG_HASH_FILE, configHash());
73
- }
74
-
75
- if (checkConfigChanged()) {
76
- if (existsSync(SERVER)) {
77
- console.log(' Paper config changed — recreating dev server...');
78
- rmSync(SERVER, { recursive: true, force: true });
79
- }
80
- saveConfigHash();
81
- }
82
-
83
- const c = { r: '\x1b[0m', b: '\x1b[1m', d: '\x1b[2m', g: '\x1b[32m', y: '\x1b[33m', B: '\x1b[34m', C: '\x1b[36m', R: '\x1b[31m', ok: '\x1b[32m✓\x1b[0m', fail: '\x1b[31m✗\x1b[0m', info: '\x1b[36mⓘ\x1b[0m', warn: '\x1b[33m⚠\x1b[0m' };
84
- const log = (msg, color = '') => console.log(`${c.d}[${new Date().toLocaleTimeString()}]${c.r} ${color}${msg}${c.r}`);
85
- const ok = msg => log(`${c.ok} ${msg}`, c.g);
86
- const fail = msg => log(`${c.fail} ${msg}`, c.R);
87
- const info = msg => log(`${c.info} ${msg}`, c.C);
88
- const warn = msg => log(`${c.warn} ${msg}`, c.y);
89
-
90
- let proc = null;
91
- let wss = null;
92
-
93
- // ── Graceful shutdown ────────────────────────────────────────────
94
- let cleaning = false;
95
- function cleanup() {
96
- if (cleaning) return;
97
- cleaning = true;
98
- if (wss) { try { wss.close(); } catch {} }
99
- if (proc && !proc.killed) {
100
- try { proc.stdin.write('stop\n'); } catch {}
101
- const killer = setTimeout(() => { if (proc && !proc.killed) try { proc.kill('SIGKILL'); } catch {} }, 10000);
102
- proc.on('close', () => { clearTimeout(killer); process.exit(0); });
103
- } else {
104
- process.exit(0);
105
- }
106
- }
107
- process.on('SIGINT', cleanup);
108
- process.on('SIGTERM', cleanup);
109
- process.on('beforeExit', () => { if (proc && !proc.killed) try { proc.kill(); } catch {} });
110
-
111
- // ── WebSocket Server ────────────────────────────────────────────
112
- function startWebSocket() {
113
- const server = createServer();
114
- wss = new WebSocketServer({ server });
115
- wss.on('connection', (ws) => {
116
- info('Java runtime connected');
117
- ws.on('message', async (data) => {
118
- try {
119
- const msg = JSON.parse(data.toString());
120
- if (msg.type === 'js-error') {
121
- await printFormattedError(msg);
122
- }
123
- } catch (e) { warn('Error processing message: ' + (e?.message || e)); }
124
- });
125
- ws.on('close', () => info('Java runtime disconnected'));
126
- });
127
- server.listen(WS_PORT, () => {
128
- info(`WebSocket server on port ${WS_PORT}`);
129
- });
130
- }
131
-
132
- function broadcast(msg) {
133
- if (!wss) return;
134
- const data = JSON.stringify(msg);
135
- wss.clients.forEach((ws) => {
136
- if (ws.readyState === 1) ws.send(data);
137
- });
138
- }
139
-
140
- // ── Download ────────────────────────────────────────────────────
141
- function download(url, dest) {
142
- return new Promise((resolve, reject) => {
143
- const f = createWriteStream(dest);
144
- https.get(url, res => {
145
- if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { f.close(); download(res.headers.location, dest).then(resolve).catch(reject); return; }
146
- if (res.statusCode !== 200) { f.close(); reject(new Error(`HTTP ${res.statusCode}`)); return; }
147
- const total = parseInt(res.headers['content-length'], 10);
148
- let dl = 0;
149
- res.on('data', chunk => { dl += chunk.length; if (total && !HEADLESS) process.stdout.write(`\r ${c.B}Downloading...${c.r} ${(dl / 1024 / 1024).toFixed(1)}MB / ${(total / 1024 / 1024).toFixed(1)}MB`); });
150
- res.pipe(f);
151
- f.on('finish', () => { f.close(); if (!HEADLESS) process.stdout.write('\n'); resolve(); });
152
- }).on('error', e => { f.close(); reject(e); });
153
- });
154
- }
155
-
156
- async function ensurePaper() {
157
- if (PAPER_URL) {
158
- // User-specified URL or file path
159
- if (PAPER_URL.startsWith('http://') || PAPER_URL.startsWith('https://')) {
160
- if (existsSync(PAPER_PATH) && statSync(PAPER_PATH).size > 1_000_000) { if (!HEADLESS) ok('Paper found in cache'); return; }
161
- mkdirSync(CACHE, { recursive: true });
162
- if (!HEADLESS) info('Downloading Paper...');
163
- try { await download(PAPER_URL, PAPER_PATH); if (!HEADLESS) ok('Paper downloaded'); }
164
- catch (e) { fail('Download failed: ' + e.message); console.log(' Manually: ' + PAPER_PATH); process.exit(1); }
165
- } else {
166
- if (!existsSync(PAPER_PATH)) { fail(`Paper JAR not found: ${PAPER_PATH}`); process.exit(1); }
167
- if (!HEADLESS) ok('Paper found at specified path');
168
- }
169
- } else {
170
- if (existsSync(PAPER_PATH) && statSync(PAPER_PATH).size > 1_000_000) { if (!HEADLESS) ok('Paper found in cache'); return; }
171
- mkdirSync(CACHE, { recursive: true });
172
- info('Downloading Paper...');
173
- try {
174
- const apiUrl = `https://api.papermc.io/v2/projects/paper/versions/${PAPER_VERSION}`;
175
- const versions = await fetch(apiUrl).then(r => r.json());
176
- const latestBuild = versions.builds[versions.builds.length - 1];
177
- const dlUrl = `https://api.papermc.io/v2/projects/paper/versions/${PAPER_VERSION}/builds/${latestBuild}/downloads/paper-${PAPER_VERSION}-${latestBuild}.jar`;
178
- await download(dlUrl, PAPER_PATH);
179
- if (!HEADLESS) ok('Paper downloaded');
180
- } catch (e) {
181
- fail('Download failed: ' + e.message);
182
- console.log(' Manually download to: ' + PAPER_PATH);
183
- console.log(' Or set "paperJar" in yeow.config.json dev section');
184
- process.exit(1);
185
- }
186
- }
187
- }
188
-
189
- function buildPlugin() {
190
- info('Building plugin...');
191
- try { execSync('node .yeow/build.js', { cwd: ROOT, stdio: 'inherit', env: { ...process.env, YEOW_DEV: 'true' } }); ok('Plugin built'); }
192
- catch (e) { fail('Build failed: ' + e.message); process.exit(1); }
193
- }
194
-
195
- function copyToPlugins(src, label) {
196
- const d = resolve(SERVER, 'plugins'); mkdirSync(d, { recursive: true });
197
- copyFileSync(src, resolve(d, basename(src))); ok(`${label} copied`);
198
- }
199
-
200
- function copyToYeowDir(src, label) {
201
- const d = resolve(SERVER, 'plugins', 'Yeow'); mkdirSync(d, { recursive: true });
202
- copyFileSync(src, resolve(d, basename(src))); ok(`${label} copied`);
203
- }
204
-
205
- /** Remove a previously deployed dev plugin JAR so it cannot conflict with the .yeow.zip in plugins/Yeow/. */
206
- function removeStaleDevJar() {
207
- const stale = resolve(SERVER, 'plugins', `${cfg.name}-${cfg.version}.jar`);
208
- if (existsSync(stale)) {
209
- rmSync(stale, { force: true });
210
- warn(`Removed stale dev JAR: ${basename(stale)} (conflicts with plugins/Yeow .yeow.zip)`);
211
- }
212
- }
213
-
214
- async function initServer() {
215
- const jar = resolve(SERVER, PAPER_JAR);
216
- if (!existsSync(jar)) copyFileSync(PAPER_PATH, jar);
217
- const eula = resolve(SERVER, 'eula.txt');
218
- if (existsSync(eula) && readFileSync(eula, 'utf-8').includes('eula=true')) return;
219
-
220
- if (EULA) {
221
- if (!existsSync(resolve(SERVER, 'server.properties'))) {
222
- info('Initializing...');
223
- await new Promise(r => { const p = spawn('java', ['-Xmx4G', '-Xms4G', '-jar', jar, '--nogui'], { cwd: SERVER, stdio: ['pipe', 'inherit', 'inherit'] }); setTimeout(() => { p.kill(); r(); }, 120000); p.on('exit', r); p.on('error', r); });
224
- }
225
- writeFileSync(eula, 'eula=true\n'); ok('EULA auto-accepted');
226
- } else {
227
- console.log(`\n Press Enter to accept Mojang EULA:`);
228
- await new Promise(r => process.stdin.once('data', () => { writeFileSync(eula, 'eula=true\n'); ok('EULA accepted'); r(); }));
229
- }
230
- }
231
-
232
- function serverProps(port) {
233
- const f = resolve(SERVER, 'server.properties');
234
- const m = { 'server-port': String(port), 'online-mode': 'false', 'spawn-protection': '0', 'enable-command-block': 'true', 'max-players': '10', 'difficulty': 'easy', 'motd': cfg.name || 'Yeow Dev' };
235
- if (existsSync(f)) {
236
- for (const l of readFileSync(f, 'utf-8').split('\n')) { const eq = l.indexOf('='); if (eq > 0 && !l.startsWith('#')) { const k = l.substring(0, eq).trim(); if (!m[k]) m[k] = l.substring(eq + 1).trim(); } }
237
- }
238
- let out = ''; for (const [k, v] of Object.entries(m)) out += `${k}=${v}\n`;
239
- writeFileSync(f, out);
240
- }
241
-
242
- // ── Hot Reload via WebSocket ────────────────────────────────────
243
- function startHotReload() {
244
- const srcDir = resolve(ROOT, 'src');
245
- const assetsDir = resolve(ROOT, 'assets');
246
- if (!existsSync(srcDir)) return;
247
-
248
- let timer = null;
249
- let building = false;
250
- const ext = existsSync(resolve(srcDir, 'index.ts')) ? 'ts' : 'js';
251
-
252
- const rebuildAndNotify = () => {
253
- if (timer) clearTimeout(timer);
254
- timer = setTimeout(() => {
255
- if (building) return;
256
- building = true;
257
- info('Source changed, rebuilding...');
258
- try {
259
- execSync('node .yeow/build.js', { cwd: ROOT, stdio: 'pipe', env: { ...process.env, YEOW_DEV: 'true' } });
260
- const compiled = resolve(ROOT, 'dist', '.dev', 'main.js');
261
- if (existsSync(compiled)) {
262
- const devAssets = resolve(ROOT, 'dist', '.dev', '.assets');
263
- // 权限随热重载一并刷新:构建脚本(build.js)已把 computedPermissions 回写至 yeow.config.json
264
- let permissions = [];
265
- try {
266
- permissions = JSON.parse(readFileSync(resolve(ROOT, 'yeow.config.json'), 'utf-8')).computedPermissions || [];
267
- } catch {}
268
- broadcast({
269
- type: 'hot-reload',
270
- plugin: cfg.name,
271
- codeFile: compiled.replace(/\\/g, '/'),
272
- assetsDir: existsSync(devAssets) ? devAssets.replace(/\\/g, '/') : null,
273
- permissions,
274
- });
275
- _consumer = null;
276
- ok('Hot reload sent via WebSocket' + (permissions.length ? ` (permissions ${permissions.length})` : ''));
277
- }
278
- } catch (e) {
279
- fail('Build failed: ' + e.message);
280
- broadcast({ type: 'build-error', plugin: cfg.name, error: e.message });
281
- }
282
- building = false;
283
- }, 300);
284
- };
285
-
286
- watch(srcDir, { recursive: true }, (event, file) => {
287
- if (!file || !file.endsWith('.' + ext)) return;
288
- rebuildAndNotify();
289
- });
290
-
291
- if (existsSync(assetsDir)) {
292
- watch(assetsDir, { recursive: true }, (event, file) => {
293
- rebuildAndNotify();
294
- });
295
- }
296
-
297
- // Worker 源码目录(dev.worker[].entry 所在目录)变化 → 重建(worker 随主插件热重载重建)
298
- const workerCfg = (cfg.dev && cfg.dev.worker) || [];
299
- const watchedWorkerDirs = new Set();
300
- for (const w of workerCfg) {
301
- if (!w?.entry) continue;
302
- const dir = resolve(ROOT, dirname(w.entry));
303
- if (!existsSync(dir) || watchedWorkerDirs.has(dir)) continue;
304
- watchedWorkerDirs.add(dir);
305
- watch(dir, { recursive: true }, (event, file) => {
306
- if (!file || !/\.(ts|js|mjs)$/.test(file)) return;
307
- rebuildAndNotify();
308
- });
309
- }
310
-
311
- info(`Watching src/ + assets/${watchedWorkerDirs.size > 0 ? ' + worker dirs' : ''} for changes (WebSocket hot reload)`);
312
- }
313
-
314
- // ── Source-Mapped Error Display ─────────────────────────────────
315
- let _consumer = null;
316
- async function getSourceMapConsumer() {
317
- if (_consumer) return _consumer;
318
- const mapFile = resolve(ROOT, 'dist', '.dev', 'main.js.map');
319
- if (!existsSync(mapFile)) return null;
320
- try {
321
- const raw = JSON.parse(readFileSync(mapFile, 'utf-8'));
322
- _consumer = await new SourceMapConsumer(raw);
323
- return _consumer;
324
- } catch { return null; }
325
- }
326
-
327
- /** 查找 Worker 的 source-map 文件(产物位于 dist/.dev/.assets/<id>/worker/<name>.js.map)。 */
328
- function findWorkerMapFile(workerName) {
329
- const assetsRoot = resolve(ROOT, 'dist', '.dev', '.assets');
330
- if (!existsSync(assetsRoot)) return null;
331
- for (const id of readdirSync(assetsRoot)) {
332
- const mapFile = resolve(assetsRoot, id, 'worker', workerName + '.js.map');
333
- if (existsSync(mapFile)) return mapFile;
334
- }
335
- return null;
336
- }
337
-
338
- /** Worker 的 source-map(产物位于 dist/.dev/.assets/<id>/worker/<name>.js(.map))。 */
339
- let _workerConsumers = {};
340
- async function getWorkerSourceMapConsumer(workerName) {
341
- if (_workerConsumers[workerName]) return _workerConsumers[workerName];
342
- try {
343
- const mapFile = findWorkerMapFile(workerName);
344
- if (mapFile) {
345
- const raw = JSON.parse(readFileSync(mapFile, 'utf-8'));
346
- _workerConsumers[workerName] = await new SourceMapConsumer(raw);
347
- return _workerConsumers[workerName];
348
- }
349
- } catch { /* 未找到 */ }
350
- _workerConsumers[workerName] = null;
351
- return null;
352
- }
353
-
354
- async function printFormattedError(err) {
355
- const c = { r: '\x1b[0m', R: '\x1b[31m', Y: '\x1b[33m', C: '\x1b[36m', B: '\x1b[1m', D: '\x1b[2m', g: '\x1b[32m' };
356
- const isWorker = err.origin && err.origin !== 'main';
357
- let out = isWorker
358
- ? `\n${c.R}${c.B} JS Error in Worker [${err.origin}]${c.r}\n`
359
- : `\n${c.R}${c.B} JS Error [${err.plugin}]${c.r}\n`;
360
- if (err.context) out += ` ${c.D}context: ${err.context}${c.r}\n`;
361
- out += ` ${c.Y}${err.message}${c.r}\n`;
362
-
363
- // 产物文件名:主插件 main.js;Worker <name>.js
364
- const bundleName = isWorker ? err.origin + '.js' : 'main.js';
365
- const bundleRe = new RegExp(bundleName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ':(\\d+):(\\d+)');
366
- const hasBundle = err.stack?.match(bundleRe) || err.fileName === bundleName;
367
- let consumer = null;
368
- if (hasBundle) {
369
- consumer = isWorker ? await getWorkerSourceMapConsumer(err.origin) : await getSourceMapConsumer();
370
- }
371
- if (hasBundle && !consumer) {
372
- const mapFile = isWorker ? findWorkerMapFile(err.origin) : resolve(ROOT, 'dist', '.dev', 'main.js.map');
373
- out += ` ${c.D}(source-map not found: ${mapFile && existsSync(mapFile) ? 'exists but failed to parse' : 'missing at ' + (mapFile || '.assets/<id>/worker/' + err.origin + '.js.map')})${c.r}\n`;
374
- }
375
-
376
- const frames = [];
377
- if (err.stack) {
378
- for (const rawLine of err.stack.split('\n')) {
379
- const m = rawLine.match(/at\s+(?:\S+\s+)?\(?([^\\/\s()]+\.js):(\d+):(\d+)\)?/);
380
- if (m && consumer && (isWorker ? m[1] === bundleName : m[1] === 'main.js')) {
381
- const orig = consumer.originalPositionFor({ line: parseInt(m[2]), column: parseInt(m[3]) });
382
- if (!orig?.source) {
383
- const orig2 = consumer.originalPositionFor({ line: parseInt(m[2]), column: parseInt(m[3]) - 1 });
384
- if (orig2?.source) { orig.source = orig2.source; orig.line = orig2.line; orig.column = orig2.column; }
385
- }
386
- frames.push({ orig, raw: rawLine });
387
- } else {
388
- frames.push({ raw: rawLine });
389
- }
390
- }
391
- }
392
-
393
- let ctxFrame = frames.find(f => f.orig?.source?.match(/[\\/]src[\\/]/) && !f.orig.source.includes('node_modules'))?.orig || null;
394
-
395
- if (ctxFrame?.source && ctxFrame.line && consumer) {
396
- const srcPath = ctxFrame.source.replace(/^\.\.\/\.\.\//, '');
397
- const ctxRaw = frames.find(f => f.orig === ctxFrame)?.raw || '';
398
- const fnM = ctxRaw.match(/at\s+(\S+)\s+\(/);
399
- const fnS = fnM ? fnM[1] + ' ' : '';
400
- out += ` ${c.C}at ${fnS}(${srcPath}:${ctxFrame.line}:${ctxFrame.column})${c.r}\n`;
401
- const content = consumer.sourceContentFor(ctxFrame.source);
402
- if (content) {
403
- const lines = content.split('\n');
404
- const start = Math.max(0, ctxFrame.line - 3);
405
- const end = Math.min(lines.length, ctxFrame.line + 2);
406
- for (let i = start; i < end; i++) {
407
- const prefix = i === ctxFrame.line - 1 ? c.R + ' →' : ' ';
408
- out += ` ${prefix} ${c.D}${String(i + 1).padStart(4)}|${c.r} ${lines[i]}\n`;
409
- if (i === ctxFrame.line - 1 && ctxFrame.column > 0) {
410
- const col = Math.max(0, ctxFrame.column);
411
- const indent = ' '.repeat(String(i + 1).padStart(4).length) + '| ';
412
- const caret = ' '.repeat(col) + c.R + '^' + c.r;
413
- out += ` ${c.R} →${c.r} ${indent}${caret}\n`;
414
- }
415
- }
416
- }
417
- }
418
-
419
- if (frames.length > 0) {
420
- out += ` ${c.D}Stack:${c.r}\n`;
421
- for (const f of frames) {
422
- if (f.orig?.source) {
423
- const srcPath = f.orig.source.replace(/^\.\.\/\.\.\//, '');
424
- const fnMatch = f.raw.match(/at\s+(\S+)\s+\(/);
425
- const fn = fnMatch ? fnMatch[1] + ' ' : '';
426
- const isUser = srcPath.startsWith('src/');
427
- const style = isUser ? c.B + c.g : c.D;
428
- out += ` ${style} at ${fn}(${srcPath}:${f.orig.line}:${f.orig.column})${c.r}\n`;
429
- } else {
430
- const internal = f.raw.includes('init.js') || f.raw.includes('unknown.js') ? ' (internal)' : '';
431
- out += `${c.D} ${f.raw}${internal}${c.r}\n`;
432
- }
433
- }
434
- }
435
- console.log(out);
436
- }
437
-
438
- function startServer() {
439
- // 注意:不设 -Dstdout.encoding=UTF-8——主路径 stdout 直接继承控制台(stdio: inherit),
440
- // 强制 UTF-8 会在 GBK 控制台(中文 Windows)产生中文乱码;不设时 JVM 自动匹配控制台编码。
441
- // headless 路径(管道 + readline 解码)才需要 UTF-8 强制。
442
- const jvmArgs = ['-Xmx4G', '-Xms4G', '-Dfile.encoding=UTF-8', '-Dyeow.dev=true', '-Dyeow.ws.port=' + WS_PORT];
443
- info(`\nStarting Paper ${PAPER_VERSION} server...`);
444
- proc = spawn('java', [...jvmArgs, '-jar', resolve(SERVER, PAPER_JAR), '--nogui'], { cwd: SERVER, stdio: ['pipe', 'inherit', 'inherit'] });
445
- proc.on('exit', code => { warn(`Server exited (${code})`); if (wss) wss.close(); process.exit(0); });
446
- process.stdin.on('data', d => { if (proc && !proc.killed) try { proc.stdin.write(d); } catch {} });
447
- if (STOP) { info(`Auto-stop in ${STOP}s`); setTimeout(() => { warn('Auto-stop'); cleanup(); }, STOP * 1000); }
448
- }
449
-
450
- async function main() {
451
- console.log(`\n${c.b}${c.B} Yeow Dev Server${c.r}\n`);
452
- if (!existsSync(RUNTIME)) { fail(`Runtime JAR not found: ${RUNTIME}`); process.exit(1); }
453
-
454
- if (HEADLESS) { await runHeadless(); return; }
455
-
456
- startWebSocket();
457
- await ensurePaper();
458
- mkdirSync(SERVER, { recursive: true });
459
- await initServer();
460
- serverProps(cfg.dev?.port || 17367);
461
- buildPlugin();
462
- removeStaleDevJar();
463
- copyToYeowDir(resolve(ROOT, 'dist', 'plugins', `${cfg.name}-${cfg.version}.yeow.zip`), 'Plugin (.yeow.zip → plugins/Yeow/)');
464
- copyToPlugins(RUNTIME, 'Runtime');
465
-
466
- startHotReload();
467
- startServer();
468
- }
469
-
470
- // ── AI 工作流(headless)─────────────────────────────────────────
471
- // 适合 AI 代理/CI:--eula 自动接受 → 下载 → 启动 → 检测加载完成 →
472
- // 等待 --wait 秒后命令自动结束(--keep 保留服务器子进程,日志见 --outfile)。
473
- async function runHeadless() {
474
- if (!EULA) {
475
- fail('AI 模式需要 --eula(自动接受 EULA)');
476
- process.exit(1);
477
- }
478
- const log = OUTFILE ? createWriteStream(OUTFILE, { flags: 'a' }) : null;
479
- const out = (line) => { if (log) log.write(line + '\n'); else console.log(line); };
480
-
481
- // 流程级超时:覆盖下载/初始化/启动/加载全程(--timeout,默认 2m)
482
- let done = false;
483
- const finish = (code) => {
484
- if (waitTimer) clearTimeout(waitTimer);
485
- try { if (log) log.end(); } catch {}
486
- process.exit(code);
487
- };
488
- const failTimer = setTimeout(() => {
489
- if (done) return;
490
- fail(`流程在 ${TIMEOUT}s 内未完成加载——请检查网络/依赖下载,或加大超时(--timeout=3m)`);
491
- killProc();
492
- finish(1);
493
- }, TIMEOUT * 1000);
494
-
495
- info('正在下载/准备服务端…');
496
- await ensurePaper();
497
- mkdirSync(SERVER, { recursive: true });
498
- await initServer();
499
- serverProps(cfg.dev?.port || 17367);
500
- buildPlugin();
501
- removeStaleDevJar();
502
- copyToYeowDir(resolve(ROOT, 'dist', 'plugins', `${cfg.name}-${cfg.version}.yeow.zip`), 'Plugin (.yeow.zip → plugins/Yeow/)');
503
- copyToPlugins(RUNTIME, 'Runtime');
504
-
505
- // 编码:确保子进程 stdout 按 UTF-8 输出(Windows 下避免中文字符乱码)
506
- const jvmArgs = ['-Xmx4G', '-Xms4G', '-Dfile.encoding=UTF-8', '-Dstdout.encoding=UTF-8',
507
- '-Dyeow.dev=true', '-Dyeow.ws.port=' + WS_PORT];
508
- info(`正在启动 Paper ${PAPER_VERSION}...`);
509
- proc = spawn('java', [...jvmArgs, '-jar', resolve(SERVER, PAPER_JAR), '--nogui'], { cwd: SERVER, stdio: ['ignore', 'pipe', 'pipe'] });
510
- info(`Server PID: ${proc.pid}`);
511
-
512
- let started = false, waitTimer = null;
513
-
514
- const onLine = (line) => {
515
- out(line);
516
- if (!started && line.includes('Starting org.bukkit.craftbukkit.Main')) {
517
- started = true;
518
- info('开始加载(Starting org.bukkit.craftbukkit.Main)');
519
- }
520
- if (!done && line.includes('Done (') && line.includes('For help')) {
521
- done = true;
522
- clearTimeout(failTimer);
523
- info(`加载完成——等待 ${WAIT}s 后命令结束${KEEP ? '(--keep 保留服务器进程)' : '(关闭服务器进程)'}…`);
524
- waitTimer = setTimeout(() => {
525
- info(`等待结束。日志${OUTFILE ? ':' + OUTFILE : '输出于上方'};PID=${proc.pid}${KEEP ? '(服务器仍在运行,按需 kill)' : ''}`);
526
- if (!KEEP) killProc();
527
- finish(0);
528
- }, WAIT * 1000);
529
- }
530
- };
531
- createInterface({ input: proc.stdout }).on('line', onLine);
532
- if (proc.stderr) createInterface({ input: proc.stderr }).on('line', (l) => out('[err] ' + l));
533
-
534
- proc.on('exit', (code) => {
535
- if (!done) fail(`服务器提前退出(code ${code})——见${OUTFILE ? '日志 ' + OUTFILE : '上方输出'}`);
536
- finish(1);
537
- });
538
- }
539
-
540
- function killProc() {
541
- if (proc && !proc.killed) { try { proc.kill('SIGKILL'); } catch {} }
542
- }
543
-
544
- main().catch(e => { fail(e.message); process.exit(1); });
1
+ import { existsSync, mkdirSync, copyFileSync, writeFileSync, readFileSync, createWriteStream, statSync, watch, readdirSync, rmSync } from 'fs';
2
+ import { resolve, dirname, basename } from 'path';
3
+ import { spawn, execSync } from 'child_process';
4
+ import { fileURLToPath } from 'url';
5
+ import { createInterface } from 'readline';
6
+ import https from 'https';
7
+ import { createServer } from 'http';
8
+ import { WebSocketServer } from 'ws';
9
+ import { SourceMapConsumer } from 'source-map';
10
+
11
+ const __dirname = dirname(fileURLToPath(import.meta.url));
12
+ const ROOT = resolve(__dirname, '..');
13
+ const DEVDIR = resolve(ROOT, '.yeow', 'dev');
14
+ const CACHE = resolve(DEVDIR, 'cache');
15
+ const SERVER = resolve(DEVDIR, 'server');
16
+ const WS_PORT = 17368;
17
+
18
+ const YES = process.argv.includes('-y') || process.env.CI === 'true';
19
+ const EULA = process.argv.includes('--eula') || YES;
20
+ const STOP = (() => { const a = process.argv.find(a => a.startsWith('--stop=')); if (!a) return null; const m = a.split('=')[1].match(/^(\d+)(s|m|h)?$/); return m ? parseInt(m[1]) * (m[2] === 'm' ? 60 : m[2] === 'h' ? 3600 : 1) : null; })();
21
+
22
+ // ── AI 工作流参数(headless 模式)─────────────────────────────
23
+ function parseDur(flag, def) {
24
+ const a = process.argv.find(a => a.startsWith(flag));
25
+ if (!a) return def;
26
+ const m = a.split('=')[1].match(/^(\d+)(s|m|h)?$/);
27
+ return m ? parseInt(m[1]) * (m[2] === 'm' ? 60 : m[2] === 'h' ? 3600 : 1) : def;
28
+ }
29
+ const TIMEOUT = parseDur('--timeout=', 120); // 服务器加载超时(秒,默认 2m)
30
+ const WAIT = parseDur('--wait=', 30); // 加载成功后等待(秒,默认 30s)
31
+ const OUTFILE = process.argv.find(a => a.startsWith('--outfile='))?.split('=').slice(1).join('=') || null;
32
+ const KEEP = process.argv.includes('--keep');
33
+ const HEADLESS = process.argv.includes('--eula') || process.argv.includes('--timeout')
34
+ || process.argv.includes('--wait') || process.argv.includes('--outfile') || KEEP;
35
+
36
+ const cfg = JSON.parse(readFileSync(resolve(ROOT, 'yeow.config.json'), 'utf-8'));
37
+ const RUNTIME = resolve(ROOT, '.yeow', 'assets', 'yeow-runtime-0.5.3.jar');
38
+
39
+ // Dev server config (optional, from yeow.config.json)
40
+ const devCfg = cfg.dev || {};
41
+ const PAPER_VERSION = devCfg.paperVersion || '1.21.4';
42
+ const PAPER_URL = devCfg.paperJar || null;
43
+ let PAPER_PATH = null;
44
+ let PAPER_JAR = null;
45
+
46
+ if (PAPER_URL) {
47
+ if (PAPER_URL.startsWith('http://') || PAPER_URL.startsWith('https://')) {
48
+ PAPER_PATH = resolve(CACHE, basename(new URL(PAPER_URL).pathname));
49
+ PAPER_JAR = basename(new URL(PAPER_URL).pathname);
50
+ } else {
51
+ PAPER_PATH = PAPER_URL;
52
+ PAPER_JAR = basename(PAPER_URL);
53
+ }
54
+ } else {
55
+ PAPER_JAR = `paper-${PAPER_VERSION}.jar`;
56
+ PAPER_PATH = resolve(CACHE, PAPER_JAR);
57
+ }
58
+
59
+ // Config hash — detect changes and recreate dev server
60
+ const CONFIG_HASH_FILE = resolve(DEVDIR, '.config-hash');
61
+ function configHash() { return JSON.stringify({ paperUrl: PAPER_URL, paperVersion: PAPER_VERSION }); }
62
+
63
+ function checkConfigChanged() {
64
+ if (!existsSync(CONFIG_HASH_FILE)) return true;
65
+ try {
66
+ return readFileSync(CONFIG_HASH_FILE, 'utf-8').trim() !== configHash();
67
+ } catch { return true; }
68
+ }
69
+
70
+ function saveConfigHash() {
71
+ mkdirSync(DEVDIR, { recursive: true });
72
+ writeFileSync(CONFIG_HASH_FILE, configHash());
73
+ }
74
+
75
+ if (checkConfigChanged()) {
76
+ if (existsSync(SERVER)) {
77
+ console.log(' Paper config changed — recreating dev server...');
78
+ rmSync(SERVER, { recursive: true, force: true });
79
+ }
80
+ saveConfigHash();
81
+ }
82
+
83
+ const c = { r: '\x1b[0m', b: '\x1b[1m', d: '\x1b[2m', g: '\x1b[32m', y: '\x1b[33m', B: '\x1b[34m', C: '\x1b[36m', R: '\x1b[31m', ok: '\x1b[32m✓\x1b[0m', fail: '\x1b[31m✗\x1b[0m', info: '\x1b[36mⓘ\x1b[0m', warn: '\x1b[33m⚠\x1b[0m' };
84
+ const log = (msg, color = '') => console.log(`${c.d}[${new Date().toLocaleTimeString()}]${c.r} ${color}${msg}${c.r}`);
85
+ const ok = msg => log(`${c.ok} ${msg}`, c.g);
86
+ const fail = msg => log(`${c.fail} ${msg}`, c.R);
87
+ const info = msg => log(`${c.info} ${msg}`, c.C);
88
+ const warn = msg => log(`${c.warn} ${msg}`, c.y);
89
+
90
+ let proc = null;
91
+ let wss = null;
92
+
93
+ // ── Graceful shutdown ────────────────────────────────────────────
94
+ let cleaning = false;
95
+ function cleanup() {
96
+ if (cleaning) return;
97
+ cleaning = true;
98
+ if (wss) { try { wss.close(); } catch {} }
99
+ if (proc && !proc.killed) {
100
+ try { proc.stdin.write('stop\n'); } catch {}
101
+ const killer = setTimeout(() => { if (proc && !proc.killed) try { proc.kill('SIGKILL'); } catch {} }, 10000);
102
+ proc.on('close', () => { clearTimeout(killer); process.exit(0); });
103
+ } else {
104
+ process.exit(0);
105
+ }
106
+ }
107
+ process.on('SIGINT', cleanup);
108
+ process.on('SIGTERM', cleanup);
109
+ process.on('beforeExit', () => { if (proc && !proc.killed) try { proc.kill(); } catch {} });
110
+
111
+ // ── WebSocket Server ────────────────────────────────────────────
112
+ function startWebSocket() {
113
+ const server = createServer();
114
+ wss = new WebSocketServer({ server });
115
+ wss.on('connection', (ws) => {
116
+ info('Java runtime connected');
117
+ ws.on('message', async (data) => {
118
+ try {
119
+ const msg = JSON.parse(data.toString());
120
+ if (msg.type === 'js-error') {
121
+ await printFormattedError(msg);
122
+ }
123
+ } catch (e) { warn('Error processing message: ' + (e?.message || e)); }
124
+ });
125
+ ws.on('close', () => info('Java runtime disconnected'));
126
+ });
127
+ server.listen(WS_PORT, () => {
128
+ info(`WebSocket server on port ${WS_PORT}`);
129
+ });
130
+ }
131
+
132
+ function broadcast(msg) {
133
+ if (!wss) return;
134
+ const data = JSON.stringify(msg);
135
+ wss.clients.forEach((ws) => {
136
+ if (ws.readyState === 1) ws.send(data);
137
+ });
138
+ }
139
+
140
+ // ── Download ────────────────────────────────────────────────────
141
+ function download(url, dest) {
142
+ return new Promise((resolve, reject) => {
143
+ const f = createWriteStream(dest);
144
+ https.get(url, res => {
145
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { f.close(); download(res.headers.location, dest).then(resolve).catch(reject); return; }
146
+ if (res.statusCode !== 200) { f.close(); reject(new Error(`HTTP ${res.statusCode}`)); return; }
147
+ const total = parseInt(res.headers['content-length'], 10);
148
+ let dl = 0;
149
+ res.on('data', chunk => { dl += chunk.length; if (total && !HEADLESS) process.stdout.write(`\r ${c.B}Downloading...${c.r} ${(dl / 1024 / 1024).toFixed(1)}MB / ${(total / 1024 / 1024).toFixed(1)}MB`); });
150
+ res.pipe(f);
151
+ f.on('finish', () => { f.close(); if (!HEADLESS) process.stdout.write('\n'); resolve(); });
152
+ }).on('error', e => { f.close(); reject(e); });
153
+ });
154
+ }
155
+
156
+ async function ensurePaper() {
157
+ if (PAPER_URL) {
158
+ // User-specified URL or file path
159
+ if (PAPER_URL.startsWith('http://') || PAPER_URL.startsWith('https://')) {
160
+ if (existsSync(PAPER_PATH) && statSync(PAPER_PATH).size > 1_000_000) { if (!HEADLESS) ok('Paper found in cache'); return; }
161
+ mkdirSync(CACHE, { recursive: true });
162
+ if (!HEADLESS) info('Downloading Paper...');
163
+ try { await download(PAPER_URL, PAPER_PATH); if (!HEADLESS) ok('Paper downloaded'); }
164
+ catch (e) { fail('Download failed: ' + e.message); console.log(' Manually: ' + PAPER_PATH); process.exit(1); }
165
+ } else {
166
+ if (!existsSync(PAPER_PATH)) { fail(`Paper JAR not found: ${PAPER_PATH}`); process.exit(1); }
167
+ if (!HEADLESS) ok('Paper found at specified path');
168
+ }
169
+ } else {
170
+ if (existsSync(PAPER_PATH) && statSync(PAPER_PATH).size > 1_000_000) { if (!HEADLESS) ok('Paper found in cache'); return; }
171
+ mkdirSync(CACHE, { recursive: true });
172
+ info('Downloading Paper...');
173
+ try {
174
+ const apiUrl = `https://api.papermc.io/v2/projects/paper/versions/${PAPER_VERSION}`;
175
+ const versions = await fetch(apiUrl).then(r => r.json());
176
+ const latestBuild = versions.builds[versions.builds.length - 1];
177
+ const dlUrl = `https://api.papermc.io/v2/projects/paper/versions/${PAPER_VERSION}/builds/${latestBuild}/downloads/paper-${PAPER_VERSION}-${latestBuild}.jar`;
178
+ await download(dlUrl, PAPER_PATH);
179
+ if (!HEADLESS) ok('Paper downloaded');
180
+ } catch (e) {
181
+ fail('Download failed: ' + e.message);
182
+ console.log(' Manually download to: ' + PAPER_PATH);
183
+ console.log(' Or set "paperJar" in yeow.config.json dev section');
184
+ process.exit(1);
185
+ }
186
+ }
187
+ }
188
+
189
+ function buildPlugin() {
190
+ info('Building plugin...');
191
+ try { execSync('node .yeow/build.js', { cwd: ROOT, stdio: 'inherit', env: { ...process.env, YEOW_DEV: 'true' } }); ok('Plugin built'); }
192
+ catch (e) { fail('Build failed: ' + e.message); process.exit(1); }
193
+ }
194
+
195
+ function copyToPlugins(src, label) {
196
+ const d = resolve(SERVER, 'plugins'); mkdirSync(d, { recursive: true });
197
+ copyFileSync(src, resolve(d, basename(src))); ok(`${label} copied`);
198
+ }
199
+
200
+ function copyToYeowDir(src, label) {
201
+ const d = resolve(SERVER, 'plugins', 'Yeow'); mkdirSync(d, { recursive: true });
202
+ copyFileSync(src, resolve(d, basename(src))); ok(`${label} copied`);
203
+ }
204
+
205
+ /** Remove a previously deployed dev plugin JAR so it cannot conflict with the .yeow.zip in plugins/Yeow/. */
206
+ function removeStaleDevJar() {
207
+ const stale = resolve(SERVER, 'plugins', `${cfg.name}-${cfg.version}.jar`);
208
+ if (existsSync(stale)) {
209
+ rmSync(stale, { force: true });
210
+ warn(`Removed stale dev JAR: ${basename(stale)} (conflicts with plugins/Yeow .yeow.zip)`);
211
+ }
212
+ }
213
+
214
+ async function initServer() {
215
+ const jar = resolve(SERVER, PAPER_JAR);
216
+ if (!existsSync(jar)) copyFileSync(PAPER_PATH, jar);
217
+ const eula = resolve(SERVER, 'eula.txt');
218
+ if (existsSync(eula) && readFileSync(eula, 'utf-8').includes('eula=true')) return;
219
+
220
+ if (EULA) {
221
+ if (!existsSync(resolve(SERVER, 'server.properties'))) {
222
+ info('Initializing...');
223
+ await new Promise(r => { const p = spawn('java', ['-Xmx4G', '-Xms4G', '-jar', jar, '--nogui'], { cwd: SERVER, stdio: ['pipe', 'inherit', 'inherit'] }); setTimeout(() => { p.kill(); r(); }, 120000); p.on('exit', r); p.on('error', r); });
224
+ }
225
+ writeFileSync(eula, 'eula=true\n'); ok('EULA auto-accepted');
226
+ } else {
227
+ console.log(`\n Press Enter to accept Mojang EULA:`);
228
+ await new Promise(r => process.stdin.once('data', () => { writeFileSync(eula, 'eula=true\n'); ok('EULA accepted'); r(); }));
229
+ }
230
+ }
231
+
232
+ function serverProps(port) {
233
+ const f = resolve(SERVER, 'server.properties');
234
+ const m = { 'server-port': String(port), 'online-mode': 'false', 'spawn-protection': '0', 'enable-command-block': 'true', 'max-players': '10', 'difficulty': 'easy', 'motd': cfg.name || 'Yeow Dev' };
235
+ if (existsSync(f)) {
236
+ for (const l of readFileSync(f, 'utf-8').split('\n')) { const eq = l.indexOf('='); if (eq > 0 && !l.startsWith('#')) { const k = l.substring(0, eq).trim(); if (!m[k]) m[k] = l.substring(eq + 1).trim(); } }
237
+ }
238
+ let out = ''; for (const [k, v] of Object.entries(m)) out += `${k}=${v}\n`;
239
+ writeFileSync(f, out);
240
+ }
241
+
242
+ // ── Hot Reload via WebSocket ────────────────────────────────────
243
+ function startHotReload() {
244
+ const srcDir = resolve(ROOT, 'src');
245
+ const assetsDir = resolve(ROOT, 'assets');
246
+ if (!existsSync(srcDir)) return;
247
+
248
+ let timer = null;
249
+ let building = false;
250
+ const ext = existsSync(resolve(srcDir, 'index.ts')) ? 'ts' : 'js';
251
+
252
+ const rebuildAndNotify = () => {
253
+ if (timer) clearTimeout(timer);
254
+ timer = setTimeout(() => {
255
+ if (building) return;
256
+ building = true;
257
+ info('Source changed, rebuilding...');
258
+ try {
259
+ execSync('node .yeow/build.js', { cwd: ROOT, stdio: 'pipe', env: { ...process.env, YEOW_DEV: 'true' } });
260
+ const compiled = resolve(ROOT, 'dist', '.dev', 'main.js');
261
+ if (existsSync(compiled)) {
262
+ const devAssets = resolve(ROOT, 'dist', '.dev', '.assets');
263
+ // 权限随热重载一并刷新:构建脚本(build.js)已把 computedPermissions 回写至 yeow.config.json
264
+ let permissions = [];
265
+ try {
266
+ permissions = JSON.parse(readFileSync(resolve(ROOT, 'yeow.config.json'), 'utf-8')).computedPermissions || [];
267
+ } catch {}
268
+ broadcast({
269
+ type: 'hot-reload',
270
+ plugin: cfg.name,
271
+ codeFile: compiled.replace(/\\/g, '/'),
272
+ assetsDir: existsSync(devAssets) ? devAssets.replace(/\\/g, '/') : null,
273
+ permissions,
274
+ });
275
+ _consumer = null;
276
+ ok('Hot reload sent via WebSocket' + (permissions.length ? ` (permissions ${permissions.length})` : ''));
277
+ }
278
+ } catch (e) {
279
+ fail('Build failed: ' + e.message);
280
+ broadcast({ type: 'build-error', plugin: cfg.name, error: e.message });
281
+ }
282
+ building = false;
283
+ }, 300);
284
+ };
285
+
286
+ watch(srcDir, { recursive: true }, (event, file) => {
287
+ if (!file || !file.endsWith('.' + ext)) return;
288
+ rebuildAndNotify();
289
+ });
290
+
291
+ if (existsSync(assetsDir)) {
292
+ watch(assetsDir, { recursive: true }, (event, file) => {
293
+ rebuildAndNotify();
294
+ });
295
+ }
296
+
297
+ // Worker 源码目录(dev.worker[].entry 所在目录)变化 → 重建(worker 随主插件热重载重建)
298
+ const workerCfg = (cfg.dev && cfg.dev.worker) || [];
299
+ const watchedWorkerDirs = new Set();
300
+ for (const w of workerCfg) {
301
+ if (!w?.entry) continue;
302
+ const dir = resolve(ROOT, dirname(w.entry));
303
+ if (!existsSync(dir) || watchedWorkerDirs.has(dir)) continue;
304
+ watchedWorkerDirs.add(dir);
305
+ watch(dir, { recursive: true }, (event, file) => {
306
+ if (!file || !/\.(ts|js|mjs)$/.test(file)) return;
307
+ rebuildAndNotify();
308
+ });
309
+ }
310
+
311
+ info(`Watching src/ + assets/${watchedWorkerDirs.size > 0 ? ' + worker dirs' : ''} for changes (WebSocket hot reload)`);
312
+ }
313
+
314
+ // ── Source-Mapped Error Display ─────────────────────────────────
315
+ let _consumer = null;
316
+ async function getSourceMapConsumer() {
317
+ if (_consumer) return _consumer;
318
+ const mapFile = resolve(ROOT, 'dist', '.dev', 'main.js.map');
319
+ if (!existsSync(mapFile)) return null;
320
+ try {
321
+ const raw = JSON.parse(readFileSync(mapFile, 'utf-8'));
322
+ _consumer = await new SourceMapConsumer(raw);
323
+ return _consumer;
324
+ } catch { return null; }
325
+ }
326
+
327
+ /** 查找 Worker 的 source-map 文件(产物位于 dist/.dev/.assets/<id>/worker/<name>.js.map)。 */
328
+ function findWorkerMapFile(workerName) {
329
+ const assetsRoot = resolve(ROOT, 'dist', '.dev', '.assets');
330
+ if (!existsSync(assetsRoot)) return null;
331
+ for (const id of readdirSync(assetsRoot)) {
332
+ const mapFile = resolve(assetsRoot, id, 'worker', workerName + '.js.map');
333
+ if (existsSync(mapFile)) return mapFile;
334
+ }
335
+ return null;
336
+ }
337
+
338
+ /** Worker 的 source-map(产物位于 dist/.dev/.assets/<id>/worker/<name>.js(.map))。 */
339
+ let _workerConsumers = {};
340
+ async function getWorkerSourceMapConsumer(workerName) {
341
+ if (_workerConsumers[workerName]) return _workerConsumers[workerName];
342
+ try {
343
+ const mapFile = findWorkerMapFile(workerName);
344
+ if (mapFile) {
345
+ const raw = JSON.parse(readFileSync(mapFile, 'utf-8'));
346
+ _workerConsumers[workerName] = await new SourceMapConsumer(raw);
347
+ return _workerConsumers[workerName];
348
+ }
349
+ } catch { /* 未找到 */ }
350
+ _workerConsumers[workerName] = null;
351
+ return null;
352
+ }
353
+
354
+ async function printFormattedError(err) {
355
+ const c = { r: '\x1b[0m', R: '\x1b[31m', Y: '\x1b[33m', C: '\x1b[36m', B: '\x1b[1m', D: '\x1b[2m', g: '\x1b[32m' };
356
+ const isWorker = err.origin && err.origin !== 'main';
357
+ let out = isWorker
358
+ ? `\n${c.R}${c.B} JS Error in Worker [${err.origin}]${c.r}\n`
359
+ : `\n${c.R}${c.B} JS Error [${err.plugin}]${c.r}\n`;
360
+ if (err.context) out += ` ${c.D}context: ${err.context}${c.r}\n`;
361
+ out += ` ${c.Y}${err.message}${c.r}\n`;
362
+
363
+ // 产物文件名:主插件 main.js;Worker <name>.js
364
+ const bundleName = isWorker ? err.origin + '.js' : 'main.js';
365
+ const bundleRe = new RegExp(bundleName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ':(\\d+):(\\d+)');
366
+ const hasBundle = err.stack?.match(bundleRe) || err.fileName === bundleName;
367
+ let consumer = null;
368
+ if (hasBundle) {
369
+ consumer = isWorker ? await getWorkerSourceMapConsumer(err.origin) : await getSourceMapConsumer();
370
+ }
371
+ if (hasBundle && !consumer) {
372
+ const mapFile = isWorker ? findWorkerMapFile(err.origin) : resolve(ROOT, 'dist', '.dev', 'main.js.map');
373
+ out += ` ${c.D}(source-map not found: ${mapFile && existsSync(mapFile) ? 'exists but failed to parse' : 'missing at ' + (mapFile || '.assets/<id>/worker/' + err.origin + '.js.map')})${c.r}\n`;
374
+ }
375
+
376
+ const frames = [];
377
+ if (err.stack) {
378
+ for (const rawLine of err.stack.split('\n')) {
379
+ const m = rawLine.match(/at\s+(?:\S+\s+)?\(?([^\\/\s()]+\.js):(\d+):(\d+)\)?/);
380
+ if (m && consumer && (isWorker ? m[1] === bundleName : m[1] === 'main.js')) {
381
+ const orig = consumer.originalPositionFor({ line: parseInt(m[2]), column: parseInt(m[3]) });
382
+ if (!orig?.source) {
383
+ const orig2 = consumer.originalPositionFor({ line: parseInt(m[2]), column: parseInt(m[3]) - 1 });
384
+ if (orig2?.source) { orig.source = orig2.source; orig.line = orig2.line; orig.column = orig2.column; }
385
+ }
386
+ frames.push({ orig, raw: rawLine });
387
+ } else {
388
+ frames.push({ raw: rawLine });
389
+ }
390
+ }
391
+ }
392
+
393
+ let ctxFrame = frames.find(f => f.orig?.source?.match(/[\\/]src[\\/]/) && !f.orig.source.includes('node_modules'))?.orig || null;
394
+
395
+ if (ctxFrame?.source && ctxFrame.line && consumer) {
396
+ const srcPath = ctxFrame.source.replace(/^\.\.\/\.\.\//, '');
397
+ const ctxRaw = frames.find(f => f.orig === ctxFrame)?.raw || '';
398
+ const fnM = ctxRaw.match(/at\s+(\S+)\s+\(/);
399
+ const fnS = fnM ? fnM[1] + ' ' : '';
400
+ out += ` ${c.C}at ${fnS}(${srcPath}:${ctxFrame.line}:${ctxFrame.column})${c.r}\n`;
401
+ const content = consumer.sourceContentFor(ctxFrame.source);
402
+ if (content) {
403
+ const lines = content.split('\n');
404
+ const start = Math.max(0, ctxFrame.line - 3);
405
+ const end = Math.min(lines.length, ctxFrame.line + 2);
406
+ for (let i = start; i < end; i++) {
407
+ const prefix = i === ctxFrame.line - 1 ? c.R + ' →' : ' ';
408
+ out += ` ${prefix} ${c.D}${String(i + 1).padStart(4)}|${c.r} ${lines[i]}\n`;
409
+ if (i === ctxFrame.line - 1 && ctxFrame.column > 0) {
410
+ const col = Math.max(0, ctxFrame.column);
411
+ const indent = ' '.repeat(String(i + 1).padStart(4).length) + '| ';
412
+ const caret = ' '.repeat(col) + c.R + '^' + c.r;
413
+ out += ` ${c.R} →${c.r} ${indent}${caret}\n`;
414
+ }
415
+ }
416
+ }
417
+ }
418
+
419
+ if (frames.length > 0) {
420
+ out += ` ${c.D}Stack:${c.r}\n`;
421
+ for (const f of frames) {
422
+ if (f.orig?.source) {
423
+ const srcPath = f.orig.source.replace(/^\.\.\/\.\.\//, '');
424
+ const fnMatch = f.raw.match(/at\s+(\S+)\s+\(/);
425
+ const fn = fnMatch ? fnMatch[1] + ' ' : '';
426
+ const isUser = srcPath.startsWith('src/');
427
+ const style = isUser ? c.B + c.g : c.D;
428
+ out += ` ${style} at ${fn}(${srcPath}:${f.orig.line}:${f.orig.column})${c.r}\n`;
429
+ } else {
430
+ const internal = f.raw.includes('init.js') || f.raw.includes('unknown.js') ? ' (internal)' : '';
431
+ out += `${c.D} ${f.raw}${internal}${c.r}\n`;
432
+ }
433
+ }
434
+ }
435
+ console.log(out);
436
+ }
437
+
438
+ function startServer() {
439
+ // 注意:不设 -Dstdout.encoding=UTF-8——主路径 stdout 直接继承控制台(stdio: inherit),
440
+ // 强制 UTF-8 会在 GBK 控制台(中文 Windows)产生中文乱码;不设时 JVM 自动匹配控制台编码。
441
+ // headless 路径(管道 + readline 解码)才需要 UTF-8 强制。
442
+ const jvmArgs = ['-Xmx4G', '-Xms4G', '-Dfile.encoding=UTF-8', '-Dyeow.dev=true', '-Dyeow.ws.port=' + WS_PORT];
443
+ info(`\nStarting Paper ${PAPER_VERSION} server...`);
444
+ proc = spawn('java', [...jvmArgs, '-jar', resolve(SERVER, PAPER_JAR), '--nogui'], { cwd: SERVER, stdio: ['pipe', 'inherit', 'inherit'] });
445
+ proc.on('exit', code => { warn(`Server exited (${code})`); if (wss) wss.close(); process.exit(0); });
446
+ process.stdin.on('data', d => { if (proc && !proc.killed) try { proc.stdin.write(d); } catch {} });
447
+ if (STOP) { info(`Auto-stop in ${STOP}s`); setTimeout(() => { warn('Auto-stop'); cleanup(); }, STOP * 1000); }
448
+ }
449
+
450
+ async function main() {
451
+ console.log(`\n${c.b}${c.B} Yeow Dev Server${c.r}\n`);
452
+ if (!existsSync(RUNTIME)) { fail(`Runtime JAR not found: ${RUNTIME}`); process.exit(1); }
453
+
454
+ if (HEADLESS) { await runHeadless(); return; }
455
+
456
+ startWebSocket();
457
+ await ensurePaper();
458
+ mkdirSync(SERVER, { recursive: true });
459
+ await initServer();
460
+ serverProps(cfg.dev?.port || 17367);
461
+ buildPlugin();
462
+ removeStaleDevJar();
463
+ copyToYeowDir(resolve(ROOT, 'dist', 'plugins', `${cfg.name}-${cfg.version}.yeow.zip`), 'Plugin (.yeow.zip → plugins/Yeow/)');
464
+ copyToPlugins(RUNTIME, 'Runtime');
465
+
466
+ startHotReload();
467
+ startServer();
468
+ }
469
+
470
+ // ── AI 工作流(headless)─────────────────────────────────────────
471
+ // 适合 AI 代理/CI:--eula 自动接受 → 下载 → 启动 → 检测加载完成 →
472
+ // 等待 --wait 秒后命令自动结束(--keep 保留服务器子进程,日志见 --outfile)。
473
+ async function runHeadless() {
474
+ if (!EULA) {
475
+ fail('AI 模式需要 --eula(自动接受 EULA)');
476
+ process.exit(1);
477
+ }
478
+ const log = OUTFILE ? createWriteStream(OUTFILE, { flags: 'a' }) : null;
479
+ const out = (line) => { if (log) log.write(line + '\n'); else console.log(line); };
480
+
481
+ // 流程级超时:覆盖下载/初始化/启动/加载全程(--timeout,默认 2m)
482
+ let done = false;
483
+ const finish = (code) => {
484
+ if (waitTimer) clearTimeout(waitTimer);
485
+ try { if (log) log.end(); } catch {}
486
+ process.exit(code);
487
+ };
488
+ const failTimer = setTimeout(() => {
489
+ if (done) return;
490
+ fail(`流程在 ${TIMEOUT}s 内未完成加载——请检查网络/依赖下载,或加大超时(--timeout=3m)`);
491
+ killProc();
492
+ finish(1);
493
+ }, TIMEOUT * 1000);
494
+
495
+ info('正在下载/准备服务端…');
496
+ await ensurePaper();
497
+ mkdirSync(SERVER, { recursive: true });
498
+ await initServer();
499
+ serverProps(cfg.dev?.port || 17367);
500
+ buildPlugin();
501
+ removeStaleDevJar();
502
+ copyToYeowDir(resolve(ROOT, 'dist', 'plugins', `${cfg.name}-${cfg.version}.yeow.zip`), 'Plugin (.yeow.zip → plugins/Yeow/)');
503
+ copyToPlugins(RUNTIME, 'Runtime');
504
+
505
+ // 编码:确保子进程 stdout 按 UTF-8 输出(Windows 下避免中文字符乱码)
506
+ const jvmArgs = ['-Xmx4G', '-Xms4G', '-Dfile.encoding=UTF-8', '-Dstdout.encoding=UTF-8',
507
+ '-Dyeow.dev=true', '-Dyeow.ws.port=' + WS_PORT];
508
+ info(`正在启动 Paper ${PAPER_VERSION}...`);
509
+ proc = spawn('java', [...jvmArgs, '-jar', resolve(SERVER, PAPER_JAR), '--nogui'], { cwd: SERVER, stdio: ['ignore', 'pipe', 'pipe'] });
510
+ info(`Server PID: ${proc.pid}`);
511
+
512
+ let started = false, waitTimer = null;
513
+
514
+ const onLine = (line) => {
515
+ out(line);
516
+ if (!started && line.includes('Starting org.bukkit.craftbukkit.Main')) {
517
+ started = true;
518
+ info('开始加载(Starting org.bukkit.craftbukkit.Main)');
519
+ }
520
+ if (!done && line.includes('Done (') && line.includes('For help')) {
521
+ done = true;
522
+ clearTimeout(failTimer);
523
+ info(`加载完成——等待 ${WAIT}s 后命令结束${KEEP ? '(--keep 保留服务器进程)' : '(关闭服务器进程)'}…`);
524
+ waitTimer = setTimeout(() => {
525
+ info(`等待结束。日志${OUTFILE ? ':' + OUTFILE : '输出于上方'};PID=${proc.pid}${KEEP ? '(服务器仍在运行,按需 kill)' : ''}`);
526
+ if (!KEEP) killProc();
527
+ finish(0);
528
+ }, WAIT * 1000);
529
+ }
530
+ };
531
+ createInterface({ input: proc.stdout }).on('line', onLine);
532
+ if (proc.stderr) createInterface({ input: proc.stderr }).on('line', (l) => out('[err] ' + l));
533
+
534
+ proc.on('exit', (code) => {
535
+ if (!done) fail(`服务器提前退出(code ${code})——见${OUTFILE ? '日志 ' + OUTFILE : '上方输出'}`);
536
+ finish(1);
537
+ });
538
+ }
539
+
540
+ function killProc() {
541
+ if (proc && !proc.killed) { try { proc.kill('SIGKILL'); } catch {} }
542
+ }
543
+
544
+ main().catch(e => { fail(e.message); process.exit(1); });