@playcraft/cli 0.0.10 → 0.0.12
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/dist/agent/agent.js +202 -0
- package/dist/agent/api-proxy.js +68 -0
- package/dist/agent/cloud-connection.js +233 -0
- package/dist/agent/cloud-connection.test.js +67 -0
- package/dist/agent/fs-backend.js +158 -0
- package/dist/agent/local-backend.js +359 -0
- package/dist/agent/local-backend.test.js +52 -0
- package/dist/commands/build.js +260 -58
- package/dist/commands/fix-ids.js +43 -0
- package/dist/commands/start.js +14 -129
- package/dist/commands/sync.js +40 -0
- package/dist/commands/upgrade.js +71 -0
- package/dist/config.js +2 -0
- package/dist/fs-handler.js +21 -0
- package/dist/index.js +51 -0
- package/dist/server.js +28 -0
- package/dist/socket.js +80 -21
- package/dist/sync/sync-engine.js +213 -0
- package/dist/sync/sync-manager.js +62 -0
- package/dist/sync/sync-manager.test.js +80 -0
- package/dist/utils/package-manager.js +37 -0
- package/dist/utils/updater.js +89 -0
- package/dist/utils/version-checker.js +84 -0
- package/package.json +11 -3
package/dist/commands/start.js
CHANGED
|
@@ -4,15 +4,10 @@ import { fileURLToPath } from 'url';
|
|
|
4
4
|
import { loadConfig } from '../config.js';
|
|
5
5
|
import { ProcessManager } from '../process-manager.js';
|
|
6
6
|
import { isPortAvailable, findAvailablePort } from '../port-utils.js';
|
|
7
|
-
import { Logger } from '../logger.js';
|
|
8
|
-
import { createServer } from '../server.js';
|
|
9
|
-
import { SocketServer } from '../socket.js';
|
|
10
|
-
import { Watcher } from '../watcher.js';
|
|
11
|
-
import { FSHandler } from '../fs-handler.js';
|
|
12
|
-
import http from 'http';
|
|
13
7
|
import pc from 'picocolors';
|
|
14
8
|
import ora from 'ora';
|
|
15
9
|
import inquirer from 'inquirer';
|
|
10
|
+
import { PlayCraftAgent } from '../agent/agent.js';
|
|
16
11
|
const __filename = fileURLToPath(import.meta.url);
|
|
17
12
|
const __dirname = path.dirname(__filename);
|
|
18
13
|
export async function startCommand(options) {
|
|
@@ -23,6 +18,7 @@ export async function startCommand(options) {
|
|
|
23
18
|
token: options.token,
|
|
24
19
|
dir: options.dir,
|
|
25
20
|
port: options.port ? parseInt(options.port) : undefined,
|
|
21
|
+
mode: options.mode || undefined,
|
|
26
22
|
});
|
|
27
23
|
if (!config.projectId) {
|
|
28
24
|
spinner.fail('项目 ID 未设置');
|
|
@@ -101,6 +97,7 @@ async function startDaemon(config) {
|
|
|
101
97
|
PLAYCRAFT_PORT: config.port.toString(),
|
|
102
98
|
PLAYCRAFT_DIR: config.dir,
|
|
103
99
|
PLAYCRAFT_DAEMON: 'true',
|
|
100
|
+
PLAYCRAFT_MODE: config.mode || 'full-local',
|
|
104
101
|
},
|
|
105
102
|
});
|
|
106
103
|
child.unref();
|
|
@@ -126,76 +123,11 @@ async function startDaemon(config) {
|
|
|
126
123
|
}
|
|
127
124
|
}
|
|
128
125
|
async function startForeground(config) {
|
|
129
|
-
const
|
|
130
|
-
await
|
|
131
|
-
console.log(pc.cyan(`\n🚀 PlayCraft Agent 启动中...\n`));
|
|
132
|
-
console.log(`${pc.bold('项目 ID:')} ${config.projectId || pc.yellow('未设置')}`);
|
|
133
|
-
console.log(`${pc.bold('目录:')} ${config.dir}`);
|
|
134
|
-
console.log(`${pc.bold('端口:')} ${config.port}\n`);
|
|
135
|
-
const fsHandler = new FSHandler(config);
|
|
136
|
-
const app = createServer(config, fsHandler);
|
|
137
|
-
const server = http.createServer(app);
|
|
138
|
-
// Connection status indicator
|
|
139
|
-
let lastConnectionCount = 0;
|
|
140
|
-
const socketServer = new SocketServer(server, config, async (count) => {
|
|
141
|
-
if (count > lastConnectionCount) {
|
|
142
|
-
const message = `✅ 编辑器已连接 (共 ${count} 个连接)`;
|
|
143
|
-
await logger.info(message);
|
|
144
|
-
console.log(pc.green(message));
|
|
145
|
-
}
|
|
146
|
-
else if (count < lastConnectionCount) {
|
|
147
|
-
const message = count === 0
|
|
148
|
-
? '⚠️ 编辑器已断开连接,等待重新连接...'
|
|
149
|
-
: `⚠️ 连接数减少 (剩余 ${count} 个连接)`;
|
|
150
|
-
await logger.info(message);
|
|
151
|
-
console.log(pc.yellow(message));
|
|
152
|
-
}
|
|
153
|
-
lastConnectionCount = count;
|
|
154
|
-
});
|
|
155
|
-
const watcher = new Watcher(config, async (filePath, type) => {
|
|
156
|
-
const message = `[${type.toUpperCase()}] ${filePath}`;
|
|
157
|
-
await logger.info(message);
|
|
158
|
-
socketServer.notifyFileChange(filePath, type);
|
|
159
|
-
});
|
|
160
|
-
server.listen(config.port, async () => {
|
|
161
|
-
await logger.info(`Local server running at http://localhost:${config.port}`);
|
|
162
|
-
console.log(pc.green(`✅ 本地服务运行在 http://localhost:${config.port}`));
|
|
163
|
-
console.log(pc.dim('等待编辑器连接...\n'));
|
|
164
|
-
});
|
|
165
|
-
// 优雅关闭
|
|
126
|
+
const agent = new PlayCraftAgent(config, false);
|
|
127
|
+
await agent.start();
|
|
166
128
|
const shutdown = async () => {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const forceExitTimeout = setTimeout(() => {
|
|
170
|
-
console.log(pc.red('强制退出...'));
|
|
171
|
-
process.exit(1);
|
|
172
|
-
}, 3000); // 3 seconds timeout
|
|
173
|
-
try {
|
|
174
|
-
// Close watcher
|
|
175
|
-
await watcher.close();
|
|
176
|
-
// Close WebSocket connections
|
|
177
|
-
socketServer.destroy();
|
|
178
|
-
// Close HTTP server
|
|
179
|
-
await new Promise((resolve) => {
|
|
180
|
-
server.close(() => {
|
|
181
|
-
resolve();
|
|
182
|
-
});
|
|
183
|
-
// Force close if it takes too long
|
|
184
|
-
setTimeout(() => {
|
|
185
|
-
resolve();
|
|
186
|
-
}, 1000);
|
|
187
|
-
});
|
|
188
|
-
// Close logger
|
|
189
|
-
await logger.close();
|
|
190
|
-
clearTimeout(forceExitTimeout);
|
|
191
|
-
console.log(pc.green('✅ Agent 已关闭'));
|
|
192
|
-
process.exit(0);
|
|
193
|
-
}
|
|
194
|
-
catch (error) {
|
|
195
|
-
clearTimeout(forceExitTimeout);
|
|
196
|
-
console.error(pc.red('关闭时出错:'), error);
|
|
197
|
-
process.exit(1);
|
|
198
|
-
}
|
|
129
|
+
await agent.stop();
|
|
130
|
+
process.exit(0);
|
|
199
131
|
};
|
|
200
132
|
process.on('SIGINT', shutdown);
|
|
201
133
|
process.on('SIGTERM', shutdown);
|
|
@@ -207,6 +139,7 @@ export async function startInternal() {
|
|
|
207
139
|
const port = parseInt(process.env.PLAYCRAFT_PORT || '2468');
|
|
208
140
|
const dir = process.env.PLAYCRAFT_DIR || process.cwd();
|
|
209
141
|
const isDaemon = process.env.PLAYCRAFT_DAEMON === 'true';
|
|
142
|
+
const mode = process.env.PLAYCRAFT_MODE || 'full-local';
|
|
210
143
|
if (!projectId) {
|
|
211
144
|
console.error('PLAYCRAFT_PROJECT_ID 环境变量未设置');
|
|
212
145
|
process.exit(1);
|
|
@@ -216,68 +149,20 @@ export async function startInternal() {
|
|
|
216
149
|
token,
|
|
217
150
|
dir,
|
|
218
151
|
port,
|
|
152
|
+
mode,
|
|
219
153
|
};
|
|
220
|
-
const
|
|
221
|
-
await
|
|
222
|
-
await logger.info(`Starting PlayCraft Agent for project: ${config.projectId || 'default'}`);
|
|
223
|
-
await logger.info(`Directory: ${config.dir}`);
|
|
224
|
-
await logger.info(`Port: ${config.port}`);
|
|
225
|
-
const fsHandler = new FSHandler(config);
|
|
226
|
-
const app = createServer(config, fsHandler);
|
|
227
|
-
const server = http.createServer(app);
|
|
228
|
-
// Connection status tracking for daemon mode
|
|
229
|
-
let lastConnectionCount = 0;
|
|
230
|
-
const socketServer = new SocketServer(server, config, async (count) => {
|
|
231
|
-
if (count > lastConnectionCount) {
|
|
232
|
-
await logger.info(`Editor connected (${count} connection(s))`);
|
|
233
|
-
}
|
|
234
|
-
else if (count < lastConnectionCount) {
|
|
235
|
-
await logger.info(count === 0 ? 'Editor disconnected' : `Connection decreased (${count} remaining)`);
|
|
236
|
-
}
|
|
237
|
-
lastConnectionCount = count;
|
|
238
|
-
});
|
|
239
|
-
const watcher = new Watcher(config, async (filePath, type) => {
|
|
240
|
-
await logger.info(`[${type.toUpperCase()}] ${filePath}`);
|
|
241
|
-
socketServer.notifyFileChange(filePath, type);
|
|
242
|
-
});
|
|
243
|
-
server.listen(config.port, async () => {
|
|
244
|
-
await logger.info(`Local server running at http://localhost:${config.port}`);
|
|
245
|
-
});
|
|
154
|
+
const agent = new PlayCraftAgent(config, isDaemon);
|
|
155
|
+
await agent.start();
|
|
246
156
|
// 保存 PID
|
|
247
157
|
await ProcessManager.savePid(config.projectId || 'default', process.pid);
|
|
248
|
-
// 优雅关闭
|
|
249
158
|
const shutdown = async () => {
|
|
250
|
-
await logger.info('Shutting down agent...');
|
|
251
|
-
// Set a timeout to force exit
|
|
252
|
-
const forceExitTimeout = setTimeout(() => {
|
|
253
|
-
process.exit(1);
|
|
254
|
-
}, 3000);
|
|
255
159
|
try {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
socketServer.destroy();
|
|
260
|
-
// Close HTTP server
|
|
261
|
-
await new Promise((resolve) => {
|
|
262
|
-
server.close(() => {
|
|
263
|
-
resolve();
|
|
264
|
-
});
|
|
265
|
-
setTimeout(() => {
|
|
266
|
-
resolve();
|
|
267
|
-
}, 1000);
|
|
268
|
-
});
|
|
269
|
-
// Remove PID file
|
|
160
|
+
await agent.stop();
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
270
163
|
await ProcessManager.removePid(config.projectId || 'default');
|
|
271
|
-
// Close logger
|
|
272
|
-
await logger.close();
|
|
273
|
-
clearTimeout(forceExitTimeout);
|
|
274
164
|
process.exit(0);
|
|
275
165
|
}
|
|
276
|
-
catch (error) {
|
|
277
|
-
clearTimeout(forceExitTimeout);
|
|
278
|
-
await logger.error(`Error during shutdown: ${error}`);
|
|
279
|
-
process.exit(1);
|
|
280
|
-
}
|
|
281
166
|
};
|
|
282
167
|
process.on('SIGINT', shutdown);
|
|
283
168
|
process.on('SIGTERM', shutdown);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { loadConfig } from '../config.js';
|
|
2
|
+
import { createSyncEngineFromConfig } from '../sync/sync-engine.js';
|
|
3
|
+
import pc from 'picocolors';
|
|
4
|
+
export async function syncCommand(action, options) {
|
|
5
|
+
const config = await loadConfig({});
|
|
6
|
+
if (config.mode !== 'hybrid') {
|
|
7
|
+
console.log(pc.yellow('Sync 命令仅在 hybrid 模式下可用'));
|
|
8
|
+
console.log(pc.dim('当前模式: ' + (config.mode || 'full-local')));
|
|
9
|
+
console.log(pc.dim('请设置 mode: "hybrid" 并配置 url 和 token'));
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
if (!config.url || !config.token || !config.projectId) {
|
|
13
|
+
console.error(pc.red('hybrid 模式需要配置 url、token 和 projectId'));
|
|
14
|
+
console.log(pc.dim('请在 playcraft.agent.config.json 或环境变量中设置'));
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
const syncEngine = await createSyncEngineFromConfig();
|
|
19
|
+
switch (action) {
|
|
20
|
+
case 'push':
|
|
21
|
+
await syncEngine.push(options);
|
|
22
|
+
break;
|
|
23
|
+
case 'pull':
|
|
24
|
+
await syncEngine.pull(options);
|
|
25
|
+
break;
|
|
26
|
+
case 'status':
|
|
27
|
+
await syncEngine.status();
|
|
28
|
+
break;
|
|
29
|
+
default:
|
|
30
|
+
console.error(pc.red(`未知操作: ${action}`));
|
|
31
|
+
console.log(pc.yellow('可用: push, pull, status'));
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
37
|
+
console.error(pc.red(message));
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* upgrade 命令
|
|
3
|
+
* 检查并更新 CLI 到最新版本
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync } from 'fs';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import { dirname, join } from 'path';
|
|
8
|
+
import pc from 'picocolors';
|
|
9
|
+
import ora from 'ora';
|
|
10
|
+
import { getLatestVersion } from '../utils/version-checker.js';
|
|
11
|
+
import { promptForUpdate } from '../utils/updater.js';
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = dirname(__filename);
|
|
14
|
+
/**
|
|
15
|
+
* 获取 package.json
|
|
16
|
+
*/
|
|
17
|
+
function getPackageJson() {
|
|
18
|
+
return JSON.parse(readFileSync(join(__dirname, '..', '..', 'package.json'), 'utf-8'));
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* upgrade 命令处理函数
|
|
22
|
+
* @param options 命令选项
|
|
23
|
+
*/
|
|
24
|
+
export async function upgradeCommand(options) {
|
|
25
|
+
const packageJson = getPackageJson();
|
|
26
|
+
const currentVersion = packageJson.version;
|
|
27
|
+
console.log(pc.bold('\n📦 PlayCraft CLI 版本检查\n'));
|
|
28
|
+
console.log(`当前版本: ${pc.cyan(currentVersion)}`);
|
|
29
|
+
try {
|
|
30
|
+
const spinner = ora('正在检查最新版本...').start();
|
|
31
|
+
const latestVersion = await getLatestVersion(packageJson);
|
|
32
|
+
spinner.stop();
|
|
33
|
+
console.log(`最新版本: ${pc.green(latestVersion)}`);
|
|
34
|
+
if (currentVersion === latestVersion) {
|
|
35
|
+
console.log(pc.green('\n✓ 已是最新版本!\n'));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
// 比较版本号
|
|
39
|
+
const [currentMajor, currentMinor, currentPatch] = currentVersion.split('.').map(Number);
|
|
40
|
+
const [latestMajor, latestMinor, latestPatch] = latestVersion.split('.').map(Number);
|
|
41
|
+
let updateType = 'patch';
|
|
42
|
+
if (latestMajor > currentMajor) {
|
|
43
|
+
updateType = 'major';
|
|
44
|
+
}
|
|
45
|
+
else if (latestMinor > currentMinor) {
|
|
46
|
+
updateType = 'minor';
|
|
47
|
+
}
|
|
48
|
+
const typeLabels = {
|
|
49
|
+
major: '主版本',
|
|
50
|
+
minor: '次版本',
|
|
51
|
+
patch: '补丁版本',
|
|
52
|
+
};
|
|
53
|
+
console.log(pc.yellow(`\n⚠ 有新版本可用 (${typeLabels[updateType]}更新)\n`));
|
|
54
|
+
if (options.checkOnly) {
|
|
55
|
+
console.log(pc.yellow(`运行 ${pc.bold('playcraft upgrade')} 来更新到最新版本\n`));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
// 触发交互式更新
|
|
59
|
+
await promptForUpdate({
|
|
60
|
+
latest: latestVersion,
|
|
61
|
+
current: currentVersion,
|
|
62
|
+
type: updateType,
|
|
63
|
+
name: packageJson.name,
|
|
64
|
+
}, currentVersion);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
console.error(pc.red(`\n✗ 检查版本失败: ${error instanceof Error ? error.message : String(error)}\n`));
|
|
68
|
+
console.error(pc.gray('提示: 请检查网络连接或稍后重试\n'));
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
}
|
package/dist/config.js
CHANGED
|
@@ -8,6 +8,7 @@ export const ConfigSchema = z.object({
|
|
|
8
8
|
dir: z.string().default(process.cwd()),
|
|
9
9
|
port: z.number().default(2468),
|
|
10
10
|
url: z.string().optional(), // Cloud API URL
|
|
11
|
+
mode: z.enum(['full-local', 'hybrid']).default('full-local'),
|
|
11
12
|
});
|
|
12
13
|
const explorer = cosmiconfig('playcraft', {
|
|
13
14
|
searchPlaces: ['playcraft.config.json', 'playcraft.agent.config.json'],
|
|
@@ -24,6 +25,7 @@ export async function loadConfig(cliOptions) {
|
|
|
24
25
|
projectId: cliOptions.projectId || process.env.PLAYCRAFT_PROJECT_ID || agentConfig.projectId,
|
|
25
26
|
token: cliOptions.token || process.env.PLAYCRAFT_TOKEN || agentConfig.token,
|
|
26
27
|
port: cliOptions.port || (process.env.PLAYCRAFT_PORT ? parseInt(process.env.PLAYCRAFT_PORT) : undefined) || agentConfig.port,
|
|
28
|
+
mode: cliOptions.mode || process.env.PLAYCRAFT_MODE || agentConfig.mode,
|
|
27
29
|
});
|
|
28
30
|
// Resolve absolute path for dir
|
|
29
31
|
config.dir = path.resolve(process.cwd(), config.dir);
|
package/dist/fs-handler.js
CHANGED
|
@@ -80,4 +80,25 @@ export class FSHandler {
|
|
|
80
80
|
throw error;
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
|
+
async writeFile(relativePath, content) {
|
|
84
|
+
if (!this.isSafePath(relativePath)) {
|
|
85
|
+
throw new Error('Access denied: path is outside of the project directory');
|
|
86
|
+
}
|
|
87
|
+
const fullPath = path.join(this.config.dir, relativePath);
|
|
88
|
+
const dir = path.dirname(fullPath);
|
|
89
|
+
await fs.mkdir(dir, { recursive: true });
|
|
90
|
+
if (typeof content === 'string') {
|
|
91
|
+
await fs.writeFile(fullPath, content, 'utf-8');
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
await fs.writeFile(fullPath, content);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
async deleteFile(relativePath) {
|
|
98
|
+
if (!this.isSafePath(relativePath)) {
|
|
99
|
+
throw new Error('Access denied: path is outside of the project directory');
|
|
100
|
+
}
|
|
101
|
+
const fullPath = path.join(this.config.dir, relativePath);
|
|
102
|
+
await fs.unlink(fullPath);
|
|
103
|
+
}
|
|
83
104
|
}
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,10 @@ import { statusCommand } from './commands/status.js';
|
|
|
10
10
|
import { logsCommand } from './commands/logs.js';
|
|
11
11
|
import { configCommand } from './commands/config.js';
|
|
12
12
|
import { buildCommand } from './commands/build.js';
|
|
13
|
+
import { upgradeCommand } from './commands/upgrade.js';
|
|
14
|
+
import { checkForUpdates } from './utils/version-checker.js';
|
|
15
|
+
import { syncCommand } from './commands/sync.js';
|
|
16
|
+
import { fixIdsCommand } from './commands/fix-ids.js';
|
|
13
17
|
const __filename = fileURLToPath(import.meta.url);
|
|
14
18
|
const __dirname = dirname(__filename);
|
|
15
19
|
const packageJson = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
@@ -18,6 +22,11 @@ program
|
|
|
18
22
|
.name('playcraft')
|
|
19
23
|
.description('PlayCraft Local Dev Agent - 本地开发助手')
|
|
20
24
|
.version(packageJson.version);
|
|
25
|
+
// 版本检查(非阻塞,后台执行)
|
|
26
|
+
// 只在非 upgrade 命令时检查,避免重复提示
|
|
27
|
+
if (!process.argv.includes('upgrade')) {
|
|
28
|
+
checkForUpdates(packageJson);
|
|
29
|
+
}
|
|
21
30
|
// init 命令
|
|
22
31
|
program
|
|
23
32
|
.command('init')
|
|
@@ -34,6 +43,7 @@ program
|
|
|
34
43
|
.option('-t, --token <token>', '认证令牌')
|
|
35
44
|
.option('-d, --dir <path>', '监听目录', process.cwd())
|
|
36
45
|
.option('--port <port>', '服务端口', '2468')
|
|
46
|
+
.option('--mode <mode>', '运行模式: full-local(默认) | hybrid', 'full-local')
|
|
37
47
|
.option('--daemon', '守护进程模式(后台运行)')
|
|
38
48
|
.action(async (options) => {
|
|
39
49
|
// 检查是否是内部调用(守护进程)
|
|
@@ -82,6 +92,33 @@ program
|
|
|
82
92
|
.action(async (action, options) => {
|
|
83
93
|
await configCommand(action, options);
|
|
84
94
|
});
|
|
95
|
+
// sync 命令(hybrid 模式下本地/云端双向同步)
|
|
96
|
+
program
|
|
97
|
+
.command('sync')
|
|
98
|
+
.description('本地/云端数据同步(hybrid 模式)')
|
|
99
|
+
.argument('<action>', 'push | pull | status')
|
|
100
|
+
.option('--force', '强制同步')
|
|
101
|
+
.action(async (action, options) => {
|
|
102
|
+
if (action !== 'push' && action !== 'pull' && action !== 'status') {
|
|
103
|
+
console.error('sync action must be push, pull or status');
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
await syncCommand(action, options);
|
|
107
|
+
});
|
|
108
|
+
// fix-ids 命令:修复已导入项目的 ID 映射
|
|
109
|
+
program
|
|
110
|
+
.command('fix-ids')
|
|
111
|
+
.description('修复已导入项目的 ID 映射(PlayCanvas ID → PlayCraft numericId)')
|
|
112
|
+
.argument('<project-id>', '项目 ID(numericId 或 UUID)')
|
|
113
|
+
.option('--api-url <url>', '后端 API 地址', process.env.BACKEND_API_URL || 'http://localhost:3001')
|
|
114
|
+
.option('--token <token>', '认证令牌(可选)')
|
|
115
|
+
.action(async (projectId, options) => {
|
|
116
|
+
await fixIdsCommand({
|
|
117
|
+
projectId,
|
|
118
|
+
apiUrl: options.apiUrl,
|
|
119
|
+
token: options.token,
|
|
120
|
+
});
|
|
121
|
+
});
|
|
85
122
|
// build 命令 - 完整流程(阶段1 + 阶段2)
|
|
86
123
|
program
|
|
87
124
|
.command('build')
|
|
@@ -92,6 +129,8 @@ program
|
|
|
92
129
|
.option('-o, --output <path>', '输出目录', './dist')
|
|
93
130
|
.option('-c, --config <file>', '配置文件路径')
|
|
94
131
|
.option('-s, --scenes <scenes>', '选择场景(逗号分隔),例如: MainMenu,Gameplay')
|
|
132
|
+
.option('--clean', '构建前清理旧的输出目录(默认启用)', true)
|
|
133
|
+
.option('--no-clean', '跳过清理旧的输出目录')
|
|
95
134
|
.option('--compress', '压缩引擎代码')
|
|
96
135
|
.option('--analyze', '生成打包分析报告')
|
|
97
136
|
.option('--replace-ammo', '检测到 Ammo 时自动替换为 p2 或 cannon')
|
|
@@ -157,6 +196,7 @@ program
|
|
|
157
196
|
.option('--compress-models', '压缩 3D 模型(默认根据平台配置)')
|
|
158
197
|
.option('--no-compress-models', '禁用模型压缩')
|
|
159
198
|
.option('--model-compression <method>', '模型压缩方法 (draco|meshopt)', 'draco')
|
|
199
|
+
.option('--esm-mode <mode>', 'ESM 模块处理模式 (auto|enabled|disabled)', 'auto')
|
|
160
200
|
.action(async (projectPath, options) => {
|
|
161
201
|
await buildCommand(projectPath, {
|
|
162
202
|
...options,
|
|
@@ -172,6 +212,8 @@ program
|
|
|
172
212
|
.option('-f, --format <format>', '输出格式 (html|zip)', 'html')
|
|
173
213
|
.option('-o, --output <path>', '输出目录', './dist')
|
|
174
214
|
.option('-c, --config <file>', '配置文件路径')
|
|
215
|
+
.option('--clean', '构建前清理旧的输出目录(默认不清理,使用覆盖模式)')
|
|
216
|
+
.option('--no-clean', '使用覆盖模式(默认行为)')
|
|
175
217
|
.option('--compress', '压缩引擎代码')
|
|
176
218
|
.option('--analyze', '生成打包分析报告')
|
|
177
219
|
.option('--replace-ammo', '检测到 Ammo 时自动替换为 p2 或 cannon')
|
|
@@ -190,6 +232,7 @@ program
|
|
|
190
232
|
.option('--compress-models', '压缩 3D 模型(默认根据平台配置)')
|
|
191
233
|
.option('--no-compress-models', '禁用模型压缩')
|
|
192
234
|
.option('--model-compression <method>', '模型压缩方法 (draco|meshopt)', 'draco')
|
|
235
|
+
.option('--esm-mode <mode>', 'ESM 模块处理模式 (auto|enabled|disabled)', 'auto')
|
|
193
236
|
.action(async (baseBuildDir, options) => {
|
|
194
237
|
await buildCommand(baseBuildDir, {
|
|
195
238
|
...options,
|
|
@@ -205,4 +248,12 @@ program
|
|
|
205
248
|
const { inspectCommand } = await import('./commands/inspect.js');
|
|
206
249
|
await inspectCommand(projectPath);
|
|
207
250
|
});
|
|
251
|
+
// upgrade 命令 - 检查并更新 CLI
|
|
252
|
+
program
|
|
253
|
+
.command('upgrade')
|
|
254
|
+
.description('检查并更新 CLI 到最新版本')
|
|
255
|
+
.option('--check-only', '仅检查版本,不更新')
|
|
256
|
+
.action(async (options) => {
|
|
257
|
+
await upgradeCommand(options);
|
|
258
|
+
});
|
|
208
259
|
program.parse(process.argv);
|
package/dist/server.js
CHANGED
|
@@ -73,6 +73,34 @@ export function createServer(config, fsHandler) {
|
|
|
73
73
|
res.status(500).json({ error: error.message });
|
|
74
74
|
}
|
|
75
75
|
});
|
|
76
|
+
// Write file
|
|
77
|
+
app.post('/file', async (req, res) => {
|
|
78
|
+
const { path: filePath, content } = req.body ?? {};
|
|
79
|
+
if (!filePath) {
|
|
80
|
+
return res.status(400).json({ error: 'Missing path parameter' });
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
await fsHandler.writeFile(filePath, content ?? '');
|
|
84
|
+
res.json({ success: true, path: filePath });
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
res.status(500).json({ error: error.message });
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
// Delete file
|
|
91
|
+
app.delete('/file', async (req, res) => {
|
|
92
|
+
const filePath = req.query.path;
|
|
93
|
+
if (!filePath) {
|
|
94
|
+
return res.status(400).json({ error: 'Missing path parameter' });
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
await fsHandler.deleteFile(filePath);
|
|
98
|
+
res.json({ success: true });
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
res.status(500).json({ error: error.message });
|
|
102
|
+
}
|
|
103
|
+
});
|
|
76
104
|
// Batch get files
|
|
77
105
|
app.post('/files', async (req, res) => {
|
|
78
106
|
const { paths } = req.body;
|
package/dist/socket.js
CHANGED
|
@@ -1,21 +1,73 @@
|
|
|
1
1
|
import { WebSocketServer, WebSocket } from 'ws';
|
|
2
|
+
/** Match file path against glob pattern. Supports * (segment) and ** (any path). */
|
|
3
|
+
function matchPattern(filePath, pattern) {
|
|
4
|
+
const pathNorm = filePath.replace(/\\/g, '/');
|
|
5
|
+
const parts = pathNorm.split('/').filter(Boolean);
|
|
6
|
+
const patternParts = pattern.replace(/\\/g, '/').split('/').filter(Boolean);
|
|
7
|
+
let pi = 0;
|
|
8
|
+
let fi = 0;
|
|
9
|
+
while (pi < patternParts.length && fi < parts.length) {
|
|
10
|
+
const p = patternParts[pi];
|
|
11
|
+
if (p === '**') {
|
|
12
|
+
pi++;
|
|
13
|
+
if (pi === patternParts.length)
|
|
14
|
+
return true;
|
|
15
|
+
while (fi < parts.length) {
|
|
16
|
+
if (matchRest(parts, patternParts, fi, pi))
|
|
17
|
+
return true;
|
|
18
|
+
fi++;
|
|
19
|
+
}
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
if (p !== '*' && p !== parts[fi])
|
|
23
|
+
return false;
|
|
24
|
+
pi++;
|
|
25
|
+
fi++;
|
|
26
|
+
}
|
|
27
|
+
if (pi < patternParts.length && patternParts[pi] === '**')
|
|
28
|
+
pi++;
|
|
29
|
+
return pi === patternParts.length && fi === parts.length;
|
|
30
|
+
}
|
|
31
|
+
function matchRest(parts, patternParts, fi, pi) {
|
|
32
|
+
while (pi < patternParts.length && fi < parts.length) {
|
|
33
|
+
const p = patternParts[pi];
|
|
34
|
+
if (p === '**') {
|
|
35
|
+
pi++;
|
|
36
|
+
if (pi === patternParts.length)
|
|
37
|
+
return true;
|
|
38
|
+
while (fi < parts.length) {
|
|
39
|
+
if (matchRest(parts, patternParts, fi, pi))
|
|
40
|
+
return true;
|
|
41
|
+
fi++;
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
if (p !== '*' && p !== parts[fi])
|
|
46
|
+
return false;
|
|
47
|
+
pi++;
|
|
48
|
+
fi++;
|
|
49
|
+
}
|
|
50
|
+
if (pi < patternParts.length && patternParts[pi] === '**')
|
|
51
|
+
pi++;
|
|
52
|
+
return pi === patternParts.length && fi === parts.length;
|
|
53
|
+
}
|
|
2
54
|
export class SocketServer {
|
|
3
55
|
config;
|
|
4
56
|
wss;
|
|
5
|
-
clients = new
|
|
57
|
+
clients = new Map();
|
|
6
58
|
pingInterval = null;
|
|
7
59
|
onConnectionChange;
|
|
8
60
|
constructor(server, config, onConnectionChange) {
|
|
9
61
|
this.config = config;
|
|
10
|
-
|
|
62
|
+
// 注意:必须显式指定 path='/',避免拦截 /realtime、/messenger 等其他 WebSocket 服务
|
|
63
|
+
this.wss = new WebSocketServer({ server, path: '/' });
|
|
11
64
|
this.onConnectionChange = onConnectionChange;
|
|
12
65
|
this.wss.on('connection', (ws) => {
|
|
13
|
-
this.clients.
|
|
66
|
+
this.clients.set(ws, { patterns: ['**/*'] });
|
|
14
67
|
// Notify connection change
|
|
15
68
|
if (this.onConnectionChange) {
|
|
16
69
|
this.onConnectionChange(this.clients.size);
|
|
17
70
|
}
|
|
18
|
-
// Handle incoming messages
|
|
19
71
|
ws.on('message', (data) => {
|
|
20
72
|
try {
|
|
21
73
|
const message = JSON.parse(data.toString());
|
|
@@ -27,12 +79,10 @@ export class SocketServer {
|
|
|
27
79
|
});
|
|
28
80
|
ws.on('close', () => {
|
|
29
81
|
this.clients.delete(ws);
|
|
30
|
-
// Notify connection change
|
|
31
82
|
if (this.onConnectionChange) {
|
|
32
83
|
this.onConnectionChange(this.clients.size);
|
|
33
84
|
}
|
|
34
85
|
});
|
|
35
|
-
// Send initial hello
|
|
36
86
|
ws.send(JSON.stringify({
|
|
37
87
|
type: 'hello',
|
|
38
88
|
payload: {
|
|
@@ -40,10 +90,8 @@ export class SocketServer {
|
|
|
40
90
|
version: '1.0.0'
|
|
41
91
|
}
|
|
42
92
|
}));
|
|
43
|
-
// Send connection status
|
|
44
93
|
this.broadcast('connection_status', { connected: true });
|
|
45
94
|
});
|
|
46
|
-
// Start ping interval
|
|
47
95
|
this.startPingInterval();
|
|
48
96
|
}
|
|
49
97
|
handleMessage(ws, message) {
|
|
@@ -51,14 +99,18 @@ export class SocketServer {
|
|
|
51
99
|
case 'ping':
|
|
52
100
|
ws.send(JSON.stringify({ type: 'pong', payload: message.payload }));
|
|
53
101
|
break;
|
|
54
|
-
case 'subscribe':
|
|
55
|
-
|
|
56
|
-
|
|
102
|
+
case 'subscribe': {
|
|
103
|
+
const patterns = message.payload?.patterns || ['**/*'];
|
|
104
|
+
const info = this.clients.get(ws);
|
|
105
|
+
if (info) {
|
|
106
|
+
info.patterns = Array.isArray(patterns) ? patterns : [patterns];
|
|
107
|
+
}
|
|
57
108
|
ws.send(JSON.stringify({
|
|
58
109
|
type: 'subscribed',
|
|
59
|
-
payload: { patterns:
|
|
110
|
+
payload: { patterns: info?.patterns ?? patterns }
|
|
60
111
|
}));
|
|
61
112
|
break;
|
|
113
|
+
}
|
|
62
114
|
}
|
|
63
115
|
}
|
|
64
116
|
startPingInterval() {
|
|
@@ -66,7 +118,7 @@ export class SocketServer {
|
|
|
66
118
|
clearInterval(this.pingInterval);
|
|
67
119
|
}
|
|
68
120
|
this.pingInterval = setInterval(() => {
|
|
69
|
-
for (const client of this.clients) {
|
|
121
|
+
for (const client of this.clients.keys()) {
|
|
70
122
|
if (client.readyState === WebSocket.OPEN) {
|
|
71
123
|
try {
|
|
72
124
|
client.send(JSON.stringify({ type: 'ping', payload: Date.now() }));
|
|
@@ -83,8 +135,7 @@ export class SocketServer {
|
|
|
83
135
|
clearInterval(this.pingInterval);
|
|
84
136
|
this.pingInterval = null;
|
|
85
137
|
}
|
|
86
|
-
|
|
87
|
-
for (const client of this.clients) {
|
|
138
|
+
for (const client of this.clients.keys()) {
|
|
88
139
|
try {
|
|
89
140
|
client.close();
|
|
90
141
|
}
|
|
@@ -93,23 +144,31 @@ export class SocketServer {
|
|
|
93
144
|
}
|
|
94
145
|
}
|
|
95
146
|
this.clients.clear();
|
|
96
|
-
// Close WebSocket server
|
|
97
147
|
this.wss.close();
|
|
98
148
|
}
|
|
99
149
|
broadcast(type, payload) {
|
|
100
150
|
const message = JSON.stringify({ type, payload });
|
|
101
|
-
for (const client of this.clients) {
|
|
151
|
+
for (const client of this.clients.keys()) {
|
|
102
152
|
if (client.readyState === WebSocket.OPEN) {
|
|
103
153
|
client.send(message);
|
|
104
154
|
}
|
|
105
155
|
}
|
|
106
156
|
}
|
|
107
|
-
notifyFileChange(
|
|
108
|
-
|
|
109
|
-
path,
|
|
157
|
+
notifyFileChange(filePath, changeType) {
|
|
158
|
+
const payload = {
|
|
159
|
+
path: filePath,
|
|
110
160
|
changeType,
|
|
111
161
|
timestamp: new Date().toISOString()
|
|
112
|
-
}
|
|
162
|
+
};
|
|
163
|
+
const message = JSON.stringify({ type: 'file_changed', payload });
|
|
164
|
+
for (const [ws, info] of this.clients) {
|
|
165
|
+
if (ws.readyState !== WebSocket.OPEN)
|
|
166
|
+
continue;
|
|
167
|
+
const matches = info.patterns.some((pattern) => matchPattern(filePath, pattern));
|
|
168
|
+
if (matches) {
|
|
169
|
+
ws.send(message);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
113
172
|
}
|
|
114
173
|
getConnectionCount() {
|
|
115
174
|
return this.clients.size;
|