@fecommunity/reactpress 4.0.0-beta.15 → 4.0.0-beta.18

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 (82) hide show
  1. package/admin-dist/assets/{admin-bamoE2Tn.js → admin-CF4LIXsB.js} +1 -1
  2. package/admin-dist/assets/{admin-CrL6Xc9b.js → admin-DYBZ60Ac.js} +1 -1
  3. package/admin-dist/assets/{cssMode-J2cfgFkq.js → cssMode-BJqbvgLG.js} +1 -1
  4. package/admin-dist/assets/{freemarker2-L_gr_Wq4.js → freemarker2-CYGOmtYQ.js} +1 -1
  5. package/admin-dist/assets/{handlebars-D81kzUDC.js → handlebars-qhE4jgvZ.js} +1 -1
  6. package/admin-dist/assets/{html-Cung-Yr0.js → html-U5gI8Ly8.js} +1 -1
  7. package/admin-dist/assets/{htmlMode-C6QemWl2.js → htmlMode-DQd5Eb2D.js} +1 -1
  8. package/admin-dist/assets/{index-DXTkTQ21.js → index-BKNoDtfb.js} +178 -178
  9. package/admin-dist/assets/{javascript-WUcGtCKt.js → javascript-DU3Ebk7L.js} +1 -1
  10. package/admin-dist/assets/{jsonMode-Dp0R9P1F.js → jsonMode-D1w8Vhjb.js} +1 -1
  11. package/admin-dist/assets/{liquid-OFb9c-6B.js → liquid-BPVbnl_K.js} +1 -1
  12. package/admin-dist/assets/{mdx-CUfjLxA3.js → mdx-CMSYE9vh.js} +1 -1
  13. package/admin-dist/assets/{python-zxEYY77U.js → python-Dbpms-Xp.js} +1 -1
  14. package/admin-dist/assets/{razor-DJrSr4ly.js → razor-CWQdYrqP.js} +1 -1
  15. package/admin-dist/assets/{tsMode-Dtxx1cRn.js → tsMode-DAsZhwU2.js} +1 -1
  16. package/admin-dist/assets/{typescript-DT11gl4K.js → typescript-7mrVr_47.js} +1 -1
  17. package/admin-dist/assets/{xml-BIbLvdwx.js → xml-DbPOrhNN.js} +1 -1
  18. package/admin-dist/assets/{yaml-BEeUZskE.js → yaml-6hDyKyrt.js} +1 -1
  19. package/admin-dist/index.html +1 -1
  20. package/out/bin/reactpress-theme-client.js +4 -0
  21. package/out/bin/reactpress.js +7 -2
  22. package/out/core/services/admin-static.js +79 -26
  23. package/out/core/services/local-site.js +68 -0
  24. package/out/lib/bootstrap.js +9 -0
  25. package/out/lib/i18n/strings.js +20 -8
  26. package/out/lib/lifecycle.js +9 -0
  27. package/out/lib/pm2-runtime.js +15 -0
  28. package/out/lib/project-logs.js +318 -12
  29. package/package.json +2 -1
  30. package/plugins/hello-world/README.md +104 -0
  31. package/plugins/hello-world/dist/config.d.ts +4 -0
  32. package/plugins/hello-world/dist/config.js +17 -0
  33. package/plugins/hello-world/dist/index.d.ts +10 -0
  34. package/plugins/hello-world/dist/index.js +15 -0
  35. package/plugins/hello-world/dist/summary.d.ts +3 -0
  36. package/plugins/hello-world/dist/summary.js +33 -0
  37. package/plugins/hello-world/dist/types.d.ts +16 -0
  38. package/plugins/hello-world/dist/types.js +2 -0
  39. package/plugins/hello-world/locales/en.json +24 -0
  40. package/plugins/hello-world/locales/zh.json +24 -0
  41. package/plugins/hello-world/package.json +20 -0
  42. package/plugins/hello-world/plugin.json +61 -0
  43. package/plugins/hello-world/tsconfig.json +16 -0
  44. package/plugins/image-optimizer/README.md +43 -0
  45. package/plugins/image-optimizer/admin/OptimizeDashboard.tsx +249 -0
  46. package/plugins/image-optimizer/admin/api.ts +133 -0
  47. package/plugins/image-optimizer/admin/index.ts +16 -0
  48. package/plugins/image-optimizer/admin/locale.ts +68 -0
  49. package/plugins/image-optimizer/admin/optimize-dashboard.module.css +80 -0
  50. package/plugins/image-optimizer/dist/config.d.ts +8 -0
  51. package/plugins/image-optimizer/dist/config.js +13 -0
  52. package/plugins/image-optimizer/dist/index.d.ts +10 -0
  53. package/plugins/image-optimizer/dist/index.js +13 -0
  54. package/plugins/image-optimizer/locales/en.json +51 -0
  55. package/plugins/image-optimizer/locales/zh.json +53 -0
  56. package/plugins/image-optimizer/package.json +20 -0
  57. package/plugins/image-optimizer/plugin.json +58 -0
  58. package/plugins/image-optimizer/tsconfig.json +16 -0
  59. package/plugins/package.json +14 -0
  60. package/plugins/seo/README.md +60 -0
  61. package/plugins/seo/admin/ArticleEditorSeoPanel.tsx +67 -0
  62. package/plugins/seo/admin/article-editor-seo-panel.module.css +74 -0
  63. package/plugins/seo/admin/index.ts +18 -0
  64. package/plugins/seo/dist/config.d.ts +4 -0
  65. package/plugins/seo/dist/config.js +19 -0
  66. package/plugins/seo/dist/index.d.ts +10 -0
  67. package/plugins/seo/dist/index.js +23 -0
  68. package/plugins/seo/dist/seo.d.ts +3 -0
  69. package/plugins/seo/dist/seo.js +79 -0
  70. package/plugins/seo/dist/slug.d.ts +2 -0
  71. package/plugins/seo/dist/slug.js +13 -0
  72. package/plugins/seo/dist/types.d.ts +23 -0
  73. package/plugins/seo/dist/types.js +2 -0
  74. package/plugins/seo/locales/en.json +32 -0
  75. package/plugins/seo/locales/zh.json +32 -0
  76. package/plugins/seo/package.json +20 -0
  77. package/plugins/seo/plugin.json +76 -0
  78. package/plugins/seo/tsconfig.json +16 -0
  79. package/server/bin/reactpress-server.js +17 -0
  80. package/server/package-lock.json +3 -3
  81. package/server/package.json +1 -1
  82. package/toolkit/package.json +1 -1
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.getLocalSitePaths = getLocalSitePaths;
7
7
  exports.ensureLocalSite = ensureLocalSite;
