@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 CHANGED
@@ -2377,12 +2377,15 @@ var syncConfig = {
2377
2377
  type: "directory",
2378
2378
  overwrite: true
2379
2379
  },
2380
- // 2. 覆写 nest-cli.json 配置,禁止用户修改
2380
+ // 2. 智能合并 nest-cli.json 配置(保留用户自定义的 assets、plugins 等)
2381
2381
  {
2382
2382
  from: "templates/nest-cli.json",
2383
2383
  to: "nest-cli.json",
2384
- type: "file",
2385
- overwrite: true
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 { execSync as execSync3 } from "child_process";
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
- execSync3(
7354
- `${tosutilPath} config -e ${endpoint} -i ${accessKeyID} -k ${secretAccessKey} -t ${sessionToken} -re ${region}`,
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
- execSync3(
7360
- `${tosutilPath} cp "${sourceDir}" "${destUrl}" -r -flat -j 5 -p 3 -ps 10485760 -f`,
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 { downloadUrlPrefix, uploadPrefix, uploadID, uploadCredential } = response.data || {};
7390
- if (!downloadUrlPrefix || !uploadPrefix || !uploadID) {
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="${downloadUrlPrefix}"`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/fullstack-cli",
3
- "version": "1.1.28-alpha.1",
3
+ "version": "1.1.28-alpha.11",
4
4
  "description": "CLI tool for fullstack template management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -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/6] 安装插件"
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/6] 更新 openapi 代码"
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/6] 清理 dist 目录"
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 "🔗 [3/6] 预上传:获取 TOS 信息并注入环境变量"
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
- # ==================== 步骤 5 ====================
99
- echo "📦 [5/6] 准备 server 依赖产物"
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
- # 拷贝 HTML
105
- cp "$ROOT_DIR/dist/client/"*.html "$OUT_DIR/dist/client/" || true
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
- # ==================== 步骤 6 ====================
118
- echo "✂️ [6/6] 智能依赖裁剪"
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
+ });
@@ -1,245 +1,2 @@
1
1
  #!/usr/bin/env bash
2
- # This file is auto-generated by @lark-apaas/fullstack-cli
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" "$@"