@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.
Files changed (37) hide show
  1. package/README.md +12 -0
  2. package/dist/build-config.js +26 -0
  3. package/dist/commands/build.js +363 -0
  4. package/dist/commands/config.js +133 -0
  5. package/dist/commands/init.js +86 -0
  6. package/dist/commands/inspect.js +209 -0
  7. package/dist/commands/logs.js +121 -0
  8. package/dist/commands/start.js +284 -0
  9. package/dist/commands/status.js +106 -0
  10. package/dist/commands/stop.js +58 -0
  11. package/dist/config.js +31 -0
  12. package/dist/fs-handler.js +83 -0
  13. package/dist/index.js +200 -0
  14. package/dist/logger.js +122 -0
  15. package/dist/playable/base-builder.js +265 -0
  16. package/dist/playable/builder.js +1462 -0
  17. package/dist/playable/converter.js +150 -0
  18. package/dist/playable/index.js +3 -0
  19. package/dist/playable/platforms/base.js +12 -0
  20. package/dist/playable/platforms/facebook.js +37 -0
  21. package/dist/playable/platforms/index.js +24 -0
  22. package/dist/playable/platforms/snapchat.js +59 -0
  23. package/dist/playable/playable-builder.js +521 -0
  24. package/dist/playable/types.js +1 -0
  25. package/dist/playable/vite/config-builder.js +136 -0
  26. package/dist/playable/vite/platform-configs.js +102 -0
  27. package/dist/playable/vite/plugin-model-compression.js +63 -0
  28. package/dist/playable/vite/plugin-platform.js +65 -0
  29. package/dist/playable/vite/plugin-playcanvas.js +454 -0
  30. package/dist/playable/vite-builder.js +125 -0
  31. package/dist/port-utils.js +27 -0
  32. package/dist/process-manager.js +96 -0
  33. package/dist/server.js +128 -0
  34. package/dist/socket.js +117 -0
  35. package/dist/watcher.js +33 -0
  36. package/package.json +41 -0
  37. package/templates/playable-ad.html +59 -0