8
+ exports.ensureBundledPlugins = ensureBundledPlugins;
8
9
  exports.initLocalProject = initLocalProject;
9
10
  const fs_extra_1 = __importDefault(require("fs-extra"));
10
11
  const node_path_1 = __importDefault(require("node:path"));
@@ -44,6 +45,7 @@ function ensureLocalSite(siteRoot, port, options = {}) {
44
45
  if (options.monorepoRoot) {
45
46
  seedBundledAssets(siteRoot, options.monorepoRoot);
46
47
  }
48
+ ensureBundledPlugins(siteRoot);
47
49
  const configPath = node_path_1.default.join(paths.reactpressDir, 'config.json');
48
50
  if (!fs_extra_1.default.existsSync(configPath)) {
49
51
  const config = {
@@ -119,6 +121,72 @@ function seedRuntimeThemes(siteRoot, monorepoRoot) {
119
121
  }
120
122
  }
121
123
  }
124
+ const PLUGIN_COPY_SKIP_DIRS = new Set(['node_modules', '.git', '.turbo', 'src', 'coverage']);
125
+ function hasUsablePluginsRegistry(pluginsDir) {
126
+ const pkgPath = node_path_1.default.join(pluginsDir, 'package.json');
127
+ if (!fs_extra_1.default.existsSync(pkgPath))
128
+ return false;
129
+ try {
130
+ const meta = JSON.parse(fs_extra_1.default.readFileSync(pkgPath, 'utf8'));
131
+ const local = Array.isArray(meta.reactpress?.local) ? meta.reactpress.local : [];
132
+ return local.some((id) => typeof id === 'string' &&
133
+ id.trim() &&
134
+ fs_extra_1.default.existsSync(node_path_1.default.join(pluginsDir, id.trim(), 'plugin.json')));
135
+ }
136
+ catch {
137
+ return false;
138
+ }
139
+ }
140
+ function copyPluginTree(sourceDir, targetDir) {
141
+ fs_extra_1.default.mkdirSync(targetDir, { recursive: true });
142
+ for (const entry of fs_extra_1.default.readdirSync(sourceDir, { withFileTypes: true })) {
143
+ const sourcePath = node_path_1.default.join(sourceDir, entry.name);
144
+ const targetPath = node_path_1.default.join(targetDir, entry.name);
145
+ if (entry.isDirectory()) {
146
+ if (PLUGIN_COPY_SKIP_DIRS.has(entry.name))
147
+ continue;
148
+ copyPluginTree(sourcePath, targetPath);
149
+ continue;
150
+ }
151
+ fs_extra_1.default.copyFileSync(sourcePath, targetPath);
152
+ }
153
+ }
154
+ function copyBundledPlugins(sourceDir, targetDir) {
155
+ const sourcePackageJson = node_path_1.default.join(sourceDir, 'package.json');
156
+ if (!fs_extra_1.default.existsSync(sourcePackageJson))
157
+ return;
158
+ fs_extra_1.default.mkdirSync(targetDir, { recursive: true });
159
+ fs_extra_1.default.copyFileSync(sourcePackageJson, node_path_1.default.join(targetDir, 'package.json'));
160
+ let meta = {};
161
+ try {
162
+ meta = JSON.parse(fs_extra_1.default.readFileSync(sourcePackageJson, 'utf8'));
163
+ }
164
+ catch {
165
+ return;
166
+ }
167
+ const local = Array.isArray(meta.reactpress?.local) ? meta.reactpress.local : [];
168
+ for (const id of local) {
169
+ if (typeof id !== 'string' || !id.trim())
170
+ continue;
171
+ const sourcePath = node_path_1.default.join(sourceDir, id.trim());
172
+ const targetPath = node_path_1.default.join(targetDir, id.trim());
173
+ if (!fs_extra_1.default.existsSync(node_path_1.default.join(sourcePath, 'plugin.json')) || fs_extra_1.default.existsSync(targetPath))
174
+ continue;
175
+ copyPluginTree(sourcePath, targetPath);
176
+ }
177
+ }
178
+ /** Seed default plugins from the CLI bundle when the site has no plugins registry yet. */
179
+ function ensureBundledPlugins(siteRoot) {
180
+ const targetDir = node_path_1.default.join(siteRoot, 'plugins');
181
+ if (hasUsablePluginsRegistry(targetDir))
182
+ return false;
183
+ const bundledDir = node_path_1.default.join((0, paths_1.getPackageRoot)(), 'plugins');
184
+ if (fs_extra_1.default.existsSync(node_path_1.default.join(bundledDir, 'package.json'))) {
185
+ copyBundledPlugins(bundledDir, targetDir);
186
+ return hasUsablePluginsRegistry(targetDir);
187
+ }
188
+ return false;
189
+ }
122
190
  async function initLocalProject(projectRoot, options = {}) {
123
191
  const paths = (0, paths_1.getProjectPaths)(projectRoot);
124
192
  const port = options.port ?? 3002;
@@ -43,6 +43,15 @@ async function ensureProjectEnvironment(projectRoot = (0, root_1.ensureOriginalC
43
43
  applyZeroDependencyDefaults();
44
44
  const root = node_path_1.default.resolve(projectRoot);
45
45
  (0, cli_context_1.setProjectCwd)(root);
46
+ try {
47
+ const { ensureBundledPlugins } = require('../core/services/local-site');
48
+ if (ensureBundledPlugins(root)) {
49
+ console.log(`[reactpress] ${(0, i18n_1.t)('init.pluginsSeeded')}`);
50
+ }
51
+ }
52
+ catch {
53
+ // bundled plugins are optional; ignore when CLI core is unavailable
54
+ }
46
55
  if (!(await (0, config_1.isReactPressProject)(root))) {
47
56
  const result = await (0, init_1.initProject)({ directory: root, force: false });
48
57
  if (!result.ok) {
@@ -76,12 +76,13 @@ const STRINGS = {
76
76
  'cli.doctor.logs.sourceOption': 'Log source: error, request, response, or all',
77
77
  'cli.doctor.logs.grepOption': 'Filter log lines by regex',
78
78
  'cli.doctor.logs.listOption': 'List available log files only',
79
- 'cli.logs.description': 'View recent API logs (error, request, response)',
79
+ 'cli.logs.description': 'View API logs (PM2 process output or structured error/request/response)',
80
80
  'cli.logs.descriptionAlias': 'View API logs (alias of reactpress logs)',
81
81
  'cli.logs.tailOption': 'Number of lines to show per log file',
82
- 'cli.logs.sourceOption': 'Log source: error, request, response, or all',
82
+ 'cli.logs.sourceOption': 'Log source: process (PM2), error, request, response, or all',
83
83
  'cli.logs.grepOption': 'Filter log lines by regex',
84
84
  'cli.logs.listOption': 'List available log files only',
85
+ 'cli.logs.followOption': 'Stream logs continuously (PM2 when supervised, otherwise tail -f)',
85
86
  'cli.stop.description': 'Stop API and site services started by init',
86
87
  'cli.db.description': 'Database operations',
87
88
  'cli.db.backup': 'Backup current project database with mysqldump',
@@ -124,6 +125,7 @@ const STRINGS = {
124
125
  'init.hint': 'Login: admin / admin · Services run in background · Stop: reactpress stop · Logs: reactpress logs',
125
126
  'init.alreadyInitialized': 'Project already initialized — ensuring services are running',
126
127
  'init.apiOnlyHint': 'API is running. Install a theme in Admin → Appearance to enable the public site.',
128
+ 'init.pluginsSeeded': 'Seeded bundled plugins (hello-world, seo, image-optimizer).',
127
129
  'init.unknownCommand': 'Unknown command: {cmd}',
128
130
  'init.useInitOnly': 'ReactPress 4: reactpress init · doctor · logs · stop',
129
131
  'banner.subtitle': '· Publishing platform · v4.0',
@@ -432,15 +434,19 @@ const STRINGS = {
432
434
  'doctor.logs.subtitle': 'API log viewer',
433
435
  'doctor.logs.dir': 'Log dir {path}',
434
436
  'doctor.logs.empty': 'No API log files found yet.',
435
- 'doctor.logs.emptyHint': 'Start the API with reactpress init — logs are written to .reactpress/logs/server/',
437
+ 'doctor.logs.emptyHint': 'Start the API with reactpress init — logs are written to .reactpress/logs/server/ (PM2 output: pm2-out.log / pm2-error.log)',
436
438
  'doctor.logs.listHeader': 'Available log files',
437
439
  'doctor.logs.fileHeader': '{file} (showing {count} line(s))',
438
440
  'doctor.logs.noMatchingLines': 'No matching log lines.',
439
441
  'doctor.logs.badPattern': 'Invalid grep pattern: {pattern}',
440
442
  'doctor.logs.noPid': 'No API pid file at {path}',
443
+ 'doctor.logs.activePid': 'API listening on port {port} (pid {pid})',
444
+ 'doctor.logs.pm2Supervised': 'API supervised by PM2 (pid {pid})',
445
+ 'doctor.logs.pm2Unavailable': 'PM2 is not available for live log streaming.',
446
+ 'doctor.logs.followWindows': 'Live follow is not supported here. Open: {path}',
441
447
  'doctor.logs.pidRunning': 'API pid {pid} (recorded in {path})',
442
448
  'doctor.logs.pidStale': 'Stale pid {pid} in {path} — process not running',
443
- 'doctor.logs.moreHint': 'Use --tail, --source, --grep, or --list for more control (see reactpress logs --help)',
449
+ 'doctor.logs.moreHint': 'Use --tail, --source, --grep, --follow, or --list for more control (see reactpress logs --help)',
444
450
  'status.title': 'ReactPress project status',
445
451
  'status.dir': 'Project {path}',
446
452
  'status.apiSource': 'API source {source}',
@@ -751,12 +757,13 @@ const STRINGS = {
751
757
  'cli.doctor.logs.sourceOption': '日志类型:error、request、response 或 all',
752
758
  'cli.doctor.logs.grepOption': '用正则过滤日志行',
753
759
  'cli.doctor.logs.listOption': '仅列出可用日志文件',
754
- 'cli.logs.description': '查看 API 日志(error / request / response)',
760
+ 'cli.logs.description': '查看 API 日志(PM2 进程输出或结构化 error/request/response)',
755
761
  'cli.logs.descriptionAlias': '查看 API 日志(同 reactpress logs)',
756
762
  'cli.logs.tailOption': '每个日志文件显示的行数',
757
- 'cli.logs.sourceOption': '日志类型:error、request、response 或 all',
763
+ 'cli.logs.sourceOption': '日志类型:process(PM2)、error、request、response 或 all',
758
764
  'cli.logs.grepOption': '用正则过滤日志行',
759
765
  'cli.logs.listOption': '仅列出可用日志文件',
766
+ 'cli.logs.followOption': '持续输出日志(PM2 托管时用 pm2 logs,否则 tail -f)',
760
767
  'cli.stop.description': '停止 init 启动的 API 与站点服务',
761
768
  'cli.db.description': '数据库运维',
762
769
  'cli.db.backup': '使用 mysqldump 备份当前项目数据库',
@@ -799,6 +806,7 @@ const STRINGS = {
799
806
  'init.hint': '账号 admin / admin · 服务已在后台运行 · 停止:reactpress stop · 日志:reactpress logs',
800
807
  'init.alreadyInitialized': '项目已初始化 — 正在确保服务运行',
801
808
  'init.apiOnlyHint': 'API 已运行。请在管理后台「外观 → 主题」安装主题以启用访客站。',
809
+ 'init.pluginsSeeded': '已写入内置插件(hello-world、seo、image-optimizer)。',
802
810
  'init.unknownCommand': '未知命令:{cmd}',
803
811
  'init.useInitOnly': 'ReactPress 4 支持:reactpress init · doctor · logs · stop',
804
812
  'banner.subtitle': '· 发布平台 · v4.0',
@@ -1106,15 +1114,19 @@ const STRINGS = {
1106
1114
  'doctor.logs.subtitle': 'API 日志查看',
1107
1115
  'doctor.logs.dir': '日志目录 {path}',
1108
1116
  'doctor.logs.empty': '尚未找到 API 日志文件。',
1109
- 'doctor.logs.emptyHint': '运行 reactpress init 启动 API 后,日志会写入 .reactpress/logs/server/',
1117
+ 'doctor.logs.emptyHint': '运行 reactpress init 启动 API 后,日志写入 .reactpress/logs/server/(PM2 输出:pm2-out.log / pm2-error.log)',
1110
1118
  'doctor.logs.listHeader': '可用日志文件',
1111
1119
  'doctor.logs.fileHeader': '{file}(显示 {count} 行)',
1112
1120
  'doctor.logs.noMatchingLines': '没有匹配的日志行。',
1113
1121
  'doctor.logs.badPattern': 'grep 正则无效:{pattern}',
1114
1122
  'doctor.logs.noPid': '未找到 API pid 文件:{path}',
1123
+ 'doctor.logs.activePid': 'API 正在监听端口 {port}(pid {pid})',
1124
+ 'doctor.logs.pm2Supervised': 'API 由 PM2 托管(pid {pid})',
1125
+ 'doctor.logs.pm2Unavailable': '无法使用 PM2 实时输出日志。',
1126
+ 'doctor.logs.followWindows': '当前环境不支持持续跟踪,请直接打开:{path}',
1115
1127
  'doctor.logs.pidRunning': 'API 进程 pid {pid}(记录在 {path})',
1116
1128
  'doctor.logs.pidStale': 'pid 文件 {path} 中的 {pid} 已失效(进程未运行)',
1117
- 'doctor.logs.moreHint': '可用 --tail、--source、--grep、--list 进一步筛选(见 reactpress logs --help)',
1129
+ 'doctor.logs.moreHint': '可用 --tail、--source、--grep、--follow、--list 进一步筛选(见 reactpress logs --help)',
1118
1130
  'status.title': 'ReactPress 项目状态',
1119
1131
  'status.dir': '项目目录 {path}',
1120
1132
  'status.apiSource': 'API 来源 {source}',
@@ -80,6 +80,8 @@ async function startApiWithPm2(projectRoot, { wait = true } = {}) {
80
80
  const serverDir = getServerDir(projectRoot);
81
81
  const serverLogDir = path.join(projectRoot, '.reactpress', 'logs', 'server');
82
82
  fs.mkdirSync(serverLogDir, { recursive: true });
83
+ const pm2OutLog = path.join(serverLogDir, 'pm2-out.log');
84
+ const pm2ErrLog = path.join(serverLogDir, 'pm2-error.log');
83
85
  const started = runPm2(projectRoot, [
84
86
  'start',
85
87
  serverBin,
@@ -87,6 +89,10 @@ async function startApiWithPm2(projectRoot, { wait = true } = {}) {
87
89
  PM2_API_APP,
88
90
  '--cwd',
89
91
  serverDir,
92
+ '--output',
93
+ pm2OutLog,
94
+ '--error',
95
+ pm2ErrLog,
90
96
  '--max-memory-restart',
91
97
  getPm2ServerMemoryRestart(),
92
98
  '--update-env',
@@ -103,6 +109,9 @@ async function startApiWithPm2(projectRoot, { wait = true } = {}) {
103
109
  return startApiDetached(projectRoot, { wait });
104
110
  }
105
111
  const pid = getPm2AppPid(projectRoot, PM2_API_APP);
112
+ if (pid) {
113
+ writePid(projectRoot, pid);
114
+ }
106
115
  console.log(t('lifecycle.apiStartedBg', { pid: pid ?? PM2_API_APP }));
107
116
  if (!wait)
108
117
  return 0;
@@ -69,6 +69,20 @@ function getPm2AppPid(projectRoot, name) {
69
69
  const app = getPm2App(projectRoot, name);
70
70
  return typeof app?.pid === 'number' ? app.pid : null;
71
71
  }
72
+ function getPm2AppLogPaths(projectRoot, name = PM2_API_APP) {
73
+ const serverLogDir = path.join(projectRoot, '.reactpress', 'logs', 'server');
74
+ const defaults = {
75
+ out: path.join(serverLogDir, 'pm2-out.log'),
76
+ err: path.join(serverLogDir, 'pm2-error.log'),
77
+ };
78
+ const app = getPm2App(projectRoot, name);
79
+ if (!app)
80
+ return defaults;
81
+ return {
82
+ out: app.pm2_env?.pm_out_log_path || defaults.out,
83
+ err: app.pm2_env?.pm_err_log_path || defaults.err,
84
+ };
85
+ }
72
86
  function stopPm2Apps(projectRoot, names) {
73
87
  const bin = resolvePm2Bin(projectRoot);
74
88
  if (!bin)
@@ -90,6 +104,7 @@ module.exports = {
90
104
  getPm2App,
91
105
  isPm2AppOnline,
92
106
  getPm2AppPid,
107
+ getPm2AppLogPaths,
93
108
  stopPm2Apps,
94
109
  };
95
110
  //# sourceMappingURL=pm2-runtime.js.map
@@ -3,35 +3,259 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  // @ts-nocheck
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
+ const { spawnSync } = require('child_process');
6
7
  const { brand, divider, gradientText, palette, sectionHeader } = require('../ui/theme');
7
8
  const { getServerDir, getPidFile } = require('./paths');
8
9
  const { readPid, isProcessRunning } = require('./process');
10
+ const { loadServerSiteUrl } = require('./http');
11
+ const { getPm2App, PM2_API_APP, isPm2AppOnline, getPm2AppLogPaths, resolvePm2Bin, } = require('./pm2-runtime');
9
12
  const { t } = require('./i18n');
10
13
  const LOG_SOURCES = ['error', 'request', 'response'];
14
+ const PROCESS_LOG_SOURCES = ['pm2-out', 'pm2-error'];
15
+ const PM2_LOG_FILES = {
16
+ 'pm2-out': 'pm2-out.log',
17
+ 'pm2-error': 'pm2-error.log',
18
+ };
19
+ function measureLogDir(logDir) {
20
+ if (!fs.existsSync(logDir))
21
+ return -1;
22
+ let total = listLogFiles(logDir, 'all').reduce((sum, file) => sum + file.size, 0);
23
+ for (const filename of Object.values(PM2_LOG_FILES)) {
24
+ const filePath = path.join(logDir, filename);
25
+ try {
26
+ if (fs.existsSync(filePath)) {
27
+ total += fs.statSync(filePath).size;
28
+ }
29
+ }
30
+ catch {
31
+ // ignore
32
+ }
33
+ }
34
+ return total;
35
+ }
36
+ function getPm2ServerLogDir(projectRoot) {
37
+ try {
38
+ const app = getPm2App(projectRoot, PM2_API_APP);
39
+ const dir = app?.pm2_env?.env?.REACTPRESS_SERVER_LOG_DIR;
40
+ if (typeof dir === 'string' && dir.trim()) {
41
+ return path.resolve(dir.trim());
42
+ }
43
+ }
44
+ catch {
45
+ // ignore
46
+ }
47
+ return null;
48
+ }
49
+ function parseUrlPort(urlString) {
50
+ try {
51
+ const url = new URL(urlString);
52
+ const port = Number(url.port);
53
+ if (port > 0)
54
+ return port;
55
+ return url.protocol === 'https:' ? 443 : 80;
56
+ }
57
+ catch {
58
+ return null;
59
+ }
60
+ }
61
+ function resolveListeningPid(port) {
62
+ if (!port || process.platform === 'win32')
63
+ return null;
64
+ const result = spawnSync('lsof', ['-nP', `-iTCP:${port}`, '-sTCP:LISTEN', '-t'], {
65
+ encoding: 'utf8',
66
+ stdio: ['ignore', 'pipe', 'ignore'],
67
+ });
68
+ if (result.status !== 0 || !result.stdout.trim())
69
+ return null;
70
+ const pids = result.stdout
71
+ .trim()
72
+ .split('\n')
73
+ .map((line) => Number.parseInt(line.trim(), 10))
74
+ .filter((pid) => Number.isFinite(pid));
75
+ return pids[0] ?? null;
76
+ }
77
+ function resolveLogDirFromPid(pid) {
78
+ if (!pid || process.platform === 'win32')
79
+ return null;
80
+ const result = spawnSync('lsof', ['-Fn', '-p', String(pid)], {
81
+ encoding: 'utf8',
82
+ stdio: ['ignore', 'pipe', 'ignore'],
83
+ });
84
+ if (result.status !== 0)
85
+ return null;
86
+ for (const line of result.stdout.split('\n')) {
87
+ if (!line.startsWith('n') || !line.includes('.log'))
88
+ continue;
89
+ const logPath = line.slice(1);
90
+ const match = logPath.match(/^(.*\/logs)\/(?:error|request|response)\//);
91
+ if (match)
92
+ return match[1];
93
+ }
94
+ return null;
95
+ }
96
+ function resolveActiveServerLogDir(projectRoot) {
97
+ const port = parseUrlPort(loadServerSiteUrl(projectRoot));
98
+ if (!port)
99
+ return null;
100
+ const pid = resolveListeningPid(port);
101
+ if (!pid)
102
+ return null;
103
+ return resolveLogDirFromPid(pid);
104
+ }
105
+ function collectLogDirCandidates(projectRoot) {
106
+ const candidates = [
107
+ path.join(projectRoot, '.reactpress', 'logs', 'server'),
108
+ path.join(getServerDir(projectRoot), 'logs'),
109
+ ];
110
+ const pm2Dir = getPm2ServerLogDir(projectRoot);
111
+ if (pm2Dir)
112
+ candidates.push(pm2Dir);
113
+ const activeDir = resolveActiveServerLogDir(projectRoot);
114
+ if (activeDir)
115
+ candidates.push(activeDir);
116
+ return [...new Set(candidates.map((dir) => path.resolve(dir)))];
117
+ }
11
118
  function getProjectServerLogDir(projectRoot) {
12
119
  const preferred = path.join(projectRoot, '.reactpress', 'logs', 'server');
13
- if (fs.existsSync(preferred)) {
14
- return preferred;
15
- }
16
120
  const bundled = path.join(getServerDir(projectRoot), 'logs');
17
- if (fs.existsSync(bundled)) {
18
- return bundled;
121
+ const pm2Dir = getPm2ServerLogDir(projectRoot);
122
+ const activeDir = resolveActiveServerLogDir(projectRoot);
123
+ const localCandidates = [preferred, bundled];
124
+ if (pm2Dir)
125
+ localCandidates.push(pm2Dir);
126
+ let bestLocal = preferred;
127
+ let bestLocalScore = -1;
128
+ for (const dir of [...new Set(localCandidates.map((d) => path.resolve(d)))]) {
129
+ const score = measureLogDir(dir);
130
+ if (score > bestLocalScore) {
131
+ bestLocalScore = score;
132
+ bestLocal = dir;
133
+ }
19
134
  }
20
- return preferred;
135
+ if (bestLocalScore > 0) {
136
+ return bestLocal;
137
+ }
138
+ if (activeDir && fs.existsSync(activeDir) && measureLogDir(activeDir) > 0) {
139
+ return activeDir;
140
+ }
141
+ return bestLocal;
21
142
  }
22
143
  function normalizeSource(source) {
23
144
  const value = String(source || 'error').toLowerCase();
24
145
  if (value === 'all')
25
146
  return 'all';
147
+ if (value === 'process' || value === 'pm2')
148
+ return 'process';
26
149
  if (LOG_SOURCES.includes(value))
27
150
  return value;
28
151
  return 'error';
29
152
  }
30
- function listLogFiles(logDir, source = 'error') {
153
+ function measureStructuredLogDir(logDir) {
154
+ if (!fs.existsSync(logDir))
155
+ return 0;
156
+ let total = 0;
157
+ for (const name of LOG_SOURCES) {
158
+ const dir = path.join(logDir, name);
159
+ if (!fs.existsSync(dir))
160
+ continue;
161
+ for (const entry of fs.readdirSync(dir)) {
162
+ if (!entry.endsWith('.log'))
163
+ continue;
164
+ try {
165
+ total += fs.statSync(path.join(dir, entry)).size;
166
+ }
167
+ catch {
168
+ // ignore
169
+ }
170
+ }
171
+ }
172
+ return total;
173
+ }
174
+ function resolveLogSource(projectRoot, sourceOption) {
175
+ if (sourceOption != null && String(sourceOption).trim() !== '') {
176
+ return normalizeSource(sourceOption);
177
+ }
178
+ const logDir = path.join(projectRoot, '.reactpress', 'logs', 'server');
179
+ if (measureStructuredLogDir(logDir) > 0) {
180
+ return 'error';
181
+ }
182
+ if (isPm2AppOnline(projectRoot, PM2_API_APP)) {
183
+ return 'process';
184
+ }
185
+ return 'error';
186
+ }
187
+ function listProcessLogFiles(projectRoot, logDir, source = 'process') {
188
+ const normalized = normalizeSource(source);
189
+ const names = normalized === 'all'
190
+ ? PROCESS_LOG_SOURCES
191
+ : normalized === 'process'
192
+ ? PROCESS_LOG_SOURCES
193
+ : PROCESS_LOG_SOURCES.includes(normalized)
194
+ ? [normalized]
195
+ : [];
196
+ if (!names.length)
197
+ return [];
198
+ const pm2Paths = getPm2AppLogPaths(projectRoot, PM2_API_APP);
199
+ const pathBySource = {
200
+ 'pm2-out': pm2Paths.out,
201
+ 'pm2-error': pm2Paths.err,
202
+ };
203
+ const files = [];
204
+ for (const name of names) {
205
+ const localPath = path.join(logDir, PM2_LOG_FILES[name]);
206
+ const candidates = [];
207
+ if (fs.existsSync(localPath)) {
208
+ candidates.push(localPath);
209
+ }
210
+ const remotePath = pathBySource[name];
211
+ if (remotePath &&
212
+ path.resolve(remotePath) !== path.resolve(localPath) &&
213
+ fs.existsSync(remotePath)) {
214
+ candidates.push(remotePath);
215
+ }
216
+ const seen = new Set();
217
+ for (const fullPath of candidates) {
218
+ const resolved = path.resolve(fullPath);
219
+ if (seen.has(resolved))
220
+ continue;
221
+ seen.add(resolved);
222
+ let stat;
223
+ try {
224
+ stat = fs.statSync(resolved);
225
+ }
226
+ catch {
227
+ continue;
228
+ }
229
+ if (!stat.isFile())
230
+ continue;
231
+ files.push({
232
+ source: name,
233
+ path: resolved,
234
+ name: path.basename(resolved),
235
+ mtimeMs: stat.mtimeMs,
236
+ size: stat.size,
237
+ });
238
+ break;
239
+ }
240
+ }
241
+ files.sort((a, b) => b.mtimeMs - a.mtimeMs);
242
+ return files;
243
+ }
244
+ function listLogFiles(logDir, source = 'error', projectRoot = null) {
31
245
  const normalized = normalizeSource(source);
32
- const sources = normalized === 'all' ? LOG_SOURCES : [normalized];
246
+ if (normalized === 'process' || PROCESS_LOG_SOURCES.includes(normalized)) {
247
+ return listProcessLogFiles(projectRoot || '', logDir, normalized);
248
+ }
249
+ const sources = normalized === 'all'
250
+ ? [...PROCESS_LOG_SOURCES, ...LOG_SOURCES]
251
+ : [normalized];
33
252
  const files = [];
253
+ if (normalized === 'all' && projectRoot) {
254
+ files.push(...listProcessLogFiles(projectRoot, logDir, 'process'));
255
+ }
34
256
  for (const name of sources) {
257
+ if (PROCESS_LOG_SOURCES.includes(name))
258
+ continue;
35
259
  const dir = path.join(logDir, name);
36
260
  if (!fs.existsSync(dir))
37
261
  continue;
@@ -104,9 +328,20 @@ function formatTimestamp(ms) {
104
328
  return new Date(ms).toISOString().replace('T', ' ').slice(0, 19);
105
329
  }
106
330
  function printProcessHint(projectRoot) {
331
+ if (isPm2AppOnline(projectRoot, PM2_API_APP)) {
332
+ const pid = getPm2App(projectRoot, PM2_API_APP)?.pid;
333
+ console.log(` ${brand.dim(t('doctor.logs.pm2Supervised', { pid: pid ?? PM2_API_APP }))}`);
334
+ return;
335
+ }
107
336
  const pid = readPid(projectRoot);
108
337
  const pidFile = getPidFile(projectRoot);
109
338
  if (!pid) {
339
+ const port = parseUrlPort(loadServerSiteUrl(projectRoot));
340
+ const activePid = port ? resolveListeningPid(port) : null;
341
+ if (activePid && isProcessRunning(activePid)) {
342
+ console.log(` ${brand.dim(t('doctor.logs.activePid', { pid: activePid, port }))}`);
343
+ return;
344
+ }
110
345
  console.log(` ${brand.dim(t('doctor.logs.noPid', { path: pidFile }))}`);
111
346
  return;
112
347
  }
@@ -115,11 +350,69 @@ function printProcessHint(projectRoot) {
115
350
  ? t('doctor.logs.pidRunning', { pid, path: pidFile })
116
351
  : t('doctor.logs.pidStale', { pid, path: pidFile }))}`);
117
352
  }
353
+ function pickPrimaryLogFile(files, source) {
354
+ if (!files.length)
355
+ return null;
356
+ if (source === 'process') {
357
+ return (files.find((file) => file.source === 'pm2-error') ||
358
+ files.find((file) => file.source === 'pm2-out') ||
359
+ files[0]);
360
+ }
361
+ const grouped = new Map();
362
+ for (const file of files) {
363
+ if (!grouped.has(file.source))
364
+ grouped.set(file.source, file);
365
+ }
366
+ const order = source === 'all' ? [...PROCESS_LOG_SOURCES, ...LOG_SOURCES] : [source];
367
+ for (const name of order) {
368
+ const file = grouped.get(name);
369
+ if (file)
370
+ return file;
371
+ }
372
+ return files[0];
373
+ }
374
+ async function runFollowLogs(projectRoot, { tail, source, logDir, files }) {
375
+ const usePm2Stream = source === 'process' && isPm2AppOnline(projectRoot, PM2_API_APP);
376
+ if (usePm2Stream) {
377
+ const bin = resolvePm2Bin(projectRoot);
378
+ if (!bin) {
379
+ console.error(` ${brand.warn(t('doctor.logs.pm2Unavailable'))}`);
380
+ return 1;
381
+ }
382
+ const { spawn } = require('child_process');
383
+ await new Promise((resolve) => {
384
+ const child = spawn(bin, ['logs', PM2_API_APP, '--lines', String(tail)], {
385
+ stdio: 'inherit',
386
+ });
387
+ child.on('exit', (code) => resolve(code ?? 0));
388
+ });
389
+ return 0;
390
+ }
391
+ const primary = pickPrimaryLogFile(files, source);
392
+ if (!primary) {
393
+ console.log(` ${brand.warn(t('doctor.logs.empty'))}`);
394
+ console.log('');
395
+ return 1;
396
+ }
397
+ if (process.platform === 'win32') {
398
+ console.log(` ${brand.dim(t('doctor.logs.followWindows', { path: primary.path }))}`);
399
+ console.log('');
400
+ return 0;
401
+ }
402
+ const { spawn } = require('child_process');
403
+ await new Promise((resolve) => {
404
+ const child = spawn('tail', ['-n', String(tail), '-f', primary.path], {
405
+ stdio: 'inherit',
406
+ });
407
+ child.on('exit', (code) => resolve(code ?? 0));
408
+ });
409
+ return 0;
410
+ }
118
411
  async function runDoctorLogs(projectRoot, options = {}) {
119
412
  const tail = Math.max(1, Math.min(parseInt(String(options.tail ?? '50'), 10) || 50, 5000));
120
- const source = normalizeSource(options.source);
413
+ const source = resolveLogSource(projectRoot, options.source);
121
414
  const logDir = getProjectServerLogDir(projectRoot);
122
- const files = listLogFiles(logDir, source);
415
+ const files = listLogFiles(logDir, source, projectRoot);
123
416
  const w = 56;
124
417
  console.log('');
125
418
  console.log(` ${gradientText(t('logs.title'), [palette.primary, palette.accent], { bold: true })} ${brand.dim(t('logs.subtitle'))}`);
@@ -127,6 +420,9 @@ async function runDoctorLogs(projectRoot, options = {}) {
127
420
  console.log(` ${brand.dim(t('doctor.logs.dir', { path: logDir }))}`);
128
421
  printProcessHint(projectRoot);
129
422
  console.log(` ${divider(w)}`);
423
+ if (options.follow) {
424
+ return runFollowLogs(projectRoot, { tail, source, logDir, files });
425
+ }
130
426
  if (options.list) {
131
427
  if (!files.length) {
132
428
  console.log(` ${brand.warn(t('doctor.logs.empty'))}`);
@@ -153,8 +449,13 @@ async function runDoctorLogs(projectRoot, options = {}) {
153
449
  if (!grouped.has(file.source))
154
450
  grouped.set(file.source, file);
155
451
  }
452
+ const displayOrder = source === 'all'
453
+ ? [...PROCESS_LOG_SOURCES, ...LOG_SOURCES]
454
+ : source === 'process'
455
+ ? PROCESS_LOG_SOURCES
456
+ : [source];
156
457
  let printed = 0;
157
- for (const name of source === 'all' ? LOG_SOURCES : [source]) {
458
+ for (const name of displayOrder) {
158
459
  const file = grouped.get(name);
159
460
  if (!file)
160
461
  continue;
@@ -171,7 +472,7 @@ async function runDoctorLogs(projectRoot, options = {}) {
171
472
  continue;
172
473
  }
173
474
  for (const line of lines) {
174
- const isError = /\[ERROR\]/i.test(line) || name === 'error';
475
+ const isError = /\[ERROR\]/i.test(line) || name === 'error' || name === 'pm2-error';
175
476
  console.log(` ${isError ? brand.warn(line) : brand.dim(line)}`);
176
477
  }
177
478
  printed += lines.length;
@@ -188,7 +489,12 @@ async function runDoctorLogs(projectRoot, options = {}) {
188
489
  }
189
490
  module.exports = {
190
491
  LOG_SOURCES,
492
+ PROCESS_LOG_SOURCES,
191
493
  getProjectServerLogDir,
494
+ collectLogDirCandidates,
495
+ measureLogDir,
496
+ resolveActiveServerLogDir,
497
+ resolveLogSource,
192
498
  listLogFiles,
193
499
  readTailLines,
194
500
  filterLines,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fecommunity/reactpress",
3
- "version": "4.0.0-beta.15",
3
+ "version": "4.0.0-beta.18",
4
4
  "description": "ReactPress CLI — publishing platform for modern developers. CMS, Admin, API, and Next.js themes in one command. Self-hosted, MIT. `npx @fecommunity/reactpress init`",
5
5
  "author": "fecommunity",
6
6
  "license": "MIT",
@@ -45,6 +45,7 @@
45
45
  "server",
46
46
  "toolkit",
47
47
  "admin-dist",
48
+ "plugins",
48
49
  "templates",
49
50
  "scripts/install-bundled-runtime.mjs",
50
51
  "README.md",