@lark-apaas/fullstack-cli 1.1.28-alpha.2 → 1.1.28-alpha.21
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 +147 -32
- package/package.json +1 -1
- package/templates/.spark_project +16 -0
- package/templates/scripts/build.sh +17 -11
- package/templates/scripts/dev.js +275 -0
- package/templates/scripts/dev.sh +1 -244
- package/templates/scripts/prune-smart.js +4 -6
- package/templates/scripts/run.sh +4 -1
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);
|
|
@@ -7166,6 +7255,12 @@ var SCENE_CONFIGS = {
|
|
|
7166
7255
|
buildRequestBody: () => ({ commitID: "" })
|
|
7167
7256
|
}
|
|
7168
7257
|
};
|
|
7258
|
+
var UPLOAD_STATIC_DEFAULTS = {
|
|
7259
|
+
staticDir: "shared/static",
|
|
7260
|
+
tosutilPath: "tosutil",
|
|
7261
|
+
endpoint: "tos-cn-beijing.volces.com",
|
|
7262
|
+
region: "cn-beijing"
|
|
7263
|
+
};
|
|
7169
7264
|
|
|
7170
7265
|
// src/commands/build/api-client.ts
|
|
7171
7266
|
async function genArtifactUploadCredential(appId, body) {
|
|
@@ -7181,17 +7276,20 @@ async function genArtifactUploadCredential(appId, body) {
|
|
|
7181
7276
|
}
|
|
7182
7277
|
async function getDefaultBucketId(appId) {
|
|
7183
7278
|
const client = getHttpClient();
|
|
7184
|
-
const url = `/
|
|
7185
|
-
const response = await client.
|
|
7279
|
+
const url = `/v1/app/${appId}/storage/inner/staticBucket`;
|
|
7280
|
+
const response = await client.post(url, {});
|
|
7186
7281
|
if (!response.ok || response.status !== 200) {
|
|
7187
7282
|
throw new Error(
|
|
7188
|
-
`
|
|
7283
|
+
`getOrCreateStaticBucket \u8BF7\u6C42\u5931\u8D25: ${response.status} ${response.statusText}`
|
|
7189
7284
|
);
|
|
7190
7285
|
}
|
|
7191
7286
|
const data = await response.json();
|
|
7192
|
-
|
|
7287
|
+
if (data.status_code !== "0") {
|
|
7288
|
+
throw new Error(`getOrCreateStaticBucket \u8FD4\u56DE\u5F02\u5E38, status_code: ${data.status_code}`);
|
|
7289
|
+
}
|
|
7290
|
+
const bucketId = data?.data?.bucketID;
|
|
7193
7291
|
if (!bucketId) {
|
|
7194
|
-
throw new Error(`\u672A\u627E\u5230\u5E94\u7528 ${appId} \u7684\
|
|
7292
|
+
throw new Error(`\u672A\u627E\u5230\u5E94\u7528 ${appId} \u7684\u9759\u6001\u8D44\u6E90\u5B58\u50A8\u6876`);
|
|
7195
7293
|
}
|
|
7196
7294
|
return bucketId;
|
|
7197
7295
|
}
|
|
@@ -7253,7 +7351,7 @@ function camelToKebab(str) {
|
|
|
7253
7351
|
// src/commands/build/upload-static.handler.ts
|
|
7254
7352
|
import * as fs25 from "fs";
|
|
7255
7353
|
import * as path21 from "path";
|
|
7256
|
-
import {
|
|
7354
|
+
import { execFileSync } from "child_process";
|
|
7257
7355
|
function readCredentialsFromEnv() {
|
|
7258
7356
|
const uploadPrefix = process.env.STATIC_UPLOAD_PREFIX;
|
|
7259
7357
|
const uploadID = process.env.STATIC_UPLOAD_ID;
|
|
@@ -7271,10 +7369,10 @@ async function uploadStatic(options) {
|
|
|
7271
7369
|
try {
|
|
7272
7370
|
const {
|
|
7273
7371
|
appId,
|
|
7274
|
-
staticDir =
|
|
7275
|
-
tosutilPath =
|
|
7276
|
-
endpoint =
|
|
7277
|
-
region =
|
|
7372
|
+
staticDir = UPLOAD_STATIC_DEFAULTS.staticDir,
|
|
7373
|
+
tosutilPath = UPLOAD_STATIC_DEFAULTS.tosutilPath,
|
|
7374
|
+
endpoint = UPLOAD_STATIC_DEFAULTS.endpoint,
|
|
7375
|
+
region = UPLOAD_STATIC_DEFAULTS.region
|
|
7278
7376
|
} = options;
|
|
7279
7377
|
const resolvedStaticDir = path21.resolve(staticDir);
|
|
7280
7378
|
if (!fs25.existsSync(resolvedStaticDir)) {
|
|
@@ -7285,9 +7383,10 @@ async function uploadStatic(options) {
|
|
|
7285
7383
|
console.error(`${LOG_PREFIX} \u76EE\u5F55\u4E3A\u7A7A: ${resolvedStaticDir}\uFF0C\u8DF3\u8FC7\u4E0A\u4F20`);
|
|
7286
7384
|
return;
|
|
7287
7385
|
}
|
|
7288
|
-
|
|
7386
|
+
const resolvedTosutil = resolveTosutilPath(tosutilPath);
|
|
7387
|
+
if (!resolvedTosutil) {
|
|
7289
7388
|
throw new Error(
|
|
7290
|
-
`tosutil \u4E0D\u5B58\u5728: ${tosutilPath}\u3002\u8BF7\u786E\u4FDD\
|
|
7389
|
+
`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
7390
|
);
|
|
7292
7391
|
}
|
|
7293
7392
|
let uploadPrefix;
|
|
@@ -7310,7 +7409,7 @@ async function uploadStatic(options) {
|
|
|
7310
7409
|
}
|
|
7311
7410
|
console.error(`${LOG_PREFIX} \u4E0A\u4F20\u76EE\u6807: ${uploadPrefix}`);
|
|
7312
7411
|
console.error(`${LOG_PREFIX} \u914D\u7F6E tosutil...`);
|
|
7313
|
-
configureTosutil(
|
|
7412
|
+
configureTosutil(resolvedTosutil, {
|
|
7314
7413
|
endpoint,
|
|
7315
7414
|
region,
|
|
7316
7415
|
accessKeyID,
|
|
@@ -7318,7 +7417,7 @@ async function uploadStatic(options) {
|
|
|
7318
7417
|
sessionToken
|
|
7319
7418
|
});
|
|
7320
7419
|
console.error(`${LOG_PREFIX} \u4E0A\u4F20 ${resolvedStaticDir} -> ${uploadPrefix}`);
|
|
7321
|
-
uploadToTos(
|
|
7420
|
+
uploadToTos(resolvedTosutil, resolvedStaticDir, uploadPrefix);
|
|
7322
7421
|
console.error(`${LOG_PREFIX} tosutil \u4E0A\u4F20\u5B8C\u6210`);
|
|
7323
7422
|
console.error(`${LOG_PREFIX} \u8C03\u7528 callbackStatic (uploadID: ${uploadID})...`);
|
|
7324
7423
|
const callbackResp = await uploadStaticAttachmentCallback(appId, bucketId, { uploadID });
|
|
@@ -7334,6 +7433,17 @@ async function uploadStatic(options) {
|
|
|
7334
7433
|
process.exit(1);
|
|
7335
7434
|
}
|
|
7336
7435
|
}
|
|
7436
|
+
function resolveTosutilPath(tosutilPath) {
|
|
7437
|
+
if (path21.isAbsolute(tosutilPath)) {
|
|
7438
|
+
return fs25.existsSync(tosutilPath) ? tosutilPath : null;
|
|
7439
|
+
}
|
|
7440
|
+
try {
|
|
7441
|
+
const resolved = execFileSync("which", [tosutilPath], { encoding: "utf-8" }).trim();
|
|
7442
|
+
return resolved || null;
|
|
7443
|
+
} catch {
|
|
7444
|
+
return null;
|
|
7445
|
+
}
|
|
7446
|
+
}
|
|
7337
7447
|
async function fetchPreUpload(appId, bucketId) {
|
|
7338
7448
|
const response = await preUploadStaticAttachment(appId, bucketId);
|
|
7339
7449
|
if (response.status_code !== "0") {
|
|
@@ -7350,14 +7460,16 @@ async function fetchPreUpload(appId, bucketId) {
|
|
|
7350
7460
|
}
|
|
7351
7461
|
function configureTosutil(tosutilPath, config) {
|
|
7352
7462
|
const { endpoint, region, accessKeyID, secretAccessKey, sessionToken } = config;
|
|
7353
|
-
|
|
7354
|
-
|
|
7463
|
+
execFileSync(
|
|
7464
|
+
tosutilPath,
|
|
7465
|
+
["config", "-e", endpoint, "-i", accessKeyID, "-k", secretAccessKey, "-t", sessionToken, "-re", region],
|
|
7355
7466
|
{ stdio: "pipe" }
|
|
7356
7467
|
);
|
|
7357
7468
|
}
|
|
7358
7469
|
function uploadToTos(tosutilPath, sourceDir, destUrl) {
|
|
7359
|
-
|
|
7360
|
-
|
|
7470
|
+
execFileSync(
|
|
7471
|
+
tosutilPath,
|
|
7472
|
+
["cp", sourceDir, destUrl, "-r", "-flat", "-j", "5", "-p", "3", "-ps", "10485760", "-f"],
|
|
7361
7473
|
{ stdio: "inherit" }
|
|
7362
7474
|
);
|
|
7363
7475
|
}
|
|
@@ -7374,6 +7486,9 @@ function isDirEmpty(dirPath) {
|
|
|
7374
7486
|
|
|
7375
7487
|
// src/commands/build/pre-upload-static.handler.ts
|
|
7376
7488
|
var LOG_PREFIX2 = "[pre-upload-static]";
|
|
7489
|
+
function shellEscape(value) {
|
|
7490
|
+
return value.replace(/["\\`$]/g, "\\$&");
|
|
7491
|
+
}
|
|
7377
7492
|
async function preUploadStatic(options) {
|
|
7378
7493
|
try {
|
|
7379
7494
|
const { appId } = options;
|
|
@@ -7386,8 +7501,8 @@ async function preUploadStatic(options) {
|
|
|
7386
7501
|
console.error(`${LOG_PREFIX2} preUploadStatic \u8FD4\u56DE\u5F02\u5E38, status_code: ${response.status_code}`);
|
|
7387
7502
|
return;
|
|
7388
7503
|
}
|
|
7389
|
-
const {
|
|
7390
|
-
if (!
|
|
7504
|
+
const { downloadURLPrefix, uploadPrefix, uploadID, uploadCredential } = response.data || {};
|
|
7505
|
+
if (!downloadURLPrefix || !uploadPrefix || !uploadID) {
|
|
7391
7506
|
console.error(`${LOG_PREFIX2} preUploadStatic \u8FD4\u56DE\u6570\u636E\u4E0D\u5B8C\u6574`);
|
|
7392
7507
|
return;
|
|
7393
7508
|
}
|
|
@@ -7395,13 +7510,13 @@ async function preUploadStatic(options) {
|
|
|
7395
7510
|
console.error(`${LOG_PREFIX2} preUploadStatic \u8FD4\u56DE\u7684\u51ED\u8BC1\u5B57\u6BB5\u4E0D\u5B8C\u6574`);
|
|
7396
7511
|
return;
|
|
7397
7512
|
}
|
|
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}"`);
|
|
7513
|
+
console.log(`export STATIC_ASSETS_BASE_URL="${shellEscape(downloadURLPrefix)}"`);
|
|
7514
|
+
console.log(`export STATIC_UPLOAD_PREFIX="${shellEscape(uploadPrefix)}"`);
|
|
7515
|
+
console.log(`export STATIC_UPLOAD_ID="${shellEscape(uploadID)}"`);
|
|
7516
|
+
console.log(`export STATIC_UPLOAD_AK="${shellEscape(uploadCredential.AccessKeyID)}"`);
|
|
7517
|
+
console.log(`export STATIC_UPLOAD_SK="${shellEscape(uploadCredential.SecretAccessKey)}"`);
|
|
7518
|
+
console.log(`export STATIC_UPLOAD_TOKEN="${shellEscape(uploadCredential.SessionToken)}"`);
|
|
7519
|
+
console.log(`export STATIC_UPLOAD_BUCKET_ID="${shellEscape(bucketId)}"`);
|
|
7405
7520
|
console.error(`${LOG_PREFIX2} \u73AF\u5883\u53D8\u91CF\u5DF2\u8F93\u51FA`);
|
|
7406
7521
|
} catch (error) {
|
|
7407
7522
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -7423,7 +7538,7 @@ var uploadStaticCommand = {
|
|
|
7423
7538
|
name: "upload-static",
|
|
7424
7539
|
description: "Upload shared/static files to TOS",
|
|
7425
7540
|
register(program) {
|
|
7426
|
-
program.command(this.name).description(this.description).requiredOption("--app-id <id>", "Application ID").option("--static-dir <dir>", "Static files directory",
|
|
7541
|
+
program.command(this.name).description(this.description).requiredOption("--app-id <id>", "Application ID").option("--static-dir <dir>", "Static files directory", UPLOAD_STATIC_DEFAULTS.staticDir).option("--tosutil-path <path>", "Path to tosutil binary", UPLOAD_STATIC_DEFAULTS.tosutilPath).option("--endpoint <endpoint>", "TOS endpoint", UPLOAD_STATIC_DEFAULTS.endpoint).option("--region <region>", "TOS region", UPLOAD_STATIC_DEFAULTS.region).action(async (options) => {
|
|
7427
7542
|
await uploadStatic(options);
|
|
7428
7543
|
});
|
|
7429
7544
|
}
|
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"]
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
set -euo pipefail
|
|
4
4
|
|
|
5
5
|
ROOT_DIR="$(pwd)"
|
|
6
|
-
|
|
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]
|
|
81
|
+
echo "📦 [4/5] 准备产物"
|
|
82
82
|
STEP_START=$(node -e "console.log(Date.now())")
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
# 拷贝 run.sh 到 dist/(prod 从 dist/ 启动,确保 cwd 一致性)
|
|
85
|
+
cp "$ROOT_DIR/scripts/run.sh" "$DIST_DIR/"
|
|
85
86
|
|
|
86
|
-
# 拷贝
|
|
87
|
-
|
|
87
|
+
# 拷贝 .env 文件(如果存在)
|
|
88
|
+
if [ -f "$ROOT_DIR/.env" ]; then
|
|
89
|
+
cp "$ROOT_DIR/.env" "$DIST_DIR/"
|
|
90
|
+
fi
|
|
88
91
|
|
|
89
|
-
#
|
|
90
|
-
|
|
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 "$
|
|
94
|
-
rm -rf "$
|
|
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 "$
|
|
115
|
-
NODE_MODULES_SIZE=$(du -sh "$
|
|
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
|
+
});
|
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" "$@"
|
|
@@ -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
|
|
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(
|
|
13
|
-
const OUT_PACKAGE_JSON = path.join(
|
|
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
|
|
package/templates/scripts/run.sh
CHANGED