@lark-apaas/fullstack-cli 1.1.28-alpha.2 → 1.1.28-alpha.20

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);
@@ -7181,17 +7270,20 @@ async function genArtifactUploadCredential(appId, body) {
7181
7270
  }
7182
7271
  async function getDefaultBucketId(appId) {
7183
7272
  const client = getHttpClient();
7184
- const url = `/b/${appId}/get_published_v2`;
7185
- const response = await client.get(url);
7273
+ const url = `/v1/app/${appId}/storage/inner/staticBucket`;
7274
+ const response = await client.post(url, {});
7186
7275
  if (!response.ok || response.status !== 200) {
7187
7276
  throw new Error(
7188
- `get_published_v2 \u8BF7\u6C42\u5931\u8D25: ${response.status} ${response.statusText}`
7277
+ `getOrCreateStaticBucket \u8BF7\u6C42\u5931\u8D25: ${response.status} ${response.statusText}`
7189
7278
  );
7190
7279
  }
7191
7280
  const data = await response.json();
7192
- const bucketId = data?.data?.app_runtime_extra?.bucket?.default_bucket_id;
7281
+ if (data.status_code !== "0") {
7282
+ throw new Error(`getOrCreateStaticBucket \u8FD4\u56DE\u5F02\u5E38, status_code: ${data.status_code}`);
7283
+ }
7284
+ const bucketId = data?.data?.bucketID;
7193
7285
  if (!bucketId) {
7194
- throw new Error(`\u672A\u627E\u5230\u5E94\u7528 ${appId} \u7684\u9ED8\u8BA4\u5B58\u50A8\u6876`);
7286
+ throw new Error(`\u672A\u627E\u5230\u5E94\u7528 ${appId} \u7684\u9759\u6001\u8D44\u6E90\u5B58\u50A8\u6876`);
7195
7287
  }
7196
7288
  return bucketId;
7197
7289
  }
