@playcraft/cli 0.0.1
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/README.md +12 -0
- package/dist/build-config.js +26 -0
- package/dist/commands/build.js +363 -0
- package/dist/commands/config.js +133 -0
- package/dist/commands/init.js +86 -0
- package/dist/commands/inspect.js +209 -0
- package/dist/commands/logs.js +121 -0
- package/dist/commands/start.js +284 -0
- package/dist/commands/status.js +106 -0
- package/dist/commands/stop.js +58 -0
- package/dist/config.js +31 -0
- package/dist/fs-handler.js +83 -0
- package/dist/index.js +200 -0
- package/dist/logger.js +122 -0
- package/dist/playable/base-builder.js +265 -0
- package/dist/playable/builder.js +1462 -0
- package/dist/playable/converter.js +150 -0
- package/dist/playable/index.js +3 -0
- package/dist/playable/platforms/base.js +12 -0
- package/dist/playable/platforms/facebook.js +37 -0
- package/dist/playable/platforms/index.js +24 -0
- package/dist/playable/platforms/snapchat.js +59 -0
- package/dist/playable/playable-builder.js +521 -0
- package/dist/playable/types.js +1 -0
- package/dist/playable/vite/config-builder.js +136 -0
- package/dist/playable/vite/platform-configs.js +102 -0
- package/dist/playable/vite/plugin-model-compression.js +63 -0
- package/dist/playable/vite/plugin-platform.js +65 -0
- package/dist/playable/vite/plugin-playcanvas.js +454 -0
- package/dist/playable/vite-builder.js +125 -0
- package/dist/port-utils.js +27 -0
- package/dist/process-manager.js +96 -0
- package/dist/server.js +128 -0
- package/dist/socket.js +117 -0
- package/dist/watcher.js +33 -0
- package/package.json +41 -0
- package/templates/playable-ad.html +59 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import pc from 'picocolors';
|
|
4
|
+
/**
|
|
5
|
+
* 检查项目类型和结构
|
|
6
|
+
*/
|
|
7
|
+
export async function inspectCommand(projectPath) {
|
|
8
|
+
console.log(pc.cyan('\n🔍 正在检查项目...\n'));
|
|
9
|
+
try {
|
|
10
|
+
const projectDir = path.resolve(projectPath);
|
|
11
|
+
// 检查目录是否存在
|
|
12
|
+
try {
|
|
13
|
+
await fs.access(projectDir);
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
console.error(pc.red(`❌ 项目目录不存在: ${projectDir}`));
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
console.log(pc.dim(`项目路径: ${projectDir}\n`));
|
|
20
|
+
// 检查文件存在性
|
|
21
|
+
const files = {
|
|
22
|
+
// Build 项目标识
|
|
23
|
+
'index.html': false,
|
|
24
|
+
'__start__.js': false,
|
|
25
|
+
'__settings__.js': false,
|
|
26
|
+
'__loading__.js': false,
|
|
27
|
+
'__game_scripts.js': false,
|
|
28
|
+
'config.json': false,
|
|
29
|
+
// 源代码项目标识
|
|
30
|
+
'manifest.json': false,
|
|
31
|
+
'assets.json': false,
|
|
32
|
+
'scenes.json': false,
|
|
33
|
+
'project.json': false,
|
|
34
|
+
// 其他
|
|
35
|
+
'files': false,
|
|
36
|
+
'assets': false,
|
|
37
|
+
'scenes': false,
|
|
38
|
+
};
|
|
39
|
+
for (const file of Object.keys(files)) {
|
|
40
|
+
try {
|
|
41
|
+
const filePath = path.join(projectDir, file);
|
|
42
|
+
await fs.access(filePath);
|
|
43
|
+
files[file] = true;
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
// 文件不存在
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// 判断项目类型
|
|
50
|
+
let projectType = 'unknown';
|
|
51
|
+
let confidence = 0;
|
|
52
|
+
// 检查是否是 Build 项目
|
|
53
|
+
const buildFiles = ['index.html', '__start__.js', 'config.json'];
|
|
54
|
+
const buildScore = buildFiles.filter(f => files[f]).length;
|
|
55
|
+
if (buildScore >= 2) {
|
|
56
|
+
projectType = 'build';
|
|
57
|
+
confidence = (buildScore / buildFiles.length) * 100;
|
|
58
|
+
}
|
|
59
|
+
// 检查是否是 PlayCraft 源代码
|
|
60
|
+
else if (files['manifest.json']) {
|
|
61
|
+
projectType = 'playcraft-source';
|
|
62
|
+
confidence = 90;
|
|
63
|
+
if (files['assets'] && files['scenes']) {
|
|
64
|
+
confidence = 100;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// 检查是否是 PlayCanvas 源代码
|
|
68
|
+
else if (files['assets.json'] || files['scenes.json']) {
|
|
69
|
+
projectType = 'playcanvas-source';
|
|
70
|
+
const pcFiles = ['assets.json', 'scenes.json', 'project.json'];
|
|
71
|
+
const pcScore = pcFiles.filter(f => files[f]).length;
|
|
72
|
+
confidence = (pcScore / pcFiles.length) * 100;
|
|
73
|
+
}
|
|
74
|
+
// 输出结果
|
|
75
|
+
console.log(pc.bold('📊 项目分析结果:\n'));
|
|
76
|
+
// 项目类型
|
|
77
|
+
console.log(`${pc.bold('项目类型:')} ${getProjectTypeDisplay(projectType, confidence)}`);
|
|
78
|
+
console.log(`${pc.bold('置信度:')} ${confidence.toFixed(0)}%\n`);
|
|
79
|
+
// 文件清单
|
|
80
|
+
console.log(pc.bold('📁 文件清单:\n'));
|
|
81
|
+
if (projectType === 'build') {
|
|
82
|
+
console.log(pc.green('✅ Build 项目文件:'));
|
|
83
|
+
printFileList(files, ['index.html', '__start__.js', '__settings__.js', '__game_scripts.js', 'config.json', '__loading__.js']);
|
|
84
|
+
if (files['files']) {
|
|
85
|
+
console.log(pc.green(' ✓ files/ 目录'));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else if (projectType === 'playcraft-source') {
|
|
89
|
+
console.log(pc.yellow('📝 PlayCraft 源代码文件:'));
|
|
90
|
+
printFileList(files, ['manifest.json', 'assets', 'scenes']);
|
|
91
|
+
}
|
|
92
|
+
else if (projectType === 'playcanvas-source') {
|
|
93
|
+
console.log(pc.yellow('📝 PlayCanvas 源代码文件:'));
|
|
94
|
+
printFileList(files, ['assets.json', 'scenes.json', 'project.json', 'files']);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
console.log(pc.red('❌ 无法识别的项目结构'));
|
|
98
|
+
console.log(pc.dim('\n找到的文件:'));
|
|
99
|
+
for (const [file, exists] of Object.entries(files)) {
|
|
100
|
+
if (exists) {
|
|
101
|
+
console.log(pc.dim(` - ${file}`));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// 建议
|
|
106
|
+
console.log('\n' + pc.bold('💡 建议:\n'));
|
|
107
|
+
if (projectType === 'build') {
|
|
108
|
+
console.log(pc.green('✅ 这是一个构建后的项目,可以直接打包为 Playable Ad'));
|
|
109
|
+
console.log(pc.dim(' 运行: playcraft build --platform facebook'));
|
|
110
|
+
}
|
|
111
|
+
else if (projectType === 'playcraft-source' || projectType === 'playcanvas-source') {
|
|
112
|
+
console.log(pc.yellow('⚠️ 这是源代码项目,建议先构建再打包\n'));
|
|
113
|
+
console.log(pc.cyan('方式 1 (推荐): 使用 PlayCanvas REST API 构建'));
|
|
114
|
+
console.log(pc.dim(' 1. 在 PlayCanvas Editor 中发布项目'));
|
|
115
|
+
console.log(pc.dim(' 2. 使用 REST API 下载构建'));
|
|
116
|
+
console.log(pc.dim(' 3. 运行: playcraft build --platform facebook\n'));
|
|
117
|
+
console.log(pc.cyan('方式 2: 使用本地构建(实验性功能)'));
|
|
118
|
+
console.log(pc.dim(' 运行: playcraft build --platform facebook --auto-build\n'));
|
|
119
|
+
console.log(pc.cyan('方式 3: 直接从源代码打包(可能不完整)'));
|
|
120
|
+
console.log(pc.dim(' 运行: playcraft build --platform facebook --skip-build'));
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
console.log(pc.red('❌ 无法识别项目类型,请确保:'));
|
|
124
|
+
console.log(pc.dim(' - Build 项目包含: index.html, config.json, __start__.js'));
|
|
125
|
+
console.log(pc.dim(' - PlayCraft 源代码包含: manifest.json'));
|
|
126
|
+
console.log(pc.dim(' - PlayCanvas 源代码包含: assets.json 和 scenes.json'));
|
|
127
|
+
}
|
|
128
|
+
console.log('');
|
|
129
|
+
// 读取并显示关键信息
|
|
130
|
+
await printProjectInfo(projectDir, files, projectType);
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
console.error(pc.red(`\n❌ 检查失败: ${error.message}`));
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function getProjectTypeDisplay(type, confidence) {
|
|
138
|
+
const emoji = {
|
|
139
|
+
'build': '✅',
|
|
140
|
+
'playcraft-source': '📝',
|
|
141
|
+
'playcanvas-source': '📝',
|
|
142
|
+
'unknown': '❓',
|
|
143
|
+
}[type] || '❓';
|
|
144
|
+
const label = {
|
|
145
|
+
'build': pc.green('Build 项目(构建后)'),
|
|
146
|
+
'playcraft-source': pc.yellow('PlayCraft 源代码'),
|
|
147
|
+
'playcanvas-source': pc.yellow('PlayCanvas 源代码'),
|
|
148
|
+
'unknown': pc.red('未知类型'),
|
|
149
|
+
}[type] || pc.red('未知类型');
|
|
150
|
+
return `${emoji} ${label}`;
|
|
151
|
+
}
|
|
152
|
+
function printFileList(files, keys) {
|
|
153
|
+
for (const key of keys) {
|
|
154
|
+
if (files[key]) {
|
|
155
|
+
console.log(pc.green(` ✓ ${key}`));
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
console.log(pc.dim(` ✗ ${key}`));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async function printProjectInfo(projectDir, files, projectType) {
|
|
163
|
+
console.log(pc.bold('📋 项目信息:\n'));
|
|
164
|
+
try {
|
|
165
|
+
if (projectType === 'build' && files['config.json']) {
|
|
166
|
+
const configPath = path.join(projectDir, 'config.json');
|
|
167
|
+
const configContent = await fs.readFile(configPath, 'utf-8');
|
|
168
|
+
const config = JSON.parse(configContent);
|
|
169
|
+
console.log(pc.dim(' 项目名称:'), config.project_name || 'N/A');
|
|
170
|
+
console.log(pc.dim(' 项目 ID:'), config.project_id || 'N/A');
|
|
171
|
+
console.log(pc.dim(' 场景数量:'), config.scenes?.length || 0);
|
|
172
|
+
console.log(pc.dim(' 资产数量:'), config.assets ? Object.keys(config.assets).length : 0);
|
|
173
|
+
}
|
|
174
|
+
else if (projectType === 'playcraft-source' && files['manifest.json']) {
|
|
175
|
+
const manifestPath = path.join(projectDir, 'manifest.json');
|
|
176
|
+
const manifestContent = await fs.readFile(manifestPath, 'utf-8');
|
|
177
|
+
const manifest = JSON.parse(manifestContent);
|
|
178
|
+
console.log(pc.dim(' 项目名称:'), manifest.project?.name || 'N/A');
|
|
179
|
+
console.log(pc.dim(' 项目 ID:'), manifest.project?.id || 'N/A');
|
|
180
|
+
console.log(pc.dim(' 场景数量:'), manifest.scenes?.length || 0);
|
|
181
|
+
console.log(pc.dim(' 资产数量:'), manifest.assets?.length || 0);
|
|
182
|
+
}
|
|
183
|
+
else if (projectType === 'playcanvas-source') {
|
|
184
|
+
if (files['project.json']) {
|
|
185
|
+
const projectJsonPath = path.join(projectDir, 'project.json');
|
|
186
|
+
const projectContent = await fs.readFile(projectJsonPath, 'utf-8');
|
|
187
|
+
const project = JSON.parse(projectContent);
|
|
188
|
+
console.log(pc.dim(' 项目名称:'), project.name || 'N/A');
|
|
189
|
+
console.log(pc.dim(' 项目 ID:'), project.id || 'N/A');
|
|
190
|
+
}
|
|
191
|
+
if (files['assets.json']) {
|
|
192
|
+
const assetsPath = path.join(projectDir, 'assets.json');
|
|
193
|
+
const assetsContent = await fs.readFile(assetsPath, 'utf-8');
|
|
194
|
+
const assets = JSON.parse(assetsContent);
|
|
195
|
+
console.log(pc.dim(' 资产数量:'), assets.length || 0);
|
|
196
|
+
}
|
|
197
|
+
if (files['scenes.json']) {
|
|
198
|
+
const scenesPath = path.join(projectDir, 'scenes.json');
|
|
199
|
+
const scenesContent = await fs.readFile(scenesPath, 'utf-8');
|
|
200
|
+
const scenes = JSON.parse(scenesContent);
|
|
201
|
+
console.log(pc.dim(' 场景数量:'), scenes.length || 0);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
console.log(pc.dim(' 无法读取项目信息'));
|
|
207
|
+
}
|
|
208
|
+
console.log('');
|
|
209
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { Logger } from '../logger.js';
|
|
2
|
+
import { loadConfig } from '../config.js';
|
|
3
|
+
import pc from 'picocolors';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import { createReadStream, watchFile, unwatchFile } from 'fs';
|
|
6
|
+
import { createInterface } from 'readline';
|
|
7
|
+
export async function logsCommand(options) {
|
|
8
|
+
const spinner = ora('加载日志...').start();
|
|
9
|
+
try {
|
|
10
|
+
let projectId = options.project;
|
|
11
|
+
// 如果没有指定项目,尝试从配置文件读取
|
|
12
|
+
if (!projectId) {
|
|
13
|
+
try {
|
|
14
|
+
const config = await loadConfig({});
|
|
15
|
+
projectId = config.projectId;
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
// 忽略配置加载错误
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
if (!projectId) {
|
|
22
|
+
spinner.fail('未指定项目 ID');
|
|
23
|
+
console.error(pc.red('请使用 --project 参数指定项目 ID'));
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
const logFile = Logger.getLogFilePath(projectId);
|
|
27
|
+
if (options.follow) {
|
|
28
|
+
// 实时跟踪模式
|
|
29
|
+
spinner.succeed('实时跟踪日志(按 Ctrl+C 退出)');
|
|
30
|
+
console.log(pc.cyan(`\n📋 日志文件: ${logFile}\n`));
|
|
31
|
+
await tailLogFile(logFile);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
// 显示最后 N 行
|
|
35
|
+
const lines = options.lines ? parseInt(options.lines, 10) : undefined;
|
|
36
|
+
const logLines = await Logger.readLogFile(projectId, lines);
|
|
37
|
+
if (logLines.length === 0) {
|
|
38
|
+
spinner.succeed('日志文件为空');
|
|
39
|
+
console.log(pc.yellow('没有找到日志内容\n'));
|
|
40
|
+
process.exit(0);
|
|
41
|
+
}
|
|
42
|
+
spinner.succeed(`显示最后 ${logLines.length} 行日志`);
|
|
43
|
+
console.log(pc.cyan(`\n📋 日志文件: ${logFile}\n`));
|
|
44
|
+
for (const line of logLines) {
|
|
45
|
+
console.log(line);
|
|
46
|
+
}
|
|
47
|
+
console.log();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
spinner.fail('加载日志失败');
|
|
52
|
+
console.error(pc.red(`错误: ${error.message}`));
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function tailLogFile(logFile) {
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
const stream = createReadStream(logFile, { encoding: 'utf-8' });
|
|
59
|
+
const rl = createInterface({
|
|
60
|
+
input: stream,
|
|
61
|
+
crlfDelay: Infinity,
|
|
62
|
+
});
|
|
63
|
+
// 先读取最后几行(如果文件很大)
|
|
64
|
+
let lastLines = [];
|
|
65
|
+
rl.on('line', (line) => {
|
|
66
|
+
lastLines.push(line);
|
|
67
|
+
if (lastLines.length > 10) {
|
|
68
|
+
lastLines.shift();
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
rl.on('close', () => {
|
|
72
|
+
// 显示最后几行
|
|
73
|
+
for (const line of lastLines) {
|
|
74
|
+
console.log(line);
|
|
75
|
+
}
|
|
76
|
+
// 然后开始监控文件变化
|
|
77
|
+
const watcher = watchFile(logFile, { interval: 1000 }, (curr, prev) => {
|
|
78
|
+
if (curr.mtime > prev.mtime) {
|
|
79
|
+
// 文件已更新,读取新内容
|
|
80
|
+
const tailStream = createReadStream(logFile, {
|
|
81
|
+
encoding: 'utf-8',
|
|
82
|
+
start: prev.size,
|
|
83
|
+
});
|
|
84
|
+
tailStream.on('data', (chunk) => {
|
|
85
|
+
const content = typeof chunk === 'string' ? chunk : chunk.toString('utf-8');
|
|
86
|
+
const lines = content.split('\n').filter((l) => l.trim());
|
|
87
|
+
for (const line of lines) {
|
|
88
|
+
console.log(line);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
// 处理 Ctrl+C
|
|
94
|
+
process.on('SIGINT', () => {
|
|
95
|
+
unwatchFile(logFile);
|
|
96
|
+
console.log(pc.yellow('\n\n停止跟踪日志\n'));
|
|
97
|
+
resolve();
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
stream.on('error', (error) => {
|
|
101
|
+
if (error.code === 'ENOENT') {
|
|
102
|
+
console.log(pc.yellow('日志文件不存在,等待创建...\n'));
|
|
103
|
+
// 等待文件创建
|
|
104
|
+
const watcher = watchFile(logFile, { interval: 1000 }, (curr) => {
|
|
105
|
+
if (curr.size > 0) {
|
|
106
|
+
unwatchFile(logFile);
|
|
107
|
+
tailLogFile(logFile).then(resolve).catch(reject);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
process.on('SIGINT', () => {
|
|
111
|
+
unwatchFile(logFile);
|
|
112
|
+
console.log(pc.yellow('\n\n停止跟踪日志\n'));
|
|
113
|
+
resolve();
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
reject(error);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { loadConfig } from '../config.js';
|
|
5
|
+
import { ProcessManager } from '../process-manager.js';
|
|
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
|
+
import pc from 'picocolors';
|
|
14
|
+
import ora from 'ora';
|
|
15
|
+
import inquirer from 'inquirer';
|
|
16
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
17
|
+
const __dirname = path.dirname(__filename);
|
|
18
|
+
export async function startCommand(options) {
|
|
19
|
+
const spinner = ora('加载配置中...').start();
|
|
20
|
+
try {
|
|
21
|
+
const config = await loadConfig({
|
|
22
|
+
projectId: options.project,
|
|
23
|
+
token: options.token,
|
|
24
|
+
dir: options.dir,
|
|
25
|
+
port: options.port ? parseInt(options.port) : undefined,
|
|
26
|
+
});
|
|
27
|
+
if (!config.projectId) {
|
|
28
|
+
spinner.fail('项目 ID 未设置');
|
|
29
|
+
console.error(pc.red('请使用 --project 参数或配置文件设置项目 ID'));
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
spinner.succeed('配置加载完成');
|
|
33
|
+
// 检查端口是否可用
|
|
34
|
+
const portAvailable = await isPortAvailable(config.port);
|
|
35
|
+
if (!portAvailable) {
|
|
36
|
+
if (options.daemon) {
|
|
37
|
+
// 守护进程模式:自动查找可用端口
|
|
38
|
+
spinner.start('端口被占用,查找可用端口...');
|
|
39
|
+
config.port = await findAvailablePort(config.port);
|
|
40
|
+
spinner.succeed(`使用端口: ${config.port}`);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
// 前台模式:询问用户
|
|
44
|
+
spinner.stop();
|
|
45
|
+
const answer = await inquirer.prompt([
|
|
46
|
+
{
|
|
47
|
+
type: 'confirm',
|
|
48
|
+
name: 'useOtherPort',
|
|
49
|
+
message: `端口 ${config.port} 已被占用,是否使用其他端口?`,
|
|
50
|
+
default: true,
|
|
51
|
+
},
|
|
52
|
+
]);
|
|
53
|
+
if (answer.useOtherPort) {
|
|
54
|
+
spinner.start('查找可用端口...');
|
|
55
|
+
config.port = await findAvailablePort(config.port);
|
|
56
|
+
spinner.succeed(`使用端口: ${config.port}`);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
console.log(pc.yellow('已取消启动'));
|
|
60
|
+
process.exit(0);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// 检查是否已有进程在运行
|
|
65
|
+
const existingPid = await ProcessManager.loadPid(config.projectId);
|
|
66
|
+
if (existingPid && ProcessManager.isRunning(existingPid)) {
|
|
67
|
+
spinner.fail('服务已在运行');
|
|
68
|
+
console.error(pc.red(`项目 ${config.projectId} 的 agent 已在运行 (PID: ${existingPid})`));
|
|
69
|
+
console.log(pc.yellow(`使用 'playcraft stop --project ${config.projectId}' 停止服务`));
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
if (options.daemon) {
|
|
73
|
+
// 守护进程模式
|
|
74
|
+
await startDaemon(config);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
// 前台模式
|
|
78
|
+
await startForeground(config);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
spinner.fail('启动失败');
|
|
83
|
+
console.error(pc.red(`错误: ${error.message}`));
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async function startDaemon(config) {
|
|
88
|
+
console.log(pc.cyan('\n🚀 启动 PlayCraft Agent (守护进程模式)...\n'));
|
|
89
|
+
// 获取当前脚本路径(编译后的 dist/index.js)
|
|
90
|
+
// 注意:在运行时,__dirname 指向 dist/commands,所以需要回到 dist
|
|
91
|
+
const scriptPath = path.join(__dirname, '..', 'index.js');
|
|
92
|
+
// 使用 spawn 启动子进程
|
|
93
|
+
const child = spawn('node', [scriptPath, 'start'], {
|
|
94
|
+
detached: true,
|
|
95
|
+
stdio: 'ignore',
|
|
96
|
+
env: {
|
|
97
|
+
...process.env,
|
|
98
|
+
PLAYCRAFT_INTERNAL: 'true',
|
|
99
|
+
PLAYCRAFT_PROJECT_ID: config.projectId,
|
|
100
|
+
PLAYCRAFT_TOKEN: config.token || '',
|
|
101
|
+
PLAYCRAFT_PORT: config.port.toString(),
|
|
102
|
+
PLAYCRAFT_DIR: config.dir,
|
|
103
|
+
PLAYCRAFT_DAEMON: 'true',
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
child.unref();
|
|
107
|
+
// 等待一下确保进程启动
|
|
108
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
109
|
+
// 检查进程是否还在运行
|
|
110
|
+
try {
|
|
111
|
+
if (!child.pid) {
|
|
112
|
+
throw new Error('无法获取进程 PID');
|
|
113
|
+
}
|
|
114
|
+
process.kill(child.pid, 0);
|
|
115
|
+
await ProcessManager.savePid(config.projectId || 'default', child.pid);
|
|
116
|
+
console.log(pc.green(`✅ Agent 已在后台启动 (PID: ${child.pid})`));
|
|
117
|
+
console.log(pc.cyan(`项目 ID: ${config.projectId}`));
|
|
118
|
+
console.log(pc.cyan(`端口: ${config.port}`));
|
|
119
|
+
console.log(pc.cyan(`目录: ${config.dir}`));
|
|
120
|
+
console.log(pc.dim(`\n使用 'playcraft status --project ${config.projectId}' 查看状态`));
|
|
121
|
+
console.log(pc.dim(`使用 'playcraft logs --project ${config.projectId}' 查看日志\n`));
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
console.error(pc.red('启动失败:进程已退出'));
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
async function startForeground(config) {
|
|
129
|
+
const logger = new Logger(config.projectId || 'default', false);
|
|
130
|
+
await logger.initialize();
|
|
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
|
+
// 优雅关闭
|
|
166
|
+
const shutdown = async () => {
|
|
167
|
+
console.log(pc.yellow('\n正在关闭 agent...'));
|
|
168
|
+
// Set a timeout to force exit if graceful shutdown fails
|
|
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
|
+
}
|
|
199
|
+
};
|
|
200
|
+
process.on('SIGINT', shutdown);
|
|
201
|
+
process.on('SIGTERM', shutdown);
|
|
202
|
+
}
|
|
203
|
+
// 内部启动函数(用于守护进程)
|
|
204
|
+
export async function startInternal() {
|
|
205
|
+
const projectId = process.env.PLAYCRAFT_PROJECT_ID;
|
|
206
|
+
const token = process.env.PLAYCRAFT_TOKEN;
|
|
207
|
+
const port = parseInt(process.env.PLAYCRAFT_PORT || '2468');
|
|
208
|
+
const dir = process.env.PLAYCRAFT_DIR || process.cwd();
|
|
209
|
+
const isDaemon = process.env.PLAYCRAFT_DAEMON === 'true';
|
|
210
|
+
if (!projectId) {
|
|
211
|
+
console.error('PLAYCRAFT_PROJECT_ID 环境变量未设置');
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
const config = {
|
|
215
|
+
projectId,
|
|
216
|
+
token,
|
|
217
|
+
dir,
|
|
218
|
+
port,
|
|
219
|
+
};
|
|
220
|
+
const logger = new Logger(config.projectId || 'default', isDaemon);
|
|
221
|
+
await logger.initialize();
|
|
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
|
+
});
|
|
246
|
+
// 保存 PID
|
|
247
|
+
await ProcessManager.savePid(config.projectId || 'default', process.pid);
|
|
248
|
+
// 优雅关闭
|
|
249
|
+
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
|
+
try {
|
|
256
|
+
// Close watcher
|
|
257
|
+
await watcher.close();
|
|
258
|
+
// Close WebSocket connections
|
|
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
|
|
270
|
+
await ProcessManager.removePid(config.projectId || 'default');
|
|
271
|
+
// Close logger
|
|
272
|
+
await logger.close();
|
|
273
|
+
clearTimeout(forceExitTimeout);
|
|
274
|
+
process.exit(0);
|
|
275
|
+
}
|
|
276
|
+
catch (error) {
|
|
277
|
+
clearTimeout(forceExitTimeout);
|
|
278
|
+
await logger.error(`Error during shutdown: ${error}`);
|
|
279
|
+
process.exit(1);
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
process.on('SIGINT', shutdown);
|
|
283
|
+
process.on('SIGTERM', shutdown);
|
|
284
|
+
}
|