@lark-apaas/fullstack-cli 1.1.28-alpha.1 → 1.1.28-alpha.11
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/dist/index.js +112 -18
- package/package.json +1 -1
- package/templates/.spark_project +16 -0
- package/templates/scripts/build.sh +15 -28
- package/templates/scripts/dev.js +275 -0
- package/templates/scripts/dev.sh +1 -244
package/dist/index.js
CHANGED
|
@@ -2377,12 +2377,15 @@ var syncConfig = {
|
|
|
2377
2377
|
type: "directory",
|
|
2378
2378
|
overwrite: true
|
|
2379
2379
|
},
|
|
2380
|
-
// 2.
|
|
2380
|
+
// 2. 智能合并 nest-cli.json 配置(保留用户自定义的 assets、plugins 等)
|
|
2381
2381
|
{
|
|
2382
2382
|
from: "templates/nest-cli.json",
|
|
2383
2383
|
to: "nest-cli.json",
|
|
2384
|
-
type: "
|
|
2385
|
-
|
|
2384
|
+
type: "merge-json",
|
|
2385
|
+
arrayMerge: {
|
|
2386
|
+
"compilerOptions.assets": { key: "include" },
|
|
2387
|
+
"compilerOptions.plugins": { key: "name" }
|
|
2388
|
+
}
|
|
2386
2389
|
},
|
|
2387
2390
|
// // 2. 追加内容到 .gitignore
|
|
2388
2391
|
// {
|
|
@@ -2427,6 +2430,13 @@ var syncConfig = {
|
|
|
2427
2430
|
type: "add-line",
|
|
2428
2431
|
to: ".gitignore",
|
|
2429
2432
|
line: ".agent/"
|
|
2433
|
+
},
|
|
2434
|
+
// 8. 同步 .spark_project 配置文件(总是覆盖)
|
|
2435
|
+
{
|
|
2436
|
+
from: "templates/.spark_project",
|
|
2437
|
+
to: ".spark_project",
|
|
2438
|
+
type: "file",
|
|
2439
|
+
overwrite: true
|
|
2430
2440
|
}
|
|
2431
2441
|
],
|
|
2432
2442
|
// 文件权限设置
|
|
@@ -2468,6 +2478,52 @@ function removeLineFromFile(filePath, pattern) {
|
|
|
2468
2478
|
return true;
|
|
2469
2479
|
}
|
|
2470
2480
|
|
|
2481
|
+
// src/utils/merge-json.ts
|
|
2482
|
+
function isPlainObject(value) {
|
|
2483
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2484
|
+
}
|
|
2485
|
+
function mergeArrayByKey(userArr, templateArr, key) {
|
|
2486
|
+
const result = [...userArr];
|
|
2487
|
+
for (const templateItem of templateArr) {
|
|
2488
|
+
if (!isPlainObject(templateItem)) continue;
|
|
2489
|
+
const templateKey = templateItem[key];
|
|
2490
|
+
const existingIndex = result.findIndex(
|
|
2491
|
+
(item) => isPlainObject(item) && item[key] === templateKey
|
|
2492
|
+
);
|
|
2493
|
+
if (existingIndex >= 0) {
|
|
2494
|
+
result[existingIndex] = templateItem;
|
|
2495
|
+
} else {
|
|
2496
|
+
result.push(templateItem);
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
return result;
|
|
2500
|
+
}
|
|
2501
|
+
function deepMergeJson(user, template, arrayMerge = {}, currentPath = "") {
|
|
2502
|
+
const result = { ...user };
|
|
2503
|
+
for (const key of Object.keys(template)) {
|
|
2504
|
+
const fullPath = currentPath ? `${currentPath}.${key}` : key;
|
|
2505
|
+
const templateValue = template[key];
|
|
2506
|
+
const userValue = user[key];
|
|
2507
|
+
if (Array.isArray(templateValue)) {
|
|
2508
|
+
const mergeConfig = arrayMerge[fullPath];
|
|
2509
|
+
if (mergeConfig && Array.isArray(userValue)) {
|
|
2510
|
+
result[key] = mergeArrayByKey(userValue, templateValue, mergeConfig.key);
|
|
2511
|
+
} else {
|
|
2512
|
+
result[key] = templateValue;
|
|
2513
|
+
}
|
|
2514
|
+
} else if (isPlainObject(templateValue)) {
|
|
2515
|
+
if (isPlainObject(userValue)) {
|
|
2516
|
+
result[key] = deepMergeJson(userValue, templateValue, arrayMerge, fullPath);
|
|
2517
|
+
} else {
|
|
2518
|
+
result[key] = templateValue;
|
|
2519
|
+
}
|
|
2520
|
+
} else {
|
|
2521
|
+
result[key] = templateValue;
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
return result;
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2471
2527
|
// src/commands/sync/run.handler.ts
|
|
2472
2528
|
async function run2(options) {
|
|
2473
2529
|
const userProjectRoot = process.env.INIT_CWD || process.cwd();
|
|
@@ -2530,6 +2586,12 @@ async function syncRule(rule, pluginRoot, userProjectRoot) {
|
|
|
2530
2586
|
addLineToFile(destPath2, rule.line);
|
|
2531
2587
|
return;
|
|
2532
2588
|
}
|
|
2589
|
+
if (rule.type === "merge-json") {
|
|
2590
|
+
const srcPath2 = path4.join(pluginRoot, rule.from);
|
|
2591
|
+
const destPath2 = path4.join(userProjectRoot, rule.to);
|
|
2592
|
+
mergeJsonFile(srcPath2, destPath2, rule.arrayMerge);
|
|
2593
|
+
return;
|
|
2594
|
+
}
|
|
2533
2595
|
if (!("from" in rule)) {
|
|
2534
2596
|
return;
|
|
2535
2597
|
}
|
|
@@ -2672,6 +2734,33 @@ function addLineToFile(filePath, line) {
|
|
|
2672
2734
|
fs6.appendFileSync(filePath, appendContent);
|
|
2673
2735
|
console.log(`[fullstack-cli] \u2713 ${fileName} (added: ${line})`);
|
|
2674
2736
|
}
|
|
2737
|
+
function mergeJsonFile(src, dest, arrayMerge) {
|
|
2738
|
+
const fileName = path4.basename(dest);
|
|
2739
|
+
if (!fs6.existsSync(src)) {
|
|
2740
|
+
console.warn(`[fullstack-cli] Source not found: ${src}`);
|
|
2741
|
+
return;
|
|
2742
|
+
}
|
|
2743
|
+
const templateContent = JSON.parse(fs6.readFileSync(src, "utf-8"));
|
|
2744
|
+
if (!fs6.existsSync(dest)) {
|
|
2745
|
+
const destDir = path4.dirname(dest);
|
|
2746
|
+
if (!fs6.existsSync(destDir)) {
|
|
2747
|
+
fs6.mkdirSync(destDir, { recursive: true });
|
|
2748
|
+
}
|
|
2749
|
+
fs6.writeFileSync(dest, JSON.stringify(templateContent, null, 2) + "\n");
|
|
2750
|
+
console.log(`[fullstack-cli] \u2713 ${fileName} (created)`);
|
|
2751
|
+
return;
|
|
2752
|
+
}
|
|
2753
|
+
const userContent = JSON.parse(fs6.readFileSync(dest, "utf-8"));
|
|
2754
|
+
const merged = deepMergeJson(userContent, templateContent, arrayMerge ?? {});
|
|
2755
|
+
const userStr = JSON.stringify(userContent, null, 2);
|
|
2756
|
+
const mergedStr = JSON.stringify(merged, null, 2);
|
|
2757
|
+
if (userStr === mergedStr) {
|
|
2758
|
+
console.log(`[fullstack-cli] \u25CB ${fileName} (already up to date)`);
|
|
2759
|
+
return;
|
|
2760
|
+
}
|
|
2761
|
+
fs6.writeFileSync(dest, mergedStr + "\n");
|
|
2762
|
+
console.log(`[fullstack-cli] \u2713 ${fileName} (merged)`);
|
|
2763
|
+
}
|
|
2675
2764
|
|
|
2676
2765
|
// src/commands/sync/index.ts
|
|
2677
2766
|
var syncCommand = {
|
|
@@ -2928,7 +3017,7 @@ function getUpgradeFilesToStage(disableGenOpenapi = true) {
|
|
|
2928
3017
|
const syncConfig2 = genSyncConfig({ disableGenOpenapi });
|
|
2929
3018
|
const filesToStage = /* @__PURE__ */ new Set();
|
|
2930
3019
|
syncConfig2.sync.forEach((rule) => {
|
|
2931
|
-
if (rule.type === "file" || rule.type === "directory") {
|
|
3020
|
+
if (rule.type === "file" || rule.type === "directory" || rule.type === "merge-json") {
|
|
2932
3021
|
filesToStage.add(rule.to);
|
|
2933
3022
|
} else if (rule.type === "remove-line" || rule.type === "add-line") {
|
|
2934
3023
|
filesToStage.add(rule.to);
|
|
@@ -7253,7 +7342,7 @@ function camelToKebab(str) {
|
|
|
7253
7342
|
// src/commands/build/upload-static.handler.ts
|
|
7254
7343
|
import * as fs25 from "fs";
|
|
7255
7344
|
import * as path21 from "path";
|
|
7256
|
-
import {
|
|
7345
|
+
import { execFileSync } from "child_process";
|
|
7257
7346
|
function readCredentialsFromEnv() {
|
|
7258
7347
|
const uploadPrefix = process.env.STATIC_UPLOAD_PREFIX;
|
|
7259
7348
|
const uploadID = process.env.STATIC_UPLOAD_ID;
|
|
@@ -7350,14 +7439,16 @@ async function fetchPreUpload(appId, bucketId) {
|
|
|
7350
7439
|
}
|
|
7351
7440
|
function configureTosutil(tosutilPath, config) {
|
|
7352
7441
|
const { endpoint, region, accessKeyID, secretAccessKey, sessionToken } = config;
|
|
7353
|
-
|
|
7354
|
-
|
|
7442
|
+
execFileSync(
|
|
7443
|
+
tosutilPath,
|
|
7444
|
+
["config", "-e", endpoint, "-i", accessKeyID, "-k", secretAccessKey, "-t", sessionToken, "-re", region],
|
|
7355
7445
|
{ stdio: "pipe" }
|
|
7356
7446
|
);
|
|
7357
7447
|
}
|
|
7358
7448
|
function uploadToTos(tosutilPath, sourceDir, destUrl) {
|
|
7359
|
-
|
|
7360
|
-
|
|
7449
|
+
execFileSync(
|
|
7450
|
+
tosutilPath,
|
|
7451
|
+
["cp", sourceDir, destUrl, "-r", "-flat", "-j", "5", "-p", "3", "-ps", "10485760", "-f"],
|
|
7361
7452
|
{ stdio: "inherit" }
|
|
7362
7453
|
);
|
|
7363
7454
|
}
|
|
@@ -7374,6 +7465,9 @@ function isDirEmpty(dirPath) {
|
|
|
7374
7465
|
|
|
7375
7466
|
// src/commands/build/pre-upload-static.handler.ts
|
|
7376
7467
|
var LOG_PREFIX2 = "[pre-upload-static]";
|
|
7468
|
+
function shellEscape(value) {
|
|
7469
|
+
return value.replace(/["\\`$]/g, "\\$&");
|
|
7470
|
+
}
|
|
7377
7471
|
async function preUploadStatic(options) {
|
|
7378
7472
|
try {
|
|
7379
7473
|
const { appId } = options;
|
|
@@ -7386,8 +7480,8 @@ async function preUploadStatic(options) {
|
|
|
7386
7480
|
console.error(`${LOG_PREFIX2} preUploadStatic \u8FD4\u56DE\u5F02\u5E38, status_code: ${response.status_code}`);
|
|
7387
7481
|
return;
|
|
7388
7482
|
}
|
|
7389
|
-
const {
|
|
7390
|
-
if (!
|
|
7483
|
+
const { downloadURLPrefix, uploadPrefix, uploadID, uploadCredential } = response.data || {};
|
|
7484
|
+
if (!downloadURLPrefix || !uploadPrefix || !uploadID) {
|
|
7391
7485
|
console.error(`${LOG_PREFIX2} preUploadStatic \u8FD4\u56DE\u6570\u636E\u4E0D\u5B8C\u6574`);
|
|
7392
7486
|
return;
|
|
7393
7487
|
}
|
|
@@ -7395,13 +7489,13 @@ async function preUploadStatic(options) {
|
|
|
7395
7489
|
console.error(`${LOG_PREFIX2} preUploadStatic \u8FD4\u56DE\u7684\u51ED\u8BC1\u5B57\u6BB5\u4E0D\u5B8C\u6574`);
|
|
7396
7490
|
return;
|
|
7397
7491
|
}
|
|
7398
|
-
console.log(`export STATIC_ASSETS_BASE_URL="${
|
|
7399
|
-
console.log(`export STATIC_UPLOAD_PREFIX="${uploadPrefix}"`);
|
|
7400
|
-
console.log(`export STATIC_UPLOAD_ID="${uploadID}"`);
|
|
7401
|
-
console.log(`export STATIC_UPLOAD_AK="${uploadCredential.AccessKeyID}"`);
|
|
7402
|
-
console.log(`export STATIC_UPLOAD_SK="${uploadCredential.SecretAccessKey}"`);
|
|
7403
|
-
console.log(`export STATIC_UPLOAD_TOKEN="${uploadCredential.SessionToken}"`);
|
|
7404
|
-
console.log(`export STATIC_UPLOAD_BUCKET_ID="${bucketId}"`);
|
|
7492
|
+
console.log(`export STATIC_ASSETS_BASE_URL="${shellEscape(downloadURLPrefix)}"`);
|
|
7493
|
+
console.log(`export STATIC_UPLOAD_PREFIX="${shellEscape(uploadPrefix)}"`);
|
|
7494
|
+
console.log(`export STATIC_UPLOAD_ID="${shellEscape(uploadID)}"`);
|
|
7495
|
+
console.log(`export STATIC_UPLOAD_AK="${shellEscape(uploadCredential.AccessKeyID)}"`);
|
|
7496
|
+
console.log(`export STATIC_UPLOAD_SK="${shellEscape(uploadCredential.SecretAccessKey)}"`);
|
|
7497
|
+
console.log(`export STATIC_UPLOAD_TOKEN="${shellEscape(uploadCredential.SessionToken)}"`);
|
|
7498
|
+
console.log(`export STATIC_UPLOAD_BUCKET_ID="${shellEscape(bucketId)}"`);
|
|
7405
7499
|
console.error(`${LOG_PREFIX2} \u73AF\u5883\u53D8\u91CF\u5DF2\u8F93\u51FA`);
|
|
7406
7500
|
} catch (error) {
|
|
7407
7501
|
const message = error instanceof Error ? error.message : String(error);
|
package/package.json
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
run = ["npm", "run", "dev"] # 默认 spark-cli dev
|
|
2
|
+
hidden = [".config", ".git", "scripts", "node_modules", "dist", ".spark", ".agent", ".agents", "tmp", ".spark_project", ".playwright-cli"]
|
|
3
|
+
lint = ["npm", "run", "lint"]
|
|
4
|
+
test = ["npm", "run", "test"]
|
|
5
|
+
genDbSchema = ["npm", "run", "gen:db-schema"]
|
|
6
|
+
genOpenApiClient = ["npm", "run", "gen:openapi"]
|
|
7
|
+
|
|
8
|
+
[deployment]
|
|
9
|
+
build = ["npm", "run", "build"]
|
|
10
|
+
run = ["npm", "run", "start"]
|
|
11
|
+
|
|
12
|
+
[files]
|
|
13
|
+
[files.restrict]
|
|
14
|
+
pathPatterns = ["client/src/api/gen", "package.json", ".spark_project", ".gitignore"]
|
|
15
|
+
[files.hidden]
|
|
16
|
+
pathPatterns = [".config", ".git", "scripts", "node_modules", "dist", ".spark", ".agent", ".agents", "tmp", ".spark_project", ".playwright-cli"]
|
|
@@ -19,46 +19,28 @@ print_time() {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
# ==================== 步骤 0 ====================
|
|
22
|
-
echo "🗑️ [0/
|
|
22
|
+
echo "🗑️ [0/5] 安装插件"
|
|
23
23
|
STEP_START=$(node -e "console.log(Date.now())")
|
|
24
24
|
npx fullstack-cli action-plugin init
|
|
25
25
|
print_time $STEP_START
|
|
26
26
|
echo ""
|
|
27
27
|
|
|
28
28
|
# ==================== 步骤 1 ====================
|
|
29
|
-
echo "📝 [1/
|
|
29
|
+
echo "📝 [1/5] 更新 openapi 代码"
|
|
30
30
|
STEP_START=$(node -e "console.log(Date.now())")
|
|
31
31
|
npm run gen:openapi
|
|
32
32
|
print_time $STEP_START
|
|
33
33
|
echo ""
|
|
34
34
|
|
|
35
35
|
# ==================== 步骤 2 ====================
|
|
36
|
-
echo "🗑️ [2/
|
|
36
|
+
echo "🗑️ [2/5] 清理 dist 目录"
|
|
37
37
|
STEP_START=$(node -e "console.log(Date.now())")
|
|
38
38
|
rm -rf "$ROOT_DIR/dist"
|
|
39
39
|
print_time $STEP_START
|
|
40
40
|
echo ""
|
|
41
41
|
|
|
42
42
|
# ==================== 步骤 3 ====================
|
|
43
|
-
echo "
|
|
44
|
-
STEP_START=$(node -e "console.log(Date.now())")
|
|
45
|
-
|
|
46
|
-
if [ -d "$ROOT_DIR/shared/static" ] && [ "$(ls -A "$ROOT_DIR/shared/static" 2>/dev/null)" ]; then
|
|
47
|
-
eval "$(npx fullstack-cli build pre-upload-static --app-id "${app_id}")"
|
|
48
|
-
if [ -n "$STATIC_ASSETS_BASE_URL" ]; then
|
|
49
|
-
echo " STATIC_ASSETS_BASE_URL=$STATIC_ASSETS_BASE_URL"
|
|
50
|
-
else
|
|
51
|
-
echo " ⚠️ 未获取到 TOS URL 前缀,将使用默认路径"
|
|
52
|
-
fi
|
|
53
|
-
else
|
|
54
|
-
echo " 跳过:shared/static 目录不存在或为空"
|
|
55
|
-
fi
|
|
56
|
-
|
|
57
|
-
print_time $STEP_START
|
|
58
|
-
echo ""
|
|
59
|
-
|
|
60
|
-
# ==================== 步骤 4 ====================
|
|
61
|
-
echo "🔨 [4/6] 并行构建 server 和 client"
|
|
43
|
+
echo "🔨 [3/5] 并行构建 server 和 client"
|
|
62
44
|
STEP_START=$(node -e "console.log(Date.now())")
|
|
63
45
|
|
|
64
46
|
# 并行构建
|
|
@@ -95,18 +77,23 @@ echo " ✅ Client 构建完成"
|
|
|
95
77
|
print_time $STEP_START
|
|
96
78
|
echo ""
|
|
97
79
|
|
|
98
|
-
# ==================== 步骤
|
|
99
|
-
echo "📦 [5
|
|
80
|
+
# ==================== 步骤 4 ====================
|
|
81
|
+
echo "📦 [4/5] 准备 server 依赖产物"
|
|
100
82
|
STEP_START=$(node -e "console.log(Date.now())")
|
|
101
83
|
|
|
102
84
|
mkdir -p "$OUT_DIR/dist/client"
|
|
103
85
|
|
|
104
|
-
#
|
|
105
|
-
|
|
86
|
+
# 移动 HTML(从 dist/client 移走,避免残留)
|
|
87
|
+
mv "$ROOT_DIR/dist/client/"*.html "$OUT_DIR/dist/client/" || true
|
|
106
88
|
|
|
107
89
|
# 拷贝 run.sh 文件
|
|
108
90
|
cp "$ROOT_DIR/scripts/run.sh" "$OUT_DIR/"
|
|
109
91
|
|
|
92
|
+
# 拷贝 .env 文件(如果存在)
|
|
93
|
+
if [ -f "$ROOT_DIR/.env" ]; then
|
|
94
|
+
cp "$ROOT_DIR/.env" "$OUT_DIR/"
|
|
95
|
+
fi
|
|
96
|
+
|
|
110
97
|
# 清理无用文件
|
|
111
98
|
rm -rf "$ROOT_DIR/dist/scripts"
|
|
112
99
|
rm -rf "$ROOT_DIR/dist/tsconfig.node.tsbuildinfo"
|
|
@@ -114,8 +101,8 @@ rm -rf "$ROOT_DIR/dist/tsconfig.node.tsbuildinfo"
|
|
|
114
101
|
print_time $STEP_START
|
|
115
102
|
echo ""
|
|
116
103
|
|
|
117
|
-
# ==================== 步骤
|
|
118
|
-
echo "✂️ [
|
|
104
|
+
# ==================== 步骤 5 ====================
|
|
105
|
+
echo "✂️ [5/5] 智能依赖裁剪"
|
|
119
106
|
STEP_START=$(node -e "console.log(Date.now())")
|
|
120
107
|
|
|
121
108
|
# 分析实际依赖、复制并裁剪 node_modules、生成精简的 package.json
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { spawn, execSync } = require('child_process');
|
|
7
|
+
const readline = require('readline');
|
|
8
|
+
|
|
9
|
+
// ── Project root ──────────────────────────────────────────────────────────────
|
|
10
|
+
const PROJECT_ROOT = path.resolve(__dirname, '..');
|
|
11
|
+
process.chdir(PROJECT_ROOT);
|
|
12
|
+
|
|
13
|
+
// ── Load .env ─────────────────────────────────────────────────────────────────
|
|
14
|
+
function loadEnv() {
|
|
15
|
+
const envPath = path.join(PROJECT_ROOT, '.env');
|
|
16
|
+
if (!fs.existsSync(envPath)) return;
|
|
17
|
+
const lines = fs.readFileSync(envPath, 'utf8').split('\n');
|
|
18
|
+
for (const line of lines) {
|
|
19
|
+
const trimmed = line.trim();
|
|
20
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
21
|
+
const eqIdx = trimmed.indexOf('=');
|
|
22
|
+
if (eqIdx === -1) continue;
|
|
23
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
24
|
+
const value = trimmed.slice(eqIdx + 1).trim();
|
|
25
|
+
if (!(key in process.env)) {
|
|
26
|
+
process.env[key] = value;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
loadEnv();
|
|
31
|
+
|
|
32
|
+
// ── Configuration ─────────────────────────────────────────────────────────────
|
|
33
|
+
const LOG_DIR = process.env.LOG_DIR || 'logs';
|
|
34
|
+
const MAX_RESTART_COUNT = parseInt(process.env.MAX_RESTART_COUNT, 10) || 10;
|
|
35
|
+
const RESTART_DELAY = parseInt(process.env.RESTART_DELAY, 10) || 2;
|
|
36
|
+
const MAX_DELAY = 60;
|
|
37
|
+
const SERVER_PORT = process.env.SERVER_PORT || '3000';
|
|
38
|
+
const CLIENT_DEV_PORT = process.env.CLIENT_DEV_PORT || '8080';
|
|
39
|
+
|
|
40
|
+
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
41
|
+
|
|
42
|
+
// ── Logging infrastructure ────────────────────────────────────────────────────
|
|
43
|
+
const devStdLogPath = path.join(LOG_DIR, 'dev.std.log');
|
|
44
|
+
const devLogPath = path.join(LOG_DIR, 'dev.log');
|
|
45
|
+
const devStdLogFd = fs.openSync(devStdLogPath, 'a');
|
|
46
|
+
const devLogFd = fs.openSync(devLogPath, 'a');
|
|
47
|
+
|
|
48
|
+
function timestamp() {
|
|
49
|
+
const now = new Date();
|
|
50
|
+
return (
|
|
51
|
+
now.getFullYear() + '-' +
|
|
52
|
+
String(now.getMonth() + 1).padStart(2, '0') + '-' +
|
|
53
|
+
String(now.getDate()).padStart(2, '0') + ' ' +
|
|
54
|
+
String(now.getHours()).padStart(2, '0') + ':' +
|
|
55
|
+
String(now.getMinutes()).padStart(2, '0') + ':' +
|
|
56
|
+
String(now.getSeconds()).padStart(2, '0')
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Write to terminal + dev.std.log */
|
|
61
|
+
function writeOutput(msg) {
|
|
62
|
+
try { process.stdout.write(msg); } catch {}
|
|
63
|
+
try { fs.writeSync(devStdLogFd, msg); } catch {}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Structured event log → terminal + dev.std.log + dev.log */
|
|
67
|
+
function logEvent(level, name, message) {
|
|
68
|
+
const msg = `[${timestamp()}] [${level}] [${name}] ${message}\n`;
|
|
69
|
+
writeOutput(msg);
|
|
70
|
+
try { fs.writeSync(devLogFd, msg); } catch {}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── Process group management ──────────────────────────────────────────────────
|
|
74
|
+
function killProcessGroup(pid, signal) {
|
|
75
|
+
try {
|
|
76
|
+
process.kill(-pid, signal);
|
|
77
|
+
} catch {}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function killOrphansByPort(port) {
|
|
81
|
+
try {
|
|
82
|
+
const pids = execSync(`lsof -ti :${port}`, { encoding: 'utf8', timeout: 5000 }).trim();
|
|
83
|
+
if (pids) {
|
|
84
|
+
const pidList = pids.split('\n').filter(Boolean);
|
|
85
|
+
for (const p of pidList) {
|
|
86
|
+
try { process.kill(parseInt(p, 10), 'SIGKILL'); } catch {}
|
|
87
|
+
}
|
|
88
|
+
return pidList;
|
|
89
|
+
}
|
|
90
|
+
} catch {}
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── Process supervision ───────────────────────────────────────────────────────
|
|
95
|
+
let stopping = false;
|
|
96
|
+
const managedProcesses = []; // { name, pid, child }
|
|
97
|
+
|
|
98
|
+
function sleep(ms) {
|
|
99
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Start and supervise a process with auto-restart and log piping.
|
|
104
|
+
* Returns a promise that resolves when the process loop ends.
|
|
105
|
+
*/
|
|
106
|
+
function startProcess({ name, command, args, cleanupPort }) {
|
|
107
|
+
const logFilePath = path.join(LOG_DIR, `${name}.std.log`);
|
|
108
|
+
const logFd = fs.openSync(logFilePath, 'a');
|
|
109
|
+
|
|
110
|
+
const entry = { name, pid: null, child: null };
|
|
111
|
+
managedProcesses.push(entry);
|
|
112
|
+
|
|
113
|
+
const run = async () => {
|
|
114
|
+
let restartCount = 0;
|
|
115
|
+
|
|
116
|
+
while (!stopping) {
|
|
117
|
+
const child = spawn(command, args, {
|
|
118
|
+
detached: true,
|
|
119
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
120
|
+
shell: true,
|
|
121
|
+
cwd: PROJECT_ROOT,
|
|
122
|
+
env: { ...process.env },
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
entry.pid = child.pid;
|
|
126
|
+
entry.child = child;
|
|
127
|
+
|
|
128
|
+
logEvent('INFO', name, `Process started (PGID: ${child.pid}): ${command} ${args.join(' ')}`);
|
|
129
|
+
|
|
130
|
+
// Pipe stdout and stderr through readline for timestamped logging
|
|
131
|
+
const pipeLines = (stream) => {
|
|
132
|
+
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
133
|
+
rl.on('line', (line) => {
|
|
134
|
+
const msg = `[${timestamp()}] [${name}] ${line}\n`;
|
|
135
|
+
try { fs.writeSync(logFd, msg); } catch {}
|
|
136
|
+
writeOutput(msg);
|
|
137
|
+
});
|
|
138
|
+
};
|
|
139
|
+
if (child.stdout) pipeLines(child.stdout);
|
|
140
|
+
if (child.stderr) pipeLines(child.stderr);
|
|
141
|
+
|
|
142
|
+
// Wait for the direct child to exit.
|
|
143
|
+
// NOTE: must use 'exit', not 'close'. With shell:true, grandchild processes
|
|
144
|
+
// (e.g. nest's server) inherit stdout pipes. 'close' won't fire until ALL
|
|
145
|
+
// pipe holders exit, causing dev.js to hang when npm/nest dies but server survives.
|
|
146
|
+
const exitCode = await new Promise((resolve) => {
|
|
147
|
+
child.on('exit', (code) => resolve(code ?? 1));
|
|
148
|
+
child.on('error', () => resolve(1));
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Kill the entire process group
|
|
152
|
+
if (entry.pid) {
|
|
153
|
+
killProcessGroup(entry.pid, 'SIGTERM');
|
|
154
|
+
await sleep(2000);
|
|
155
|
+
killProcessGroup(entry.pid, 'SIGKILL');
|
|
156
|
+
}
|
|
157
|
+
entry.pid = null;
|
|
158
|
+
entry.child = null;
|
|
159
|
+
|
|
160
|
+
// Port cleanup fallback
|
|
161
|
+
if (cleanupPort) {
|
|
162
|
+
const orphans = killOrphansByPort(cleanupPort);
|
|
163
|
+
if (orphans.length > 0) {
|
|
164
|
+
logEvent('WARN', name, `Killed orphan processes on port ${cleanupPort}: ${orphans.join(' ')}`);
|
|
165
|
+
await sleep(500);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (stopping) break;
|
|
170
|
+
|
|
171
|
+
restartCount++;
|
|
172
|
+
if (restartCount >= MAX_RESTART_COUNT) {
|
|
173
|
+
logEvent('ERROR', name, `Max restart count (${MAX_RESTART_COUNT}) reached, giving up`);
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const delay = Math.min(RESTART_DELAY * (1 << (restartCount - 1)), MAX_DELAY);
|
|
178
|
+
logEvent('WARN', name, `Process exited with code ${exitCode}, restarting (${restartCount}/${MAX_RESTART_COUNT}) in ${delay}s...`);
|
|
179
|
+
await sleep(delay * 1000);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
try { fs.closeSync(logFd); } catch {}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
return run();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ── Cleanup ───────────────────────────────────────────────────────────────────
|
|
189
|
+
let cleanupDone = false;
|
|
190
|
+
|
|
191
|
+
async function cleanup() {
|
|
192
|
+
if (cleanupDone) return;
|
|
193
|
+
cleanupDone = true;
|
|
194
|
+
stopping = true;
|
|
195
|
+
|
|
196
|
+
logEvent('INFO', 'main', 'Shutting down all processes...');
|
|
197
|
+
|
|
198
|
+
// Kill all managed process groups
|
|
199
|
+
for (const entry of managedProcesses) {
|
|
200
|
+
if (entry.pid) {
|
|
201
|
+
logEvent('INFO', 'main', `Stopping process group (PGID: ${entry.pid})`);
|
|
202
|
+
killProcessGroup(entry.pid, 'SIGTERM');
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Wait for graceful shutdown
|
|
207
|
+
await sleep(2000);
|
|
208
|
+
|
|
209
|
+
// Force kill any remaining
|
|
210
|
+
for (const entry of managedProcesses) {
|
|
211
|
+
if (entry.pid) {
|
|
212
|
+
logEvent('WARN', 'main', `Force killing process group (PGID: ${entry.pid})`);
|
|
213
|
+
killProcessGroup(entry.pid, 'SIGKILL');
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Port cleanup fallback
|
|
218
|
+
killOrphansByPort(SERVER_PORT);
|
|
219
|
+
killOrphansByPort(CLIENT_DEV_PORT);
|
|
220
|
+
|
|
221
|
+
logEvent('INFO', 'main', 'All processes stopped');
|
|
222
|
+
|
|
223
|
+
try { fs.closeSync(devStdLogFd); } catch {}
|
|
224
|
+
try { fs.closeSync(devLogFd); } catch {}
|
|
225
|
+
|
|
226
|
+
process.exit(0);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
process.on('SIGTERM', cleanup);
|
|
230
|
+
process.on('SIGINT', cleanup);
|
|
231
|
+
process.on('SIGHUP', cleanup);
|
|
232
|
+
|
|
233
|
+
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
234
|
+
async function main() {
|
|
235
|
+
logEvent('INFO', 'main', '========== Dev session started ==========');
|
|
236
|
+
|
|
237
|
+
// Initialize action plugins
|
|
238
|
+
writeOutput('\n🔌 Initializing action plugins...\n');
|
|
239
|
+
try {
|
|
240
|
+
execSync('fullstack-cli action-plugin init', { cwd: PROJECT_ROOT, stdio: 'inherit' });
|
|
241
|
+
writeOutput('✅ Action plugins initialized\n\n');
|
|
242
|
+
} catch {
|
|
243
|
+
writeOutput('⚠️ Action plugin initialization failed, continuing anyway...\n\n');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Start server and client
|
|
247
|
+
const serverPromise = startProcess({
|
|
248
|
+
name: 'server',
|
|
249
|
+
command: 'npm',
|
|
250
|
+
args: ['run', 'dev:server'],
|
|
251
|
+
cleanupPort: SERVER_PORT,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const clientPromise = startProcess({
|
|
255
|
+
name: 'client',
|
|
256
|
+
command: 'npm',
|
|
257
|
+
args: ['run', 'dev:client'],
|
|
258
|
+
cleanupPort: CLIENT_DEV_PORT,
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
writeOutput(`📋 Dev processes running. Press Ctrl+C to stop.\n`);
|
|
262
|
+
writeOutput(`📄 Logs: ${devStdLogPath}\n\n`);
|
|
263
|
+
|
|
264
|
+
// Wait for both (they loop until stopping or max restarts)
|
|
265
|
+
await Promise.all([serverPromise, clientPromise]);
|
|
266
|
+
|
|
267
|
+
if (!cleanupDone) {
|
|
268
|
+
await cleanup();
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
main().catch((err) => {
|
|
273
|
+
console.error('Fatal error:', err);
|
|
274
|
+
process.exit(1);
|
|
275
|
+
});
|
package/templates/scripts/dev.sh
CHANGED
|
@@ -1,245 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
set -uo pipefail
|
|
5
|
-
|
|
6
|
-
# Ensure the script always runs from the project root
|
|
7
|
-
cd "$(dirname "${BASH_SOURCE[0]}")/.."
|
|
8
|
-
|
|
9
|
-
# Configuration
|
|
10
|
-
LOG_DIR=${LOG_DIR:-logs}
|
|
11
|
-
DEV_LOG="${LOG_DIR}/dev.log"
|
|
12
|
-
MAX_RESTART_COUNT=${MAX_RESTART_COUNT:-10}
|
|
13
|
-
RESTART_DELAY=${RESTART_DELAY:-2}
|
|
14
|
-
|
|
15
|
-
# Process tracking
|
|
16
|
-
SERVER_PID=""
|
|
17
|
-
CLIENT_PID=""
|
|
18
|
-
PARENT_PID=$$
|
|
19
|
-
STOP_FLAG_FILE="/tmp/dev_sh_stop_$$"
|
|
20
|
-
CLEANUP_DONE=false
|
|
21
|
-
|
|
22
|
-
mkdir -p "${LOG_DIR}"
|
|
23
|
-
|
|
24
|
-
# Redirect all stdout/stderr to both terminal and log file
|
|
25
|
-
DEV_STD_LOG="${LOG_DIR}/dev.std.log"
|
|
26
|
-
exec > >(tee -a "$DEV_STD_LOG") 2>&1
|
|
27
|
-
|
|
28
|
-
# Log event to dev.log with timestamp
|
|
29
|
-
log_event() {
|
|
30
|
-
local level=$1
|
|
31
|
-
local process_name=$2
|
|
32
|
-
local message=$3
|
|
33
|
-
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] [${level}] [${process_name}] ${message}"
|
|
34
|
-
echo "$msg"
|
|
35
|
-
echo "$msg" >> "${DEV_LOG}"
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
# Check if PID is valid (positive integer and process exists)
|
|
39
|
-
is_valid_pid() {
|
|
40
|
-
local pid=$1
|
|
41
|
-
[[ -n "$pid" ]] && [[ "$pid" =~ ^[0-9]+$ ]] && [[ "$pid" -gt 0 ]] && kill -0 "$pid" 2>/dev/null
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
# Check if parent process is still alive
|
|
45
|
-
is_parent_alive() {
|
|
46
|
-
kill -0 "$PARENT_PID" 2>/dev/null
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
# Check if should stop (parent exited or stop flag exists)
|
|
50
|
-
should_stop() {
|
|
51
|
-
[[ -f "$STOP_FLAG_FILE" ]] || ! is_parent_alive
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
# Kill entire process tree (process and all descendants)
|
|
55
|
-
kill_tree() {
|
|
56
|
-
local pid=$1
|
|
57
|
-
local signal=${2:-TERM}
|
|
58
|
-
|
|
59
|
-
# Get all descendant PIDs
|
|
60
|
-
local children
|
|
61
|
-
children=$(pgrep -P "$pid" 2>/dev/null) || true
|
|
62
|
-
|
|
63
|
-
# Recursively kill children first
|
|
64
|
-
for child in $children; do
|
|
65
|
-
kill_tree "$child" "$signal"
|
|
66
|
-
done
|
|
67
|
-
|
|
68
|
-
# Kill the process itself
|
|
69
|
-
if kill -0 "$pid" 2>/dev/null; then
|
|
70
|
-
kill -"$signal" "$pid" 2>/dev/null || true
|
|
71
|
-
fi
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
# Start a process with supervision
|
|
75
|
-
# $1: name
|
|
76
|
-
# $2: command
|
|
77
|
-
# $3: cleanup port for orphan processes (optional)
|
|
78
|
-
start_supervised_process() {
|
|
79
|
-
local name=$1
|
|
80
|
-
local cmd=$2
|
|
81
|
-
local cleanup_port=${3:-""}
|
|
82
|
-
local log_file="${LOG_DIR}/${name}.std.log"
|
|
83
|
-
|
|
84
|
-
(
|
|
85
|
-
local restart_count=0
|
|
86
|
-
local child_pid=""
|
|
87
|
-
local max_delay=60 # Maximum delay in seconds
|
|
88
|
-
|
|
89
|
-
# Handle signals to kill child process tree
|
|
90
|
-
trap 'if [[ -n "$child_pid" ]]; then kill_tree "$child_pid" TERM; fi' TERM INT
|
|
91
|
-
|
|
92
|
-
while true; do
|
|
93
|
-
# Check if we should stop (parent exited or stop flag)
|
|
94
|
-
if should_stop; then
|
|
95
|
-
log_event "INFO" "$name" "Process stopped (parent exited or user requested)"
|
|
96
|
-
break
|
|
97
|
-
fi
|
|
98
|
-
|
|
99
|
-
# Start command in background and capture output with timestamps
|
|
100
|
-
eval "$cmd" > >(
|
|
101
|
-
while IFS= read -r line; do
|
|
102
|
-
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] [${name}] ${line}"
|
|
103
|
-
echo "$msg"
|
|
104
|
-
echo "$msg" >> "$log_file"
|
|
105
|
-
done
|
|
106
|
-
) 2>&1 &
|
|
107
|
-
child_pid=$!
|
|
108
|
-
|
|
109
|
-
log_event "INFO" "$name" "Process started (PID: ${child_pid}): ${cmd}"
|
|
110
|
-
|
|
111
|
-
# Wait for child to exit
|
|
112
|
-
set +e
|
|
113
|
-
wait "$child_pid"
|
|
114
|
-
exit_code=$?
|
|
115
|
-
set -e
|
|
116
|
-
|
|
117
|
-
# Kill entire process tree to avoid orphans
|
|
118
|
-
if [[ -n "$child_pid" ]]; then
|
|
119
|
-
kill_tree "$child_pid" TERM
|
|
120
|
-
sleep 0.5
|
|
121
|
-
kill_tree "$child_pid" KILL
|
|
122
|
-
fi
|
|
123
|
-
child_pid=""
|
|
124
|
-
|
|
125
|
-
# Cleanup orphan processes by port (for processes that escaped kill_tree)
|
|
126
|
-
if [[ -n "$cleanup_port" ]]; then
|
|
127
|
-
local orphan_pids
|
|
128
|
-
orphan_pids=$(lsof -ti ":${cleanup_port}" 2>/dev/null) || true
|
|
129
|
-
if [[ -n "$orphan_pids" ]]; then
|
|
130
|
-
log_event "WARN" "$name" "Killing orphan processes on port ${cleanup_port}: $(echo $orphan_pids | tr '\n' ' ')"
|
|
131
|
-
echo "$orphan_pids" | xargs kill -9 2>/dev/null || true
|
|
132
|
-
sleep 0.5
|
|
133
|
-
fi
|
|
134
|
-
fi
|
|
135
|
-
|
|
136
|
-
# Check if we should stop (parent exited or stop flag)
|
|
137
|
-
if should_stop; then
|
|
138
|
-
log_event "INFO" "$name" "Process stopped (parent exited or user requested)"
|
|
139
|
-
break
|
|
140
|
-
fi
|
|
141
|
-
|
|
142
|
-
# Process exited unexpectedly, restart
|
|
143
|
-
restart_count=$((restart_count + 1))
|
|
144
|
-
|
|
145
|
-
if [[ $restart_count -ge $MAX_RESTART_COUNT ]]; then
|
|
146
|
-
log_event "ERROR" "$name" "Max restart count (${MAX_RESTART_COUNT}) reached, giving up"
|
|
147
|
-
break
|
|
148
|
-
fi
|
|
149
|
-
|
|
150
|
-
# Exponential backoff: delay = RESTART_DELAY * 2^(restart_count-1), capped at max_delay
|
|
151
|
-
local delay=$((RESTART_DELAY * (1 << (restart_count - 1))))
|
|
152
|
-
if [[ $delay -gt $max_delay ]]; then
|
|
153
|
-
delay=$max_delay
|
|
154
|
-
fi
|
|
155
|
-
|
|
156
|
-
log_event "WARN" "$name" "Process exited with code ${exit_code}, restarting (${restart_count}/${MAX_RESTART_COUNT}) in ${delay}s..."
|
|
157
|
-
sleep "$delay"
|
|
158
|
-
done
|
|
159
|
-
) &
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
# Cleanup function
|
|
163
|
-
cleanup() {
|
|
164
|
-
# Prevent multiple cleanup calls
|
|
165
|
-
if [[ "$CLEANUP_DONE" == "true" ]]; then
|
|
166
|
-
return
|
|
167
|
-
fi
|
|
168
|
-
CLEANUP_DONE=true
|
|
169
|
-
|
|
170
|
-
log_event "INFO" "main" "Shutting down all processes..."
|
|
171
|
-
|
|
172
|
-
# Create stop flag to signal child processes
|
|
173
|
-
touch "$STOP_FLAG_FILE"
|
|
174
|
-
|
|
175
|
-
# Kill entire process trees (TERM first)
|
|
176
|
-
for pid in $SERVER_PID $CLIENT_PID; do
|
|
177
|
-
if is_valid_pid "$pid"; then
|
|
178
|
-
log_event "INFO" "main" "Stopping process tree (PID: ${pid})"
|
|
179
|
-
kill_tree "$pid" TERM
|
|
180
|
-
fi
|
|
181
|
-
done
|
|
182
|
-
|
|
183
|
-
# Kill any remaining background jobs
|
|
184
|
-
local bg_pids
|
|
185
|
-
bg_pids=$(jobs -p 2>/dev/null) || true
|
|
186
|
-
if [[ -n "$bg_pids" ]]; then
|
|
187
|
-
for pid in $bg_pids; do
|
|
188
|
-
kill_tree "$pid" TERM
|
|
189
|
-
done
|
|
190
|
-
fi
|
|
191
|
-
|
|
192
|
-
# Wait for processes to terminate
|
|
193
|
-
sleep 1
|
|
194
|
-
|
|
195
|
-
# Force kill if still running
|
|
196
|
-
for pid in $SERVER_PID $CLIENT_PID; do
|
|
197
|
-
if is_valid_pid "$pid"; then
|
|
198
|
-
log_event "WARN" "main" "Force killing process tree (PID: ${pid})"
|
|
199
|
-
kill_tree "$pid" KILL
|
|
200
|
-
fi
|
|
201
|
-
done
|
|
202
|
-
|
|
203
|
-
# Cleanup stop flag
|
|
204
|
-
rm -f "$STOP_FLAG_FILE"
|
|
205
|
-
|
|
206
|
-
log_event "INFO" "main" "All processes stopped"
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
# Set up signal handlers
|
|
210
|
-
trap cleanup EXIT INT TERM HUP
|
|
211
|
-
|
|
212
|
-
# Remove any stale stop flag
|
|
213
|
-
rm -f "$STOP_FLAG_FILE"
|
|
214
|
-
|
|
215
|
-
# Initialize dev.log
|
|
216
|
-
echo "" >> "${DEV_LOG}"
|
|
217
|
-
log_event "INFO" "main" "========== Dev session started =========="
|
|
218
|
-
|
|
219
|
-
# Initialize action plugins before starting dev servers
|
|
220
|
-
echo "🔌 Initializing action plugins..."
|
|
221
|
-
if fullstack-cli action-plugin init; then
|
|
222
|
-
echo "✅ Action plugins initialized"
|
|
223
|
-
else
|
|
224
|
-
echo "⚠️ Action plugin initialization failed, continuing anyway..."
|
|
225
|
-
fi
|
|
226
|
-
echo ""
|
|
227
|
-
|
|
228
|
-
# Start server (cleanup orphan processes on SERVER_PORT)
|
|
229
|
-
start_supervised_process "server" "npm run dev:server" "${SERVER_PORT:-3000}"
|
|
230
|
-
SERVER_PID=$!
|
|
231
|
-
log_event "INFO" "server" "Supervisor started with PID ${SERVER_PID}"
|
|
232
|
-
|
|
233
|
-
# Start client (cleanup orphan processes on CLIENT_DEV_PORT)
|
|
234
|
-
start_supervised_process "client" "npm run dev:client" "${CLIENT_DEV_PORT:-8080}"
|
|
235
|
-
CLIENT_PID=$!
|
|
236
|
-
log_event "INFO" "client" "Supervisor started with PID ${CLIENT_PID}"
|
|
237
|
-
|
|
238
|
-
log_event "INFO" "main" "All processes started, monitoring..."
|
|
239
|
-
echo ""
|
|
240
|
-
echo "📋 Dev processes running. Press Ctrl+C to stop."
|
|
241
|
-
echo "📄 Logs: ${DEV_STD_LOG}"
|
|
242
|
-
echo ""
|
|
243
|
-
|
|
244
|
-
# Wait for all background processes
|
|
245
|
-
wait
|
|
2
|
+
exec node "$(dirname "$0")/dev.js" "$@"
|