@@ -7253,7 +7345,7 @@ function camelToKebab(str) {
7253
7345
  // src/commands/build/upload-static.handler.ts
7254
7346
  import * as fs25 from "fs";
7255
7347
  import * as path21 from "path";
7256
- import { execSync as execSync3 } from "child_process";
7348
+ import { execFileSync } from "child_process";
7257
7349
  function readCredentialsFromEnv() {
7258
7350
  const uploadPrefix = process.env.STATIC_UPLOAD_PREFIX;
7259
7351
  const uploadID = process.env.STATIC_UPLOAD_ID;
@@ -7272,7 +7364,7 @@ async function uploadStatic(options) {
7272
7364
  const {
7273
7365
  appId,
7274
7366
  staticDir = "shared/static",
7275
- tosutilPath = "/workspace/tosutil",
7367
+ tosutilPath = "tosutil",
7276
7368
  endpoint = "tos-cn-beijing.volces.com",
7277
7369
  region = "cn-beijing"
7278
7370
  } = options;
@@ -7285,9 +7377,10 @@ async function uploadStatic(options) {
7285
7377
  console.error(`${LOG_PREFIX} \u76EE\u5F55\u4E3A\u7A7A: ${resolvedStaticDir}\uFF0C\u8DF3\u8FC7\u4E0A\u4F20`);
7286
7378
  return;
7287
7379
  }
7288
- if (!fs25.existsSync(tosutilPath)) {
7380
+ const resolvedTosutil = resolveTosutilPath(tosutilPath);
7381
+ if (!resolvedTosutil) {
7289
7382
  throw new Error(
7290
- `tosutil \u4E0D\u5B58\u5728: ${tosutilPath}\u3002\u8BF7\u786E\u4FDD\u6D41\u6C34\u7EBF\u5DF2\u5728"\u4EA7\u7269\u6253\u5305\u4E0A\u4F20"\u6B65\u9AA4\u4E2D\u4E0B\u8F7D tosutil\u3002`
7383
+ `tosutil \u4E0D\u5B58\u5728: ${tosutilPath}\u3002\u8BF7\u786E\u4FDD tosutil \u5DF2\u5B89\u88C5\u5728\u7CFB\u7EDF PATH \u4E2D\u6216\u901A\u8FC7 --tosutil-path \u6307\u5B9A\u8DEF\u5F84\u3002`
7291
7384
  );
7292
7385
  }
7293
7386
  let uploadPrefix;
@@ -7310,7 +7403,7 @@ async function uploadStatic(options) {
7310
7403
  }
7311
7404
  console.error(`${LOG_PREFIX} \u4E0A\u4F20\u76EE\u6807: ${uploadPrefix}`);
7312
7405
  console.error(`${LOG_PREFIX} \u914D\u7F6E tosutil...`);
7313
- configureTosutil(tosutilPath, {
7406
+ configureTosutil(resolvedTosutil, {
7314
7407
  endpoint,
7315
7408
  region,
7316
7409
  accessKeyID,
@@ -7318,7 +7411,7 @@ async function uploadStatic(options) {
7318
7411
  sessionToken
7319
7412
  });
7320
7413
  console.error(`${LOG_PREFIX} \u4E0A\u4F20 ${resolvedStaticDir} -> ${uploadPrefix}`);
7321
- uploadToTos(tosutilPath, resolvedStaticDir, uploadPrefix);
7414
+ uploadToTos(resolvedTosutil, resolvedStaticDir, uploadPrefix);
7322
7415
  console.error(`${LOG_PREFIX} tosutil \u4E0A\u4F20\u5B8C\u6210`);
7323
7416
  console.error(`${LOG_PREFIX} \u8C03\u7528 callbackStatic (uploadID: ${uploadID})...`);
7324
7417
  const callbackResp = await uploadStaticAttachmentCallback(appId, bucketId, { uploadID });
@@ -7334,6 +7427,17 @@ async function uploadStatic(options) {
7334
7427
  process.exit(1);
7335
7428
  }
7336
7429
  }
7430
+ function resolveTosutilPath(tosutilPath) {
7431
+ if (path21.isAbsolute(tosutilPath)) {
7432
+ return fs25.existsSync(tosutilPath) ? tosutilPath : null;
7433
+ }
7434
+ try {
7435
+ const resolved = execFileSync("which", [tosutilPath], { encoding: "utf-8" }).trim();
7436
+ return resolved || null;
7437
+ } catch {
7438
+ return null;
7439
+ }
7440
+ }
7337
7441
  async function fetchPreUpload(appId, bucketId) {
7338
7442
  const response = await preUploadStaticAttachment(appId, bucketId);
7339
7443
  if (response.status_code !== "0") {
@@ -7350,14 +7454,16 @@ async function fetchPreUpload(appId, bucketId) {
7350
7454
  }
7351
7455
  function configureTosutil(tosutilPath, config) {
7352
7456
  const { endpoint, region, accessKeyID, secretAccessKey, sessionToken } = config;
7353
- execSync3(
7354
- `${tosutilPath} config -e ${endpoint} -i ${accessKeyID} -k ${secretAccessKey} -t ${sessionToken} -re ${region}`,
7457
+ execFileSync(
7458
+ tosutilPath,
7459
+ ["config", "-e", endpoint, "-i", accessKeyID, "-k", secretAccessKey, "-t", sessionToken, "-re", region],
7355
7460
  { stdio: "pipe" }
7356
7461
  );
7357
7462
  }
7358
7463
  function uploadToTos(tosutilPath, sourceDir, destUrl) {
7359
- execSync3(
7360
- `${tosutilPath} cp "${sourceDir}" "${destUrl}" -r -flat -j 5 -p 3 -ps 10485760 -f`,
7464
+ execFileSync(
7465
+ tosutilPath,
7466
+ ["cp", sourceDir, destUrl, "-r", "-flat", "-j", "5", "-p", "3", "-ps", "10485760", "-f"],
7361
7467
  { stdio: "inherit" }
7362
7468
  );
7363
7469
  }
@@ -7374,6 +7480,9 @@ function isDirEmpty(dirPath) {
7374
7480
 
7375
7481
  // src/commands/build/pre-upload-static.handler.ts
7376
7482
  var LOG_PREFIX2 = "[pre-upload-static]";
7483
+ function shellEscape(value) {
7484
+ return value.replace(/["\\`$]/g, "\\$&");
7485
+ }
7377
7486
  async function preUploadStatic(options) {
7378
7487
  try {
7379
7488
  const { appId } = options;
@@ -7386,8 +7495,8 @@ async function preUploadStatic(options) {
7386
7495
  console.error(`${LOG_PREFIX2} preUploadStatic \u8FD4\u56DE\u5F02\u5E38, status_code: ${response.status_code}`);
7387
7496
  return;
7388
7497
  }
7389
- const { downloadUrlPrefix, uploadPrefix, uploadID, uploadCredential } = response.data || {};
7390
- if (!downloadUrlPrefix || !uploadPrefix || !uploadID) {
7498
+ const { downloadURLPrefix, uploadPrefix, uploadID, uploadCredential } = response.data || {};
7499
+ if (!downloadURLPrefix || !uploadPrefix || !uploadID) {
7391
7500
  console.error(`${LOG_PREFIX2} preUploadStatic \u8FD4\u56DE\u6570\u636E\u4E0D\u5B8C\u6574`);
7392
7501
  return;
7393
7502
  }
@@ -7395,13 +7504,13 @@ async function preUploadStatic(options) {
7395
7504
  console.error(`${LOG_PREFIX2} preUploadStatic \u8FD4\u56DE\u7684\u51ED\u8BC1\u5B57\u6BB5\u4E0D\u5B8C\u6574`);
7396
7505
  return;
7397
7506
  }
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}"`);
7507
+ console.log(`export STATIC_ASSETS_BASE_URL="${shellEscape(downloadURLPrefix)}"`);
7508
+ console.log(`export STATIC_UPLOAD_PREFIX="${shellEscape(uploadPrefix)}"`);
7509
+ console.log(`export STATIC_UPLOAD_ID="${shellEscape(uploadID)}"`);
7510
+ console.log(`export STATIC_UPLOAD_AK="${shellEscape(uploadCredential.AccessKeyID)}"`);
7511
+ console.log(`export STATIC_UPLOAD_SK="${shellEscape(uploadCredential.SecretAccessKey)}"`);
7512
+ console.log(`export STATIC_UPLOAD_TOKEN="${shellEscape(uploadCredential.SessionToken)}"`);
7513
+ console.log(`export STATIC_UPLOAD_BUCKET_ID="${shellEscape(bucketId)}"`);
7405
7514
  console.error(`${LOG_PREFIX2} \u73AF\u5883\u53D8\u91CF\u5DF2\u8F93\u51FA`);
7406
7515
  } catch (error) {
7407
7516
  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.2",
3
+ "version": "1.1.28-alpha.20",
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"]
@@ -3,7 +3,7 @@
3
3
  set -euo pipefail
4
4
 
5
5
  ROOT_DIR="$(pwd)"
6
- OUT_DIR="$ROOT_DIR/dist/server"
6
+ DIST_DIR="$ROOT_DIR/dist"
7
7
 
8
8
  # 记录总开始时间
9
9
  TOTAL_START=$(node -e "console.log(Date.now())")
@@ -78,20 +78,26 @@ print_time $STEP_START
78
78
  echo ""
79
79
 
80
80
  # ==================== 步骤 4 ====================
81
- echo "📦 [4/5] 准备 server 依赖产物"
81
+ echo "📦 [4/5] 准备产物"
82
82
  STEP_START=$(node -e "console.log(Date.now())")
83
83
 
84
- mkdir -p "$OUT_DIR/dist/client"
84
+ # 拷贝 run.sh 到 dist/(prod 从 dist/ 启动,确保 cwd 一致性)
85
+ cp "$ROOT_DIR/scripts/run.sh" "$DIST_DIR/"
85
86
 
86
- # 拷贝 HTML
87
- cp "$ROOT_DIR/dist/client/"*.html "$OUT_DIR/dist/client/" || true
87
+ # 拷贝 .env 文件(如果存在)
88
+ if [ -f "$ROOT_DIR/.env" ]; then
89
+ cp "$ROOT_DIR/.env" "$DIST_DIR/"
90
+ fi
88
91
 
89
- # 拷贝 run.sh 文件
90
- cp "$ROOT_DIR/scripts/run.sh" "$OUT_DIR/"
92
+ # 移动 client 下的 HTML 文件到 dist/dist/client,保证 views 路径在 dev/prod 下一致
93
+ if [ -d "$DIST_DIR/client" ]; then
94
+ mkdir -p "$DIST_DIR/dist/client"
95
+ find "$DIST_DIR/client" -maxdepth 1 -name "*.html" -exec mv {} "$DIST_DIR/dist/client/" \;
96
+ fi
91
97
 
92
98
  # 清理无用文件
93
- rm -rf "$ROOT_DIR/dist/scripts"
94
- rm -rf "$ROOT_DIR/dist/tsconfig.node.tsbuildinfo"
99
+ rm -rf "$DIST_DIR/scripts"
100
+ rm -rf "$DIST_DIR/tsconfig.node.tsbuildinfo"
95
101
 
96
102
  print_time $STEP_START
97
103
  echo ""
@@ -111,8 +117,8 @@ echo "构建完成"
111
117
  print_time $TOTAL_START
112
118
 
113
119
  # 输出产物信息
114
- DIST_SIZE=$(du -sh "$OUT_DIR" | cut -f1)
115
- NODE_MODULES_SIZE=$(du -sh "$ROOT_DIR/dist/node_modules" | cut -f1)
120
+ DIST_SIZE=$(du -sh "$DIST_DIR" | cut -f1)
121
+ NODE_MODULES_SIZE=$(du -sh "$DIST_DIR/node_modules" | cut -f1)
116
122
  echo ""
117
123
  echo "📊 构建产物统计:"
118
124
  echo " 产物大小: $DIST_SIZE"
@@ -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" "$@"
@@ -6,11 +6,12 @@ const fs = require('fs');
6
6
  const path = require('path');
7
7
 
8
8
  const ROOT_DIR = path.resolve(__dirname, '..');
9
- const DIST_SERVER_DIR = path.join(ROOT_DIR, 'dist/server');
9
+ const DIST_DIR = path.join(ROOT_DIR, 'dist');
10
+ const DIST_SERVER_DIR = path.join(DIST_DIR, 'server');
10
11
  const ROOT_PACKAGE_JSON = path.join(ROOT_DIR, 'package.json');
11
12
  const ROOT_NODE_MODULES = path.join(ROOT_DIR, 'node_modules');
12
- const OUT_NODE_MODULES = path.join(ROOT_DIR, 'dist/node_modules');
13
- const OUT_PACKAGE_JSON = path.join(DIST_SERVER_DIR, 'package.json');
13
+ const OUT_NODE_MODULES = path.join(DIST_DIR, 'node_modules');
14
+ const OUT_PACKAGE_JSON = path.join(DIST_DIR, 'package.json');
14
15
 
15
16
  // Server 入口文件
16
17
  const SERVER_ENTRY = path.join(DIST_SERVER_DIR, 'main.js');
@@ -294,9 +295,6 @@ async function smartPrune() {
294
295
  version: originalPackage.version,
295
296
  private: true,
296
297
  dependencies: prunedDependencies,
297
- scripts: {
298
- start: originalPackage.scripts?.start || 'node main.js'
299
- },
300
298
  engines: originalPackage.engines
301
299
  };
302
300
 
@@ -2,4 +2,7 @@
2
2
  # This file is auto-generated by @lark-apaas/fullstack-cli
3
3
 
4
4
  # 生产环境下,启动服务
5
- npm run start
5
+ # dist/ 根目录执行,确保 process.cwd() 与 dev 模式一致
6
+ # dev: cwd = 项目根, shared/ client/ 可达
7
+ # prod: cwd = dist/, shared/ client/ 可达
8
+ NODE_ENV=production node server/main.js