@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,125 @@
|
|
|
1
|
+
import { build as viteBuild } from 'vite';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { ViteConfigBuilder } from './vite/config-builder.js';
|
|
5
|
+
import { PLATFORM_CONFIGS } from './vite/platform-configs.js';
|
|
6
|
+
/**
|
|
7
|
+
* Vite 构建器 - 使用 Vite 构建 Playable Ads
|
|
8
|
+
*
|
|
9
|
+
* 职责:
|
|
10
|
+
* 1. 验证输入是有效的基础构建
|
|
11
|
+
* 2. 创建 Vite 配置
|
|
12
|
+
* 3. 执行 Vite 构建
|
|
13
|
+
* 4. 验证输出大小
|
|
14
|
+
* 5. 生成报告
|
|
15
|
+
*/
|
|
16
|
+
export class ViteBuilder {
|
|
17
|
+
baseBuildDir;
|
|
18
|
+
options;
|
|
19
|
+
sizeReport;
|
|
20
|
+
constructor(baseBuildDir, options) {
|
|
21
|
+
this.baseBuildDir = baseBuildDir;
|
|
22
|
+
this.options = options;
|
|
23
|
+
const platformConfig = PLATFORM_CONFIGS[options.platform];
|
|
24
|
+
this.sizeReport = {
|
|
25
|
+
engine: 0,
|
|
26
|
+
assets: {},
|
|
27
|
+
total: 0,
|
|
28
|
+
limit: platformConfig.sizeLimit,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 执行构建
|
|
33
|
+
*/
|
|
34
|
+
async build() {
|
|
35
|
+
// 1. 验证输入
|
|
36
|
+
await this.validateBaseBuild();
|
|
37
|
+
// 2. 创建 Vite 配置
|
|
38
|
+
const configBuilder = new ViteConfigBuilder(this.baseBuildDir, this.options.platform, this.options);
|
|
39
|
+
const viteConfig = configBuilder.create();
|
|
40
|
+
// 3. 执行 Vite 构建
|
|
41
|
+
await viteBuild(viteConfig);
|
|
42
|
+
// 4. 验证输出大小
|
|
43
|
+
const outputPath = this.getOutputPath();
|
|
44
|
+
await this.validateSize(outputPath);
|
|
45
|
+
// 5. 生成报告
|
|
46
|
+
this.generateReport(outputPath);
|
|
47
|
+
return outputPath;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* 验证基础构建
|
|
51
|
+
*/
|
|
52
|
+
async validateBaseBuild() {
|
|
53
|
+
const requiredFiles = [
|
|
54
|
+
'index.html',
|
|
55
|
+
'config.json',
|
|
56
|
+
'__start__.js',
|
|
57
|
+
];
|
|
58
|
+
const missingFiles = [];
|
|
59
|
+
for (const file of requiredFiles) {
|
|
60
|
+
try {
|
|
61
|
+
await fs.access(path.join(this.baseBuildDir, file));
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
missingFiles.push(file);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (missingFiles.length > 0) {
|
|
68
|
+
throw new Error(`基础构建产物缺少必需文件: ${missingFiles.join(', ')}\n` +
|
|
69
|
+
`请确保输入目录包含完整的多文件构建产物。`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* 获取输出路径
|
|
74
|
+
*/
|
|
75
|
+
getOutputPath() {
|
|
76
|
+
const platformConfig = PLATFORM_CONFIGS[this.options.platform];
|
|
77
|
+
const outputDir = this.options.outputDir || './dist';
|
|
78
|
+
if (platformConfig.outputFormat === 'zip') {
|
|
79
|
+
return path.join(outputDir, 'playable.zip');
|
|
80
|
+
}
|
|
81
|
+
return path.join(outputDir, platformConfig.outputFileName);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* 验证输出大小
|
|
85
|
+
*/
|
|
86
|
+
async validateSize(outputPath) {
|
|
87
|
+
try {
|
|
88
|
+
const stats = await fs.stat(outputPath);
|
|
89
|
+
this.sizeReport.total = stats.size;
|
|
90
|
+
this.sizeReport.assets[path.basename(outputPath)] = stats.size;
|
|
91
|
+
if (stats.size > this.sizeReport.limit) {
|
|
92
|
+
const sizeMB = (stats.size / 1024 / 1024).toFixed(2);
|
|
93
|
+
const limitMB = (this.sizeReport.limit / 1024 / 1024).toFixed(2);
|
|
94
|
+
console.warn(`⚠️ 警告: 文件大小 ${sizeMB} MB 超过限制 ${limitMB} MB`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
console.warn(`警告: 无法读取输出文件: ${outputPath}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* 生成报告
|
|
103
|
+
*/
|
|
104
|
+
generateReport(outputPath) {
|
|
105
|
+
// 报告已在 validateSize 中生成
|
|
106
|
+
// 这里可以添加额外的报告逻辑
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* 获取大小报告
|
|
110
|
+
*/
|
|
111
|
+
getSizeReport() {
|
|
112
|
+
return this.sizeReport;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* 格式化字节数
|
|
116
|
+
*/
|
|
117
|
+
formatBytes(bytes) {
|
|
118
|
+
if (bytes === 0)
|
|
119
|
+
return '0 Bytes';
|
|
120
|
+
const k = 1024;
|
|
121
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
122
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
123
|
+
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createServer } from 'net';
|
|
2
|
+
export async function isPortAvailable(port) {
|
|
3
|
+
return new Promise((resolve) => {
|
|
4
|
+
const server = createServer();
|
|
5
|
+
server.listen(port, () => {
|
|
6
|
+
server.once('close', () => resolve(true));
|
|
7
|
+
server.close();
|
|
8
|
+
});
|
|
9
|
+
server.on('error', (err) => {
|
|
10
|
+
if (err.code === 'EADDRINUSE') {
|
|
11
|
+
resolve(false);
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
resolve(false);
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
export async function findAvailablePort(startPort, maxAttempts = 100) {
|
|
20
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
21
|
+
const port = startPort + i;
|
|
22
|
+
if (await isPortAvailable(port)) {
|
|
23
|
+
return port;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
throw new Error(`Could not find an available port starting from ${startPort}`);
|
|
27
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
const PLAYCRAFT_DIR = path.join(os.homedir(), '.playcraft');
|
|
5
|
+
const PIDS_DIR = path.join(PLAYCRAFT_DIR, 'pids');
|
|
6
|
+
export class ProcessManager {
|
|
7
|
+
static async ensureDirectories() {
|
|
8
|
+
try {
|
|
9
|
+
await fs.mkdir(PLAYCRAFT_DIR, { recursive: true });
|
|
10
|
+
await fs.mkdir(PIDS_DIR, { recursive: true });
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
if (error.code !== 'EEXIST') {
|
|
14
|
+
throw new Error(`Failed to create directories: ${error.message}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
static getPidFilePath(projectId) {
|
|
19
|
+
return path.join(PIDS_DIR, `${projectId}.pid`);
|
|
20
|
+
}
|
|
21
|
+
static async savePid(projectId, pid) {
|
|
22
|
+
await this.ensureDirectories();
|
|
23
|
+
const pidFile = this.getPidFilePath(projectId);
|
|
24
|
+
await fs.writeFile(pidFile, pid.toString(), 'utf-8');
|
|
25
|
+
}
|
|
26
|
+
static async loadPid(projectId) {
|
|
27
|
+
try {
|
|
28
|
+
const pidFile = this.getPidFilePath(projectId);
|
|
29
|
+
const content = await fs.readFile(pidFile, 'utf-8');
|
|
30
|
+
const pid = parseInt(content.trim(), 10);
|
|
31
|
+
return isNaN(pid) ? null : pid;
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (error.code === 'ENOENT') {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
static async removePid(projectId) {
|
|
41
|
+
try {
|
|
42
|
+
const pidFile = this.getPidFilePath(projectId);
|
|
43
|
+
await fs.unlink(pidFile);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (error.code !== 'ENOENT') {
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
static isRunning(pid) {
|
|
52
|
+
try {
|
|
53
|
+
// 发送信号 0 来检查进程是否存在(不会实际发送信号)
|
|
54
|
+
process.kill(pid, 0);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
// ESRCH 表示进程不存在
|
|
59
|
+
return error.code !== 'ESRCH';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
static async killProcess(pid, signal = 'SIGTERM') {
|
|
63
|
+
if (!this.isRunning(pid)) {
|
|
64
|
+
throw new Error(`Process ${pid} is not running`);
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
process.kill(pid, signal);
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
throw new Error(`Failed to kill process ${pid}: ${error.message}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
static async getAllPids() {
|
|
74
|
+
await this.ensureDirectories();
|
|
75
|
+
try {
|
|
76
|
+
const files = await fs.readdir(PIDS_DIR);
|
|
77
|
+
const pids = [];
|
|
78
|
+
for (const file of files) {
|
|
79
|
+
if (file.endsWith('.pid')) {
|
|
80
|
+
const projectId = file.replace('.pid', '');
|
|
81
|
+
const pid = await this.loadPid(projectId);
|
|
82
|
+
if (pid !== null) {
|
|
83
|
+
pids.push({ projectId, pid });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return pids;
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
if (error.code === 'ENOENT') {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import cors from 'cors';
|
|
3
|
+
export function createServer(config, fsHandler) {
|
|
4
|
+
const app = express();
|
|
5
|
+
app.use(cors());
|
|
6
|
+
app.use(express.json());
|
|
7
|
+
// Health check
|
|
8
|
+
app.get('/health', (req, res) => {
|
|
9
|
+
res.json({ status: 'ok', projectId: config.projectId });
|
|
10
|
+
});
|
|
11
|
+
// Get file content
|
|
12
|
+
app.get('/file', async (req, res) => {
|
|
13
|
+
const filePath = req.query.path;
|
|
14
|
+
if (!filePath) {
|
|
15
|
+
return res.status(400).json({ error: 'Missing path parameter' });
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
const result = await fsHandler.readFile(filePath);
|
|
19
|
+
if (result.exists) {
|
|
20
|
+
res.json(result);
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
res.status(404).json({ error: 'File not found' });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
res.status(500).json({ error: error.message });
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
// List directory
|
|
31
|
+
app.get('/list', async (req, res) => {
|
|
32
|
+
const dirPath = req.query.path || '.';
|
|
33
|
+
try {
|
|
34
|
+
const entries = await fsHandler.listDirectory(dirPath);
|
|
35
|
+
res.json({ entries });
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
res.status(500).json({ error: error.message });
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
// Get project info
|
|
42
|
+
app.get('/info', (req, res) => {
|
|
43
|
+
res.json({
|
|
44
|
+
projectId: config.projectId,
|
|
45
|
+
version: '1.0.0',
|
|
46
|
+
dir: config.dir,
|
|
47
|
+
port: config.port,
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
// Get binary file
|
|
51
|
+
app.get('/file/binary', async (req, res) => {
|
|
52
|
+
const filePath = req.query.path;
|
|
53
|
+
if (!filePath) {
|
|
54
|
+
return res.status(400).json({ error: 'Missing path parameter' });
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const result = await fsHandler.readBinaryFile(filePath);
|
|
58
|
+
if (result.exists) {
|
|
59
|
+
// Set appropriate content type
|
|
60
|
+
const ext = filePath.split('.').pop()?.toLowerCase();
|
|
61
|
+
const contentType = getContentType(ext || '');
|
|
62
|
+
res.setHeader('Content-Type', contentType);
|
|
63
|
+
if (result.size !== undefined) {
|
|
64
|
+
res.setHeader('Content-Length', result.size);
|
|
65
|
+
}
|
|
66
|
+
res.send(result.buffer);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
res.status(404).json({ error: 'File not found' });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
res.status(500).json({ error: error.message });
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
// Batch get files
|
|
77
|
+
app.post('/files', async (req, res) => {
|
|
78
|
+
const { paths } = req.body;
|
|
79
|
+
if (!Array.isArray(paths)) {
|
|
80
|
+
return res.status(400).json({ error: 'paths must be an array' });
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
const files = await Promise.all(paths.map(async (filePath) => {
|
|
84
|
+
try {
|
|
85
|
+
const result = await fsHandler.readFile(filePath);
|
|
86
|
+
return {
|
|
87
|
+
path: filePath,
|
|
88
|
+
exists: result.exists,
|
|
89
|
+
content: result.exists ? result.content : null,
|
|
90
|
+
mtime: result.exists ? result.mtime : null,
|
|
91
|
+
size: result.exists ? result.size : null,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
return {
|
|
96
|
+
path: filePath,
|
|
97
|
+
exists: false,
|
|
98
|
+
error: error.message,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}));
|
|
102
|
+
res.json({ files });
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
res.status(500).json({ error: error.message });
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
return app;
|
|
109
|
+
}
|
|
110
|
+
function getContentType(ext) {
|
|
111
|
+
const contentTypes = {
|
|
112
|
+
png: 'image/png',
|
|
113
|
+
jpg: 'image/jpeg',
|
|
114
|
+
jpeg: 'image/jpeg',
|
|
115
|
+
gif: 'image/gif',
|
|
116
|
+
webp: 'image/webp',
|
|
117
|
+
hdr: 'image/vnd.radiance',
|
|
118
|
+
glb: 'model/gltf-binary',
|
|
119
|
+
gltf: 'model/gltf+json',
|
|
120
|
+
fbx: 'application/octet-stream',
|
|
121
|
+
mp3: 'audio/mpeg',
|
|
122
|
+
wav: 'audio/wav',
|
|
123
|
+
ogg: 'audio/ogg',
|
|
124
|
+
mp4: 'video/mp4',
|
|
125
|
+
webm: 'video/webm',
|
|
126
|
+
};
|
|
127
|
+
return contentTypes[ext] || 'application/octet-stream';
|
|
128
|
+
}
|
package/dist/socket.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
2
|
+
export class SocketServer {
|
|
3
|
+
config;
|
|
4
|
+
wss;
|
|
5
|
+
clients = new Set();
|
|
6
|
+
pingInterval = null;
|
|
7
|
+
onConnectionChange;
|
|
8
|
+
constructor(server, config, onConnectionChange) {
|
|
9
|
+
this.config = config;
|
|
10
|
+
this.wss = new WebSocketServer({ server });
|
|
11
|
+
this.onConnectionChange = onConnectionChange;
|
|
12
|
+
this.wss.on('connection', (ws) => {
|
|
13
|
+
this.clients.add(ws);
|
|
14
|
+
// Notify connection change
|
|
15
|
+
if (this.onConnectionChange) {
|
|
16
|
+
this.onConnectionChange(this.clients.size);
|
|
17
|
+
}
|
|
18
|
+
// Handle incoming messages
|
|
19
|
+
ws.on('message', (data) => {
|
|
20
|
+
try {
|
|
21
|
+
const message = JSON.parse(data.toString());
|
|
22
|
+
this.handleMessage(ws, message);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
// Ignore invalid JSON
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
ws.on('close', () => {
|
|
29
|
+
this.clients.delete(ws);
|
|
30
|
+
// Notify connection change
|
|
31
|
+
if (this.onConnectionChange) {
|
|
32
|
+
this.onConnectionChange(this.clients.size);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
// Send initial hello
|
|
36
|
+
ws.send(JSON.stringify({
|
|
37
|
+
type: 'hello',
|
|
38
|
+
payload: {
|
|
39
|
+
projectId: this.config.projectId,
|
|
40
|
+
version: '1.0.0'
|
|
41
|
+
}
|
|
42
|
+
}));
|
|
43
|
+
// Send connection status
|
|
44
|
+
this.broadcast('connection_status', { connected: true });
|
|
45
|
+
});
|
|
46
|
+
// Start ping interval
|
|
47
|
+
this.startPingInterval();
|
|
48
|
+
}
|
|
49
|
+
handleMessage(ws, message) {
|
|
50
|
+
switch (message.type) {
|
|
51
|
+
case 'ping':
|
|
52
|
+
ws.send(JSON.stringify({ type: 'pong', payload: message.payload }));
|
|
53
|
+
break;
|
|
54
|
+
case 'subscribe':
|
|
55
|
+
// Store subscription patterns for this client (simplified implementation)
|
|
56
|
+
// In a full implementation, we would filter file change notifications
|
|
57
|
+
ws.send(JSON.stringify({
|
|
58
|
+
type: 'subscribed',
|
|
59
|
+
payload: { patterns: message.payload?.patterns || [] }
|
|
60
|
+
}));
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
startPingInterval() {
|
|
65
|
+
if (this.pingInterval) {
|
|
66
|
+
clearInterval(this.pingInterval);
|
|
67
|
+
}
|
|
68
|
+
this.pingInterval = setInterval(() => {
|
|
69
|
+
for (const client of this.clients) {
|
|
70
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
71
|
+
try {
|
|
72
|
+
client.send(JSON.stringify({ type: 'ping', payload: Date.now() }));
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
// Ignore send errors
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}, 30000); // Ping every 30 seconds
|
|
80
|
+
}
|
|
81
|
+
destroy() {
|
|
82
|
+
if (this.pingInterval) {
|
|
83
|
+
clearInterval(this.pingInterval);
|
|
84
|
+
this.pingInterval = null;
|
|
85
|
+
}
|
|
86
|
+
// Close all WebSocket connections
|
|
87
|
+
for (const client of this.clients) {
|
|
88
|
+
try {
|
|
89
|
+
client.close();
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
// Ignore errors
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
this.clients.clear();
|
|
96
|
+
// Close WebSocket server
|
|
97
|
+
this.wss.close();
|
|
98
|
+
}
|
|
99
|
+
broadcast(type, payload) {
|
|
100
|
+
const message = JSON.stringify({ type, payload });
|
|
101
|
+
for (const client of this.clients) {
|
|
102
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
103
|
+
client.send(message);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
notifyFileChange(path, changeType) {
|
|
108
|
+
this.broadcast('file_changed', {
|
|
109
|
+
path,
|
|
110
|
+
changeType,
|
|
111
|
+
timestamp: new Date().toISOString()
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
getConnectionCount() {
|
|
115
|
+
return this.clients.size;
|
|
116
|
+
}
|
|
117
|
+
}
|
package/dist/watcher.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { watch } from 'chokidar';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
export class Watcher {
|
|
4
|
+
config;
|
|
5
|
+
onChange;
|
|
6
|
+
watcher;
|
|
7
|
+
constructor(config, onChange) {
|
|
8
|
+
this.config = config;
|
|
9
|
+
this.onChange = onChange;
|
|
10
|
+
this.watcher = watch(this.config.dir, {
|
|
11
|
+
ignored: [
|
|
12
|
+
'**/node_modules/**',
|
|
13
|
+
'**/.git/**',
|
|
14
|
+
'**/dist/**',
|
|
15
|
+
'**/playcraft.agent.config.json',
|
|
16
|
+
'**/.DS_Store'
|
|
17
|
+
],
|
|
18
|
+
persistent: true,
|
|
19
|
+
ignoreInitial: true,
|
|
20
|
+
});
|
|
21
|
+
this.watcher
|
|
22
|
+
.on('add', (filePath) => this.handleEvent(filePath, 'add'))
|
|
23
|
+
.on('change', (filePath) => this.handleEvent(filePath, 'modify'))
|
|
24
|
+
.on('unlink', (filePath) => this.handleEvent(filePath, 'delete'));
|
|
25
|
+
}
|
|
26
|
+
handleEvent(filePath, type) {
|
|
27
|
+
const relativePath = path.relative(this.config.dir, filePath);
|
|
28
|
+
this.onChange(relativePath, type);
|
|
29
|
+
}
|
|
30
|
+
close() {
|
|
31
|
+
return this.watcher.close();
|
|
32
|
+
}
|
|
33
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@playcraft/cli",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"playcraft": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"templates",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"dev": "tsc -w",
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"start": "node dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@playcraft/build": "workspace:*",
|
|
21
|
+
"chokidar": "^4.0.3",
|
|
22
|
+
"commander": "^13.1.0",
|
|
23
|
+
"cors": "^2.8.5",
|
|
24
|
+
"cosmiconfig": "^9.0.0",
|
|
25
|
+
"dotenv": "^17.2.3",
|
|
26
|
+
"express": "^5.2.1",
|
|
27
|
+
"inquirer": "^9.2.12",
|
|
28
|
+
"ora": "^8.2.0",
|
|
29
|
+
"picocolors": "^1.1.1",
|
|
30
|
+
"ws": "^8.18.0",
|
|
31
|
+
"zod": "^3.24.1"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/cors": "^2.8.17",
|
|
35
|
+
"@types/express": "^5.0.0",
|
|
36
|
+
"@types/inquirer": "^9.0.7",
|
|
37
|
+
"@types/node": "^22.10.5",
|
|
38
|
+
"@types/ws": "^8.5.13",
|
|
39
|
+
"typescript": "^5.7.2"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
|
6
|
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
7
|
+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
8
|
+
<title>Playable Ad</title>
|
|
9
|
+
<style>
|
|
10
|
+
* {
|
|
11
|
+
margin: 0;
|
|
12
|
+
padding: 0;
|
|
13
|
+
box-sizing: border-box;
|
|
14
|
+
}
|
|
15
|
+
html, body {
|
|
16
|
+
width: 100%;
|
|
17
|
+
height: 100%;
|
|
18
|
+
overflow: hidden;
|
|
19
|
+
background: #000;
|
|
20
|
+
touch-action: none;
|
|
21
|
+
-webkit-touch-callout: none;
|
|
22
|
+
-webkit-user-select: none;
|
|
23
|
+
user-select: none;
|
|
24
|
+
}
|
|
25
|
+
#application-canvas {
|
|
26
|
+
display: block;
|
|
27
|
+
width: 100%;
|
|
28
|
+
height: 100%;
|
|
29
|
+
margin: 0 auto;
|
|
30
|
+
image-rendering: -webkit-optimize-contrast;
|
|
31
|
+
image-rendering: crisp-edges;
|
|
32
|
+
}
|
|
33
|
+
/* 加载指示器 */
|
|
34
|
+
#loading {
|
|
35
|
+
position: absolute;
|
|
36
|
+
top: 50%;
|
|
37
|
+
left: 50%;
|
|
38
|
+
transform: translate(-50%, -50%);
|
|
39
|
+
color: #fff;
|
|
40
|
+
font-family: Arial, sans-serif;
|
|
41
|
+
font-size: 14px;
|
|
42
|
+
}
|
|
43
|
+
</style>
|
|
44
|
+
</head>
|
|
45
|
+
<body>
|
|
46
|
+
<div id="loading">Loading...</div>
|
|
47
|
+
<canvas id="application-canvas"></canvas>
|
|
48
|
+
<!-- PlayCanvas Engine, Config 和 Start Script 将在这里注入 -->
|
|
49
|
+
<script>
|
|
50
|
+
// 隐藏加载指示器
|
|
51
|
+
window.addEventListener('load', function() {
|
|
52
|
+
const loading = document.getElementById('loading');
|
|
53
|
+
if (loading) {
|
|
54
|
+
loading.style.display = 'none';
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
</script>
|
|
58
|
+
</body>
|
|
59
|
+
</html>
|