@@ -0,0 +1,106 @@
1
+ import { ProcessManager } from '../process-manager.js';
2
+ import { loadConfig } from '../config.js';
3
+ import pc from 'picocolors';
4
+ import ora from 'ora';
5
+ export async function statusCommand(options) {
6
+ if (options.all) {
7
+ // 显示所有运行中的服务
8
+ await showAllStatus();
9
+ }
10
+ else {
11
+ // 显示指定项目的状态
12
+ await showProjectStatus(options.project);
13
+ }
14
+ }
15
+ async function showProjectStatus(projectId) {
16
+ const spinner = ora('检查服务状态...').start();
17
+ try {
18
+ // 如果没有指定项目,尝试从配置文件读取
19
+ if (!projectId) {
20
+ try {
21
+ const config = await loadConfig({});
22
+ projectId = config.projectId;
23
+ }
24
+ catch (error) {
25
+ spinner.fail('未指定项目 ID');
26
+ console.error(pc.red('请使用 --project 参数指定项目 ID,或使用 --all 查看所有服务'));
27
+ process.exit(1);
28
+ }
29
+ }
30
+ if (!projectId) {
31
+ spinner.fail('未指定项目 ID');
32
+ console.error(pc.red('请使用 --project 参数指定项目 ID'));
33
+ process.exit(1);
34
+ }
35
+ const pid = await ProcessManager.loadPid(projectId);
36
+ if (!pid) {
37
+ spinner.fail('服务未运行');
38
+ console.log(pc.yellow(`项目 ${projectId} 的 agent 未在运行\n`));
39
+ process.exit(0);
40
+ }
41
+ const isRunning = ProcessManager.isRunning(pid);
42
+ if (!isRunning) {
43
+ spinner.warn('发现无效的 PID 文件');
44
+ await ProcessManager.removePid(projectId);
45
+ console.log(pc.yellow(`项目 ${projectId} 的 agent 未在运行(已清理 PID 文件)\n`));
46
+ process.exit(0);
47
+ }
48
+ spinner.succeed('服务运行中');
49
+ // 尝试读取配置获取更多信息
50
+ let config;
51
+ try {
52
+ config = await loadConfig({ projectId });
53
+ }
54
+ catch (error) {
55
+ // 忽略配置加载错误
56
+ }
57
+ console.log(pc.cyan('\n📊 服务状态\n'));
58
+ console.log(`${pc.bold('项目 ID:')} ${projectId}`);
59
+ console.log(`${pc.bold('进程 ID:')} ${pid}`);
60
+ if (config) {
61
+ console.log(`${pc.bold('端口:')} ${config.port}`);
62
+ console.log(`${pc.bold('目录:')} ${config.dir}`);
63
+ }
64
+ console.log();
65
+ }
66
+ catch (error) {
67
+ spinner.fail('检查失败');
68
+ console.error(pc.red(`错误: ${error.message}`));
69
+ process.exit(1);
70
+ }
71
+ }
72
+ async function showAllStatus() {
73
+ const spinner = ora('检查所有服务状态...').start();
74
+ try {
75
+ const pids = await ProcessManager.getAllPids();
76
+ if (pids.length === 0) {
77
+ spinner.succeed('没有运行中的服务');
78
+ console.log(pc.yellow('没有找到运行中的 agent 服务\n'));
79
+ process.exit(0);
80
+ }
81
+ spinner.succeed(`找到 ${pids.length} 个运行中的服务`);
82
+ console.log(pc.cyan('\n📊 运行中的服务\n'));
83
+ for (const { projectId, pid } of pids) {
84
+ const isRunning = ProcessManager.isRunning(pid);
85
+ const status = isRunning ? pc.green('运行中') : pc.red('已停止');
86
+ console.log(`${pc.bold('项目:')} ${projectId}`);
87
+ console.log(`${pc.bold('PID:')} ${pid}`);
88
+ console.log(`${pc.bold('状态:')} ${status}`);
89
+ // 尝试读取配置
90
+ try {
91
+ const config = await loadConfig({ projectId });
92
+ console.log(`${pc.bold('端口:')} ${config.port}`);
93
+ console.log(`${pc.bold('目录:')} ${config.dir}`);
94
+ }
95
+ catch (error) {
96
+ // 忽略配置加载错误
97
+ }
98
+ console.log();
99
+ }
100
+ }
101
+ catch (error) {
102
+ spinner.fail('检查失败');
103
+ console.error(pc.red(`错误: ${error.message}`));
104
+ process.exit(1);
105
+ }
106
+ }
@@ -0,0 +1,58 @@
1
+ import { ProcessManager } from '../process-manager.js';
2
+ import { loadConfig } from '../config.js';
3
+ import pc from 'picocolors';
4
+ import ora from 'ora';
5
+ export async function stopCommand(options) {
6
+ const spinner = ora('查找运行中的服务...').start();
7
+ try {
8
+ let projectId = options.project;
9
+ // 如果没有指定项目,尝试从配置文件读取
10
+ if (!projectId) {
11
+ try {
12
+ const config = await loadConfig({});
13
+ projectId = config.projectId;
14
+ }
15
+ catch (error) {
16
+ // 忽略配置加载错误
17
+ }
18
+ }
19
+ if (!projectId) {
20
+ spinner.fail('未指定项目 ID');
21
+ console.error(pc.red('请使用 --project 参数指定要停止的项目 ID'));
22
+ process.exit(1);
23
+ }
24
+ const pid = await ProcessManager.loadPid(projectId);
25
+ if (!pid) {
26
+ spinner.fail('未找到运行中的服务');
27
+ console.log(pc.yellow(`项目 ${projectId} 的 agent 未在运行`));
28
+ process.exit(0);
29
+ }
30
+ if (!ProcessManager.isRunning(pid)) {
31
+ spinner.warn('发现僵尸 PID 文件');
32
+ await ProcessManager.removePid(projectId);
33
+ console.log(pc.yellow('已清理无效的 PID 文件'));
34
+ process.exit(0);
35
+ }
36
+ spinner.text = `正在停止进程 ${pid}...`;
37
+ await ProcessManager.killProcess(pid, 'SIGTERM');
38
+ // 等待进程退出
39
+ let attempts = 0;
40
+ while (ProcessManager.isRunning(pid) && attempts < 10) {
41
+ await new Promise(resolve => setTimeout(resolve, 500));
42
+ attempts++;
43
+ }
44
+ if (ProcessManager.isRunning(pid)) {
45
+ // 强制杀死
46
+ spinner.warn('进程未响应,强制终止...');
47
+ await ProcessManager.killProcess(pid, 'SIGKILL');
48
+ }
49
+ await ProcessManager.removePid(projectId);
50
+ spinner.succeed('服务已停止');
51
+ console.log(pc.green(`✅ 项目 ${projectId} 的 agent 已停止\n`));
52
+ }
53
+ catch (error) {
54
+ spinner.fail('停止失败');
55
+ console.error(pc.red(`错误: ${error.message}`));
56
+ process.exit(1);
57
+ }
58
+ }
package/dist/config.js ADDED
@@ -0,0 +1,31 @@
1
+ import { cosmiconfig } from 'cosmiconfig';
2
+ import { z } from 'zod';
3
+ import path from 'path';
4
+ import 'dotenv/config';
5
+ export const ConfigSchema = z.object({
6
+ projectId: z.string().optional(),
7
+ token: z.string().optional(),
8
+ dir: z.string().default(process.cwd()),
9
+ port: z.number().default(2468),
10
+ url: z.string().optional(), // Cloud API URL
11
+ });
12
+ const explorer = cosmiconfig('playcraft', {
13
+ searchPlaces: ['playcraft.config.json', 'playcraft.agent.config.json'],
14
+ });
15
+ export async function loadConfig(cliOptions) {
16
+ const result = await explorer.search(cliOptions.dir || process.cwd());
17
+ const fileConfig = result?.config || {};
18
+ const agentConfig = (fileConfig && typeof fileConfig === 'object' && 'agent' in fileConfig)
19
+ ? (fileConfig.agent || {})
20
+ : fileConfig;
21
+ const config = ConfigSchema.parse({
22
+ ...agentConfig,
23
+ ...cliOptions,
24
+ projectId: cliOptions.projectId || process.env.PLAYCRAFT_PROJECT_ID || agentConfig.projectId,
25
+ token: cliOptions.token || process.env.PLAYCRAFT_TOKEN || agentConfig.token,
26
+ port: cliOptions.port || (process.env.PLAYCRAFT_PORT ? parseInt(process.env.PLAYCRAFT_PORT) : undefined) || agentConfig.port,
27
+ });
28
+ // Resolve absolute path for dir
29
+ config.dir = path.resolve(process.cwd(), config.dir);
30
+ return config;
31
+ }
@@ -0,0 +1,83 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ export class FSHandler {
4
+ config;
5
+ constructor(config) {
6
+ this.config = config;
7
+ }
8
+ isSafePath(targetPath) {
9
+ const resolvedPath = path.resolve(this.config.dir, targetPath);
10
+ return resolvedPath.startsWith(this.config.dir);
11
+ }
12
+ async readFile(relativePath) {
13
+ if (!this.isSafePath(relativePath)) {
14
+ throw new Error('Access denied: path is outside of the project directory');
15
+ }
16
+ const fullPath = path.join(this.config.dir, relativePath);
17
+ try {
18
+ const stats = await fs.stat(fullPath);
19
+ if (!stats.isFile()) {
20
+ throw new Error('Path is not a file');
21
+ }
22
+ const content = await fs.readFile(fullPath, 'utf-8');
23
+ return {
24
+ exists: true,
25
+ content,
26
+ mtime: stats.mtime,
27
+ size: stats.size,
28
+ };
29
+ }
30
+ catch (error) {
31
+ if (error.code === 'ENOENT') {
32
+ return { exists: false };
33
+ }
34
+ throw error;
35
+ }
36
+ }
37
+ async listDirectory(relativePath = '.') {
38
+ if (!this.isSafePath(relativePath)) {
39
+ throw new Error('Access denied: path is outside of the project directory');
40
+ }
41
+ const fullPath = path.join(this.config.dir, relativePath);
42
+ try {
43
+ const entries = await fs.readdir(fullPath, { withFileTypes: true });
44
+ return entries
45
+ .filter(entry => !entry.name.startsWith('.')) // Filter hidden files
46
+ .map(entry => ({
47
+ name: entry.name,
48
+ type: entry.isDirectory() ? 'directory' : 'file',
49
+ }));
50
+ }
51
+ catch (error) {
52
+ if (error.code === 'ENOENT') {
53
+ throw new Error('Directory not found');
54
+ }
55
+ throw error;
56
+ }
57
+ }
58
+ async readBinaryFile(relativePath) {
59
+ if (!this.isSafePath(relativePath)) {
60
+ throw new Error('Access denied: path is outside of the project directory');
61
+ }
62
+ const fullPath = path.join(this.config.dir, relativePath);
63
+ try {
64
+ const stats = await fs.stat(fullPath);
65
+ if (!stats.isFile()) {
66
+ throw new Error('Path is not a file');
67
+ }
68
+ const buffer = await fs.readFile(fullPath);
69
+ return {
70
+ exists: true,
71
+ buffer,
72
+ mtime: stats.mtime,
73
+ size: stats.size,
74
+ };
75
+ }
76
+ catch (error) {
77
+ if (error.code === 'ENOENT') {
78
+ return { exists: false };
79
+ }
80
+ throw error;
81
+ }
82
+ }
83
+ }
package/dist/index.js ADDED
@@ -0,0 +1,200 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { initCommand } from './commands/init.js';
4
+ import { startCommand, startInternal } from './commands/start.js';
5
+ import { stopCommand } from './commands/stop.js';
6
+ import { statusCommand } from './commands/status.js';
7
+ import { logsCommand } from './commands/logs.js';
8
+ import { configCommand } from './commands/config.js';
9
+ import { buildCommand } from './commands/build.js';
10
+ const program = new Command();
11
+ program
12
+ .name('playcraft')
13
+ .description('PlayCraft Local Dev Agent - 本地开发助手')
14
+ .version('0.0.1');
15
+ // init 命令
16
+ program
17
+ .command('init')
18
+ .description('交互式初始化配置文件')
19
+ .option('-d, --dir <path>', '配置文件目录', process.cwd())
20
+ .action(async (options) => {
21
+ await initCommand(options.dir);
22
+ });
23
+ // start 命令
24
+ program
25
+ .command('start')
26
+ .description('启动本地开发 agent')
27
+ .option('-p, --project <id>', '项目 ID')
28
+ .option('-t, --token <token>', '认证令牌')
29
+ .option('-d, --dir <path>', '监听目录', process.cwd())
30
+ .option('--port <port>', '服务端口', '2468')
31
+ .option('--daemon', '守护进程模式(后台运行)')
32
+ .action(async (options) => {
33
+ // 检查是否是内部调用(守护进程)
34
+ if (process.env.PLAYCRAFT_INTERNAL === 'true') {
35
+ await startInternal();
36
+ }
37
+ else {
38
+ await startCommand(options);
39
+ }
40
+ });
41
+ // stop 命令
42
+ program
43
+ .command('stop')
44
+ .description('停止运行中的 agent 服务')
45
+ .option('-p, --project <id>', '项目 ID')
46
+ .action(async (options) => {
47
+ await stopCommand(options);
48
+ });
49
+ // status 命令
50
+ program
51
+ .command('status')
52
+ .description('查看服务运行状态')
53
+ .option('-p, --project <id>', '项目 ID')
54
+ .option('-a, --all', '显示所有运行中的服务')
55
+ .action(async (options) => {
56
+ await statusCommand(options);
57
+ });
58
+ // logs 命令
59
+ program
60
+ .command('logs')
61
+ .description('查看服务日志')
62
+ .option('-p, --project <id>', '项目 ID')
63
+ .option('-f, --follow', '实时跟踪日志')
64
+ .option('-n, --lines <number>', '显示最后 N 行', '50')
65
+ .action(async (options) => {
66
+ await logsCommand(options);
67
+ });
68
+ // config 命令
69
+ program
70
+ .command('config')
71
+ .description('配置管理')
72
+ .argument('<action>', '操作: get, set, list')
73
+ .option('-p, --project <id>', '项目 ID')
74
+ .option('-k, --key <key>', '配置键(用于 get/set)')
75
+ .option('-v, --value <value>', '配置值(用于 set)')
76
+ .action(async (action, options) => {
77
+ await configCommand(action, options);
78
+ });
79
+ // build 命令 - 完整流程(阶段1 + 阶段2)
80
+ program
81
+ .command('build')
82
+ .description('打包项目为 Playable Ad(自动执行基础构建和渠道打包)')
83
+ .argument('[project-path]', '项目路径', process.cwd())
84
+ .option('-p, --platform <platform>', '目标平台 (facebook|snapchat|ironsource|applovin|google|tiktok|unity|liftoff|moloco|bigo)')
85
+ .option('-f, --format <format>', '输出格式 (html|zip)', 'html')
86
+ .option('-o, --output <path>', '输出目录', './dist')
87
+ .option('-c, --config <file>', '配置文件路径')
88
+ .option('--compress', '压缩引擎代码')
89
+ .option('--analyze', '生成打包分析报告')
90
+ .option('--replace-ammo', '检测到 Ammo 时自动替换为 p2 或 cannon')
91
+ .option('--ammo-engine <engine>', '指定替换物理引擎 (p2|cannon)')
92
+ .option('--auto-build', '如果是源代码项目,自动执行本地构建')
93
+ .option('--skip-build', '跳过本地构建,直接从源代码打包(可能不完整)')
94
+ .option('--mode <mode>', '构建模式 (full|base|playable)')
95
+ .option('--use-vite', '使用 Vite 构建(默认启用)', true)
96
+ .option('--no-use-vite', '禁用 Vite,使用旧构建器')
97
+ .option('--css-minify', '压缩 CSS(默认根据平台配置)')
98
+ .option('--no-css-minify', '禁用 CSS 压缩')
99
+ .option('--js-minify', '压缩 JS(默认根据平台配置)')
100
+ .option('--no-js-minify', '禁用 JS 压缩')
101
+ .option('--compress-images', '压缩图片(默认根据平台配置)')
102
+ .option('--no-compress-images', '禁用图片压缩')
103
+ .option('--image-quality <quality>', '图片质量 0-100(默认根据平台配置)', parseInt)
104
+ .option('--convert-webp', '转换为 WebP 格式(默认启用)', true)
105
+ .option('--no-convert-webp', '禁用 WebP 转换')
106
+ .option('--compress-models', '压缩 3D 模型(默认根据平台配置)')
107
+ .option('--no-compress-models', '禁用模型压缩')
108
+ .option('--model-compression <method>', '模型压缩方法 (draco|meshopt)', 'draco')
109
+ .action(async (projectPath, options) => {
110
+ await buildCommand(projectPath, options);
111
+ });
112
+ // build-base 命令 - 只执行阶段1:基础构建
113
+ program
114
+ .command('build-base')
115
+ .description('生成可运行的多文件构建产物(阶段1)')
116
+ .argument('<project-path>', '项目路径')
117
+ .option('-o, --output <path>', '输出目录', './build')
118
+ .action(async (projectPath, options) => {
119
+ await buildCommand(projectPath, {
120
+ ...options,
121
+ baseOnly: true,
122
+ platform: 'facebook', // 占位符,不会被使用
123
+ });
124
+ });
125
+ // analyze 命令 - 生成打包分析报告
126
+ program
127
+ .command('analyze')
128
+ .description('生成打包分析报告(可视化 + 体积建议)')
129
+ .argument('[project-path]', '项目路径', process.cwd())
130
+ .option('-p, --platform <platform>', '目标平台 (facebook|snapchat|ironsource|applovin|google|tiktok|unity|liftoff|moloco|bigo)')
131
+ .option('-f, --format <format>', '输出格式 (html|zip)', 'html')
132
+ .option('-o, --output <path>', '输出目录', './dist')
133
+ .option('-c, --config <file>', '配置文件路径')
134
+ .option('--compress', '压缩引擎代码')
135
+ .option('--replace-ammo', '检测到 Ammo 时自动替换为 p2 或 cannon')
136
+ .option('--ammo-engine <engine>', '指定替换物理引擎 (p2|cannon)')
137
+ .option('--mode <mode>', '构建模式 (full|base|playable)')
138
+ .option('--use-vite', '使用 Vite 构建(默认启用)', true)
139
+ .option('--no-use-vite', '禁用 Vite,使用旧构建器')
140
+ .option('--css-minify', '压缩 CSS(默认根据平台配置)')
141
+ .option('--no-css-minify', '禁用 CSS 压缩')
142
+ .option('--js-minify', '压缩 JS(默认根据平台配置)')
143
+ .option('--no-js-minify', '禁用 JS 压缩')
144
+ .option('--compress-images', '压缩图片(默认根据平台配置)')
145
+ .option('--no-compress-images', '禁用图片压缩')
146
+ .option('--image-quality <quality>', '图片质量 0-100(默认根据平台配置)', parseInt)
147
+ .option('--convert-webp', '转换为 WebP 格式(默认启用)', true)
148
+ .option('--no-convert-webp', '禁用 WebP 转换')
149
+ .option('--compress-models', '压缩 3D 模型(默认根据平台配置)')
150
+ .option('--no-compress-models', '禁用模型压缩')
151
+ .option('--model-compression <method>', '模型压缩方法 (draco|meshopt)', 'draco')
152
+ .action(async (projectPath, options) => {
153
+ await buildCommand(projectPath, {
154
+ ...options,
155
+ analyze: true,
156
+ });
157
+ });
158
+ // build-playable 命令 - 只执行阶段2:渠道打包
159
+ program
160
+ .command('build-playable')
161
+ .description('将多文件构建产物转换为单HTML Playable Ads(阶段2)')
162
+ .argument('<base-build-dir>', '多文件构建产物目录')
163
+ .requiredOption('-p, --platform <platform>', '目标平台 (facebook|snapchat|ironsource|applovin|google|tiktok|unity|liftoff|moloco|bigo)')
164
+ .option('-f, --format <format>', '输出格式 (html|zip)', 'html')
165
+ .option('-o, --output <path>', '输出目录', './dist')
166
+ .option('-c, --config <file>', '配置文件路径')
167
+ .option('--compress', '压缩引擎代码')
168
+ .option('--analyze', '生成打包分析报告')
169
+ .option('--replace-ammo', '检测到 Ammo 时自动替换为 p2 或 cannon')
170
+ .option('--ammo-engine <engine>', '指定替换物理引擎 (p2|cannon)')
171
+ .option('--use-vite', '使用 Vite 构建(默认启用)', true)
172
+ .option('--no-use-vite', '禁用 Vite,使用旧构建器')
173
+ .option('--css-minify', '压缩 CSS(默认根据平台配置)')
174
+ .option('--no-css-minify', '禁用 CSS 压缩')
175
+ .option('--js-minify', '压缩 JS(默认根据平台配置)')
176
+ .option('--no-js-minify', '禁用 JS 压缩')
177
+ .option('--compress-images', '压缩图片(默认根据平台配置)')
178
+ .option('--no-compress-images', '禁用图片压缩')
179
+ .option('--image-quality <quality>', '图片质量 0-100(默认根据平台配置)', parseInt)
180
+ .option('--convert-webp', '转换为 WebP 格式(默认启用)', true)
181
+ .option('--no-convert-webp', '禁用 WebP 转换')
182
+ .option('--compress-models', '压缩 3D 模型(默认根据平台配置)')
183
+ .option('--no-compress-models', '禁用模型压缩')
184
+ .option('--model-compression <method>', '模型压缩方法 (draco|meshopt)', 'draco')
185
+ .action(async (baseBuildDir, options) => {
186
+ await buildCommand(baseBuildDir, {
187
+ ...options,
188
+ mode: 'playable',
189
+ });
190
+ });
191
+ // 新增:inspect 命令 - 检查项目类型
192
+ program
193
+ .command('inspect')
194
+ .description('检查项目类型和结构')
195
+ .argument('[path]', '项目路径', process.cwd())
196
+ .action(async (projectPath) => {
197
+ const { inspectCommand } = await import('./commands/inspect.js');
198
+ await inspectCommand(projectPath);
199
+ });
200
+ program.parse(process.argv);
package/dist/logger.js ADDED
@@ -0,0 +1,122 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import { createWriteStream } from 'fs';
5
+ import pc from 'picocolors';
6
+ const PLAYCRAFT_DIR = path.join(os.homedir(), '.playcraft');
7
+ const LOGS_DIR = path.join(PLAYCRAFT_DIR, 'logs');
8
+ export var LogLevel;
9
+ (function (LogLevel) {
10
+ LogLevel["DEBUG"] = "DEBUG";
11
+ LogLevel["INFO"] = "INFO";
12
+ LogLevel["WARN"] = "WARN";
13
+ LogLevel["ERROR"] = "ERROR";
14
+ })(LogLevel || (LogLevel = {}));
15
+ export class Logger {
16
+ logFile;
17
+ stream = null;
18
+ projectId;
19
+ isDaemon;
20
+ constructor(projectId, isDaemon = false) {
21
+ this.projectId = projectId;
22
+ this.isDaemon = isDaemon;
23
+ this.logFile = path.join(LOGS_DIR, `${projectId}.log`);
24
+ }
25
+ async ensureLogDirectory() {
26
+ try {
27
+ await fs.mkdir(LOGS_DIR, { recursive: true });
28
+ }
29
+ catch (error) {
30
+ if (error.code !== 'EEXIST') {
31
+ throw new Error(`Failed to create log directory: ${error.message}`);
32
+ }
33
+ }
34
+ }
35
+ formatMessage(level, message) {
36
+ const timestamp = new Date().toISOString();
37
+ return `[${timestamp}] [${level}] ${message}\n`;
38
+ }
39
+ getColorFunction(level) {
40
+ switch (level) {
41
+ case LogLevel.DEBUG:
42
+ return pc.gray;
43
+ case LogLevel.INFO:
44
+ return pc.cyan;
45
+ case LogLevel.WARN:
46
+ return pc.yellow;
47
+ case LogLevel.ERROR:
48
+ return pc.red;
49
+ default:
50
+ return (text) => text;
51
+ }
52
+ }
53
+ async initialize() {
54
+ await this.ensureLogDirectory();
55
+ if (this.isDaemon) {
56
+ this.stream = createWriteStream(this.logFile, { flags: 'a' });
57
+ }
58
+ }
59
+ async write(level, message) {
60
+ const formatted = this.formatMessage(level, message);
61
+ // 写入文件(如果是守护进程模式)
62
+ if (this.stream) {
63
+ this.stream.write(formatted);
64
+ }
65
+ else {
66
+ // 前台模式:同时写入文件和终端
67
+ await fs.appendFile(this.logFile, formatted, 'utf-8');
68
+ }
69
+ // 输出到终端(如果不是守护进程模式)
70
+ if (!this.isDaemon) {
71
+ const colorFn = this.getColorFunction(level);
72
+ const prefix = colorFn(`[${level}]`);
73
+ console.log(`${prefix} ${message}`);
74
+ }
75
+ }
76
+ async debug(message) {
77
+ await this.write(LogLevel.DEBUG, message);
78
+ }
79
+ async info(message) {
80
+ await this.write(LogLevel.INFO, message);
81
+ }
82
+ async warn(message) {
83
+ await this.write(LogLevel.WARN, message);
84
+ }
85
+ async error(message) {
86
+ await this.write(LogLevel.ERROR, message);
87
+ }
88
+ async close() {
89
+ if (this.stream) {
90
+ return new Promise((resolve, reject) => {
91
+ this.stream.end(() => {
92
+ this.stream = null;
93
+ resolve();
94
+ });
95
+ this.stream.on('error', reject);
96
+ });
97
+ }
98
+ }
99
+ getLogFilePath() {
100
+ return this.logFile;
101
+ }
102
+ static async readLogFile(projectId, lines) {
103
+ const logFile = path.join(LOGS_DIR, `${projectId}.log`);
104
+ try {
105
+ const content = await fs.readFile(logFile, 'utf-8');
106
+ const allLines = content.split('\n').filter(line => line.trim());
107
+ if (lines && lines > 0) {
108
+ return allLines.slice(-lines);
109
+ }
110
+ return allLines;
111
+ }
112
+ catch (error) {
113
+ if (error.code === 'ENOENT') {
114
+ return [];
115
+ }
116
+ throw error;
117
+ }
118
+ }
119
+ static getLogFilePath(projectId) {
120
+ return path.join(LOGS_DIR, `${projectId}.log`);
121
+ }
122
+ }