@fecommunity/reactpress 4.0.0-beta.17 → 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.
@@ -36,6 +36,10 @@ function runStartCommand() {
36
36
  return ['npm', ['run', 'start']];
37
37
  }
38
38
  function ensureBuilt() {
39
+ const { ensureBundledPlugins } = require('../core/services/local-site');
40
+ if (ensureBundledPlugins(originalCwd)) {
41
+ console.log('[ReactPress Client] Seeded bundled plugins into project');
42
+ }
39
43
  const adminSynced = ensureAdminStaticForTheme(originalCwd, clientDir);
40
44
  const apiProxyPatched = patchThemeApiProxyRoute(clientDir);
41
45
  const { activeTheme } = readActiveThemeManifest(originalCwd);
@@ -39,9 +39,10 @@ const LEGACY_COMMANDS = new Set([
39
39
  function attachLogsOptions(cmd) {
40
40
  return cmd
41
41
  .option('--tail <n>', t('cli.logs.tailOption'), '50')
42
- .option('--source <name>', t('cli.logs.sourceOption'), 'error')
42
+ .option('--source <name>', t('cli.logs.sourceOption'))
43
43
  .option('--grep <pattern>', t('cli.logs.grepOption'))
44
- .option('--list', t('cli.logs.listOption'));
44
+ .option('--list', t('cli.logs.listOption'))
45
+ .option('-f, --follow', t('cli.logs.followOption'));
45
46
  }
46
47
  function createLogsAction() {
47
48
  return async (directory, options) => {
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.resolveBundledAdminDistDir = resolveBundledAdminDistDir;
7
7
  exports.resolveWebPackageRoot = resolveWebPackageRoot;
8
+ exports.adminSourceIsNewerThanTheme = adminSourceIsNewerThanTheme;
8
9
  exports.ensureAdminRewritesInNextConfig = ensureAdminRewritesInNextConfig;
9
10
  exports.ensureAdminStaticForTheme = ensureAdminStaticForTheme;
10
11
  exports.shouldRebuildThemeAfterAdminSync = shouldRebuildThemeAfterAdminSync;
@@ -26,6 +27,45 @@ function resolveWebPackageRoot(projectRoot) {
26
27
  }
27
28
  return null;
28
29
  }
30
+ function readAdminMainAssetId(indexHtmlPath) {
31
+ try {
32
+ const html = fs_extra_1.default.readFileSync(indexHtmlPath, 'utf8');
33
+ return html.match(/assets\/(index-[^"]+\.js)/)?.[1] ?? null;
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ }
39
+ function resolveAdminDistSource(projectRoot) {
40
+ const bundled = resolveBundledAdminDistDir();
41
+ const webRoot = resolveWebPackageRoot(projectRoot);
42
+ const webDist = webRoot ? node_path_1.default.join(webRoot, 'dist') : null;
43
+ const webIndex = webDist ? node_path_1.default.join(webDist, 'index.html') : null;
44
+ if (webIndex && fs_extra_1.default.existsSync(webIndex)) {
45
+ if (!bundled)
46
+ return webDist;
47
+ const bundledIndex = node_path_1.default.join(bundled, 'index.html');
48
+ const webMtime = fs_extra_1.default.statSync(webIndex).mtimeMs;
49
+ const bundledMtime = fs_extra_1.default.statSync(bundledIndex).mtimeMs;
50
+ return webMtime >= bundledMtime ? webDist : bundled;
51
+ }
52
+ return bundled;
53
+ }
54
+ /** True when CLI/web admin-dist is newer than the theme's copied public/admin bundle. */
55
+ function adminSourceIsNewerThanTheme(projectRoot, themeDir) {
56
+ const source = resolveAdminDistSource(projectRoot);
57
+ const sourceIndex = source ? node_path_1.default.join(source, 'index.html') : null;
58
+ const themeAdminIndex = node_path_1.default.join(themeDir, 'public', 'admin', 'index.html');
59
+ if (!sourceIndex || !fs_extra_1.default.existsSync(sourceIndex))
60
+ return false;
61
+ if (!fs_extra_1.default.existsSync(themeAdminIndex))
62
+ return true;
63
+ const sourceId = readAdminMainAssetId(sourceIndex);
64
+ const themeId = readAdminMainAssetId(themeAdminIndex);
65
+ if (sourceId && themeId)
66
+ return sourceId !== themeId;
67
+ return fs_extra_1.default.statSync(sourceIndex).mtimeMs > fs_extra_1.default.statSync(themeAdminIndex).mtimeMs;
68
+ }
29
69
  function syncDirToPublicAdmin(sourceDist, themeDir) {
30
70
  const target = node_path_1.default.join(themeDir, 'public', 'admin');
31
71
  fs_extra_1.default.removeSync(target);
@@ -78,40 +118,53 @@ function ensureAdminRewritesInNextConfig(themeDir) {
78
118
  function finalizeAdminThemeSetup(themeDir) {
79
119
  return ensureAdminRewritesInNextConfig(themeDir);
80
120
  }
121
+ function syncAdminFromWeb(projectRoot, themeDir) {
122
+ const webRoot = resolveWebPackageRoot(projectRoot);
123
+ if (!webRoot)
124
+ return false;
125
+ const webDist = node_path_1.default.join(webRoot, 'dist', 'index.html');
126
+ if (!fs_extra_1.default.existsSync(webDist)) {
127
+ console.log('[ReactPress Client] Building admin SPA (web/)…');
128
+ const buildWeb = (0, node_child_process_1.spawnSync)('pnpm', ['run', 'build'], { cwd: webRoot, stdio: 'inherit' });
129
+ if (buildWeb.status !== 0)
130
+ process.exit(buildWeb.status || 1);
131
+ }
132
+ const syncScript = node_path_1.default.join(webRoot, 'bin', 'reactpress-web.js');
133
+ const publicAdmin = node_path_1.default.join(themeDir, 'public', 'admin');
134
+ console.log('[ReactPress Client] Syncing admin static → theme public/admin…');
135
+ const sync = (0, node_child_process_1.spawnSync)(process.execPath, [syncScript, 'sync-public', publicAdmin], {
136
+ stdio: 'inherit',
137
+ cwd: webRoot,
138
+ });
139
+ if (sync.status !== 0)
140
+ process.exit(sync.status || 1);
141
+ return finalizeAdminThemeSetup(themeDir) || true;
142
+ }
143
+ function syncAdminFromSource(sourceDist, themeDir) {
144
+ console.log('[ReactPress Client] Syncing bundled admin static → theme public/admin…');
145
+ syncDirToPublicAdmin(sourceDist, themeDir);
146
+ return finalizeAdminThemeSetup(themeDir) || true;
147
+ }
81
148
  /** Returns true when admin static or rewrites were newly applied (theme rebuild recommended). */
82
149
  function ensureAdminStaticForTheme(projectRoot, themeDir) {
83
150
  const adminIndex = node_path_1.default.join(themeDir, 'public', 'admin', 'index.html');
84
- if (fs_extra_1.default.existsSync(adminIndex)) {
151
+ const needsResync = !fs_extra_1.default.existsSync(adminIndex) || adminSourceIsNewerThanTheme(projectRoot, themeDir);
152
+ if (!needsResync) {
85
153
  return finalizeAdminThemeSetup(themeDir);
86
154
  }
87
- const webRoot = resolveWebPackageRoot(projectRoot);
88
- if (webRoot) {
89
- const webDist = node_path_1.default.join(webRoot, 'dist', 'index.html');
90
- if (!fs_extra_1.default.existsSync(webDist)) {
91
- console.log('[ReactPress Client] Building admin SPA (web/)…');
92
- const buildWeb = (0, node_child_process_1.spawnSync)('pnpm', ['run', 'build'], { cwd: webRoot, stdio: 'inherit' });
93
- if (buildWeb.status !== 0)
94
- process.exit(buildWeb.status || 1);
155
+ const source = resolveAdminDistSource(projectRoot);
156
+ if (source && fs_extra_1.default.existsSync(node_path_1.default.join(source, 'index.html'))) {
157
+ const webRoot = resolveWebPackageRoot(projectRoot);
158
+ if (webRoot && node_path_1.default.resolve(source) === node_path_1.default.resolve(node_path_1.default.join(webRoot, 'dist'))) {
159
+ return syncAdminFromWeb(projectRoot, themeDir);
95
160
  }
96
- const syncScript = node_path_1.default.join(webRoot, 'bin', 'reactpress-web.js');
97
- const publicAdmin = node_path_1.default.join(themeDir, 'public', 'admin');
98
- console.log('[ReactPress Client] Syncing admin static → theme public/admin…');
99
- const sync = (0, node_child_process_1.spawnSync)(process.execPath, [syncScript, 'sync-public', publicAdmin], {
100
- stdio: 'inherit',
101
- cwd: webRoot,
102
- });
103
- if (sync.status !== 0)
104
- process.exit(sync.status || 1);
105
- return finalizeAdminThemeSetup(themeDir) || true;
161
+ return syncAdminFromSource(source, themeDir);
106
162
  }
107
- const bundledDist = resolveBundledAdminDistDir();
108
- if (!bundledDist) {
109
- console.warn('[ReactPress Client] Admin static missing and bundled admin-dist not found');
110
- return false;
163
+ if (syncAdminFromWeb(projectRoot, themeDir)) {
164
+ return true;
111
165
  }
112
- console.log('[ReactPress Client] Syncing bundled admin static → theme public/admin…');
113
- syncDirToPublicAdmin(bundledDist, themeDir);
114
- return finalizeAdminThemeSetup(themeDir) || true;
166
+ console.warn('[ReactPress Client] Admin static missing and bundled admin-dist not found');
167
+ return false;
115
168
  }
116
169
  function shouldRebuildThemeAfterAdminSync(themeDir) {
117
170
  const adminIndex = node_path_1.default.join(themeDir, 'public', 'admin', 'index.html');
@@ -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',
@@ -433,15 +434,19 @@ const STRINGS = {
433
434
  'doctor.logs.subtitle': 'API log viewer',
434
435
  'doctor.logs.dir': 'Log dir {path}',
435
436
  'doctor.logs.empty': 'No API log files found yet.',
436
- '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)',
437
438
  'doctor.logs.listHeader': 'Available log files',
438
439
  'doctor.logs.fileHeader': '{file} (showing {count} line(s))',
439
440
  'doctor.logs.noMatchingLines': 'No matching log lines.',
440
441
  'doctor.logs.badPattern': 'Invalid grep pattern: {pattern}',
441
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}',
442
447
  'doctor.logs.pidRunning': 'API pid {pid} (recorded in {path})',
443
448
  'doctor.logs.pidStale': 'Stale pid {pid} in {path} — process not running',
444
- '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)',
445
450
  'status.title': 'ReactPress project status',
446
451
  'status.dir': 'Project {path}',
447
452
  'status.apiSource': 'API source {source}',
@@ -752,12 +757,13 @@ const STRINGS = {
752
757
  'cli.doctor.logs.sourceOption': '日志类型:error、request、response 或 all',
753
758
  'cli.doctor.logs.grepOption': '用正则过滤日志行',
754
759
  'cli.doctor.logs.listOption': '仅列出可用日志文件',
755
- 'cli.logs.description': '查看 API 日志(error / request / response)',
760
+ 'cli.logs.description': '查看 API 日志(PM2 进程输出或结构化 error/request/response)',
756
761
  'cli.logs.descriptionAlias': '查看 API 日志(同 reactpress logs)',
757
762
  'cli.logs.tailOption': '每个日志文件显示的行数',
758
- 'cli.logs.sourceOption': '日志类型:error、request、response 或 all',
763
+ 'cli.logs.sourceOption': '日志类型:process(PM2)、error、request、response 或 all',
759
764
  'cli.logs.grepOption': '用正则过滤日志行',
760
765
  'cli.logs.listOption': '仅列出可用日志文件',
766
+ 'cli.logs.followOption': '持续输出日志(PM2 托管时用 pm2 logs,否则 tail -f)',
761
767
  'cli.stop.description': '停止 init 启动的 API 与站点服务',
762
768
  'cli.db.description': '数据库运维',
763
769
  'cli.db.backup': '使用 mysqldump 备份当前项目数据库',
@@ -1108,15 +1114,19 @@ const STRINGS = {
1108
1114
  'doctor.logs.subtitle': 'API 日志查看',
1109
1115
  'doctor.logs.dir': '日志目录 {path}',
1110
1116
  'doctor.logs.empty': '尚未找到 API 日志文件。',
1111
- '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)',
1112
1118
  'doctor.logs.listHeader': '可用日志文件',
1113
1119
  'doctor.logs.fileHeader': '{file}(显示 {count} 行)',
1114
1120
  'doctor.logs.noMatchingLines': '没有匹配的日志行。',
1115
1121
  'doctor.logs.badPattern': 'grep 正则无效:{pattern}',
1116
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}',
1117
1127
  'doctor.logs.pidRunning': 'API 进程 pid {pid}(记录在 {path})',
1118
1128
  'doctor.logs.pidStale': 'pid 文件 {path} 中的 {pid} 已失效(进程未运行)',
1119
- 'doctor.logs.moreHint': '可用 --tail、--source、--grep、--list 进一步筛选(见 reactpress logs --help)',
1129
+ 'doctor.logs.moreHint': '可用 --tail、--source、--grep、--follow、--list 进一步筛选(见 reactpress logs --help)',
1120
1130
  'status.title': 'ReactPress 项目状态',
1121
1131
  'status.dir': '项目目录 {path}',
1122
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.17",
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",
@@ -196,6 +196,23 @@ function startWithNode() {
196
196
  console.log(`[ReactPress Server] Using existing REACTPRESS_ORIGINAL_CWD: ${process.env.REACTPRESS_ORIGINAL_CWD}`);
197
197
  }
198
198
 
199
+ const siteRoot = process.env.REACTPRESS_ORIGINAL_CWD;
200
+ for (const localSiteModule of [
201
+ path.join(serverDir, '..', 'cli', 'out', 'core', 'services', 'local-site.js'),
202
+ path.join(serverDir, '..', 'out', 'core', 'services', 'local-site.js'),
203
+ ]) {
204
+ if (!siteRoot || !fs.existsSync(localSiteModule)) continue;
205
+ try {
206
+ const { ensureBundledPlugins } = require(localSiteModule);
207
+ if (ensureBundledPlugins(siteRoot)) {
208
+ console.log('[ReactPress Server] Seeded bundled plugins into project');
209
+ }
210
+ } catch {
211
+ // ignore — plugins seeding is best-effort
212
+ }
213
+ break;
214
+ }
215
+
199
216
  // Change to the server directory
200
217
  process.chdir(serverDir);
201
218
 
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@fecommunity/reactpress-server",
3
- "version": "4.0.0-beta.16",
3
+ "version": "4.0.0-beta.18",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@fecommunity/reactpress-server",
9
- "version": "4.0.0-beta.16",
9
+ "version": "4.0.0-beta.18",
10
10
  "deprecated": "Bundled in @fecommunity/reactpress CLI from v3.1. Run `npx @fecommunity/reactpress init` for new projects.",
11
11
  "license": "MIT",
12
12
  "dependencies": {
@@ -91,7 +91,7 @@
91
91
  },
92
92
  "../toolkit": {
93
93
  "name": "@fecommunity/reactpress-toolkit",
94
- "version": "4.0.0-beta.16",
94
+ "version": "4.0.0-beta.18",
95
95
  "dependencies": {
96
96
  "ajv": "^8.17.1",
97
97
  "axios": "^1.12.2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fecommunity/reactpress-server",
3
- "version": "4.0.0-beta.17",
3
+ "version": "4.0.0-beta.18",
4
4
  "deprecated": "Bundled in @fecommunity/reactpress CLI from v3.1. Run `npx @fecommunity/reactpress init` for new projects.",
5
5
  "description": "ReactPress NestJS REST API (deprecated). Bundled in @fecommunity/reactpress CLI from v3.1. Headless CMS for articles, media, plugins, and themes",
6
6
  "author": "fecommunity",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fecommunity/reactpress-toolkit",
3
- "version": "4.0.0-beta.17",
3
+ "version": "4.0.0-beta.18",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "exports": {