@lark-apaas/fullstack-cli 1.1.16-beta.6 → 1.1.16-beta.8
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 +326 -17
- package/package.json +1 -1
- package/templates/.spark_project +16 -0
- package/templates/nest-cli.json +1 -1
- 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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import
|
|
3
|
-
import
|
|
2
|
+
import fs26 from "fs";
|
|
3
|
+
import path22 from "path";
|
|
4
4
|
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
5
5
|
import { config as dotenvConfig } from "dotenv";
|
|
6
6
|
|
|
@@ -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);
|
|
@@ -4285,7 +4374,7 @@ var PROMPT_PATTERNS = [
|
|
|
4285
4374
|
{ pattern: /proceed\?/i, answer: "y\n" }
|
|
4286
4375
|
];
|
|
4287
4376
|
async function executeShadcnAdd(registryItemPath) {
|
|
4288
|
-
return new Promise((
|
|
4377
|
+
return new Promise((resolve2) => {
|
|
4289
4378
|
let output = "";
|
|
4290
4379
|
const args = ["--yes", "shadcn@3.8.2", "add", registryItemPath];
|
|
4291
4380
|
const ptyProcess = pty.spawn("npx", args, {
|
|
@@ -4311,7 +4400,7 @@ async function executeShadcnAdd(registryItemPath) {
|
|
|
4311
4400
|
});
|
|
4312
4401
|
const timeoutId = setTimeout(() => {
|
|
4313
4402
|
ptyProcess.kill();
|
|
4314
|
-
|
|
4403
|
+
resolve2({
|
|
4315
4404
|
success: false,
|
|
4316
4405
|
files: [],
|
|
4317
4406
|
error: "\u6267\u884C\u8D85\u65F6"
|
|
@@ -4322,7 +4411,7 @@ async function executeShadcnAdd(registryItemPath) {
|
|
|
4322
4411
|
const success = exitCode === 0;
|
|
4323
4412
|
const filePaths = parseOutput(output);
|
|
4324
4413
|
const files = filePaths.map(toFileInfo);
|
|
4325
|
-
|
|
4414
|
+
resolve2({
|
|
4326
4415
|
success,
|
|
4327
4416
|
files,
|
|
4328
4417
|
error: success ? void 0 : output || `Process exited with code ${exitCode}`
|
|
@@ -4333,12 +4422,12 @@ async function executeShadcnAdd(registryItemPath) {
|
|
|
4333
4422
|
|
|
4334
4423
|
// src/commands/component/add.handler.ts
|
|
4335
4424
|
function runActionPluginInit() {
|
|
4336
|
-
return new Promise((
|
|
4425
|
+
return new Promise((resolve2) => {
|
|
4337
4426
|
execFile("fullstack-cli", ["action-plugin", "init"], { cwd: process.cwd(), stdio: "ignore" }, (error) => {
|
|
4338
4427
|
if (error) {
|
|
4339
4428
|
debug("action-plugin init \u5931\u8D25: %s", error.message);
|
|
4340
4429
|
}
|
|
4341
|
-
|
|
4430
|
+
resolve2();
|
|
4342
4431
|
});
|
|
4343
4432
|
});
|
|
4344
4433
|
}
|
|
@@ -7174,7 +7263,45 @@ async function genArtifactUploadCredential(appId, body) {
|
|
|
7174
7263
|
const response = await client.post(url, body);
|
|
7175
7264
|
if (!response.ok || response.status !== 200) {
|
|
7176
7265
|
throw new Error(
|
|
7177
|
-
`
|
|
7266
|
+
`gen_artifact_upload_credential \u8BF7\u6C42\u5931\u8D25: ${response.status} ${response.statusText}`
|
|
7267
|
+
);
|
|
7268
|
+
}
|
|
7269
|
+
return response.json();
|
|
7270
|
+
}
|
|
7271
|
+
async function getDefaultBucketId(appId) {
|
|
7272
|
+
const client = getHttpClient();
|
|
7273
|
+
const url = `/b/${appId}/get_published_v2`;
|
|
7274
|
+
const response = await client.get(url);
|
|
7275
|
+
if (!response.ok || response.status !== 200) {
|
|
7276
|
+
throw new Error(
|
|
7277
|
+
`get_published_v2 \u8BF7\u6C42\u5931\u8D25: ${response.status} ${response.statusText}`
|
|
7278
|
+
);
|
|
7279
|
+
}
|
|
7280
|
+
const data = await response.json();
|
|
7281
|
+
const bucketId = data?.data?.app_runtime_extra?.bucket?.default_bucket_id;
|
|
7282
|
+
if (!bucketId) {
|
|
7283
|
+
throw new Error(`\u672A\u627E\u5230\u5E94\u7528 ${appId} \u7684\u9ED8\u8BA4\u5B58\u50A8\u6876`);
|
|
7284
|
+
}
|
|
7285
|
+
return bucketId;
|
|
7286
|
+
}
|
|
7287
|
+
async function preUploadStaticAttachment(appId, bucketId) {
|
|
7288
|
+
const client = getHttpClient();
|
|
7289
|
+
const url = `/v1/app/${appId}/storage/bucket/${bucketId}/preUploadStatic`;
|
|
7290
|
+
const response = await client.post(url, {});
|
|
7291
|
+
if (!response.ok || response.status !== 200) {
|
|
7292
|
+
throw new Error(
|
|
7293
|
+
`preUploadStatic \u8BF7\u6C42\u5931\u8D25: ${response.status} ${response.statusText}`
|
|
7294
|
+
);
|
|
7295
|
+
}
|
|
7296
|
+
return response.json();
|
|
7297
|
+
}
|
|
7298
|
+
async function uploadStaticAttachmentCallback(appId, bucketId, body) {
|
|
7299
|
+
const client = getHttpClient();
|
|
7300
|
+
const url = `/v1/app/${appId}/storage/bucket/${bucketId}/object/callbackStatic`;
|
|
7301
|
+
const response = await client.post(url, body);
|
|
7302
|
+
if (!response.ok || response.status !== 200) {
|
|
7303
|
+
throw new Error(
|
|
7304
|
+
`callbackStatic \u8BF7\u6C42\u5931\u8D25: ${response.status} ${response.statusText}`
|
|
7178
7305
|
);
|
|
7179
7306
|
}
|
|
7180
7307
|
return response.json();
|
|
@@ -7212,6 +7339,170 @@ function camelToKebab(str) {
|
|
|
7212
7339
|
return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
|
7213
7340
|
}
|
|
7214
7341
|
|
|
7342
|
+
// src/commands/build/upload-static.handler.ts
|
|
7343
|
+
import * as fs25 from "fs";
|
|
7344
|
+
import * as path21 from "path";
|
|
7345
|
+
import { execFileSync } from "child_process";
|
|
7346
|
+
function readCredentialsFromEnv() {
|
|
7347
|
+
const uploadPrefix = process.env.STATIC_UPLOAD_PREFIX;
|
|
7348
|
+
const uploadID = process.env.STATIC_UPLOAD_ID;
|
|
7349
|
+
const bucketId = process.env.STATIC_UPLOAD_BUCKET_ID;
|
|
7350
|
+
const accessKeyID = process.env.STATIC_UPLOAD_AK;
|
|
7351
|
+
const secretAccessKey = process.env.STATIC_UPLOAD_SK;
|
|
7352
|
+
const sessionToken = process.env.STATIC_UPLOAD_TOKEN;
|
|
7353
|
+
if (!uploadPrefix || !uploadID || !bucketId || !accessKeyID || !secretAccessKey || !sessionToken) {
|
|
7354
|
+
return null;
|
|
7355
|
+
}
|
|
7356
|
+
return { uploadPrefix, uploadID, bucketId, accessKeyID, secretAccessKey, sessionToken };
|
|
7357
|
+
}
|
|
7358
|
+
var LOG_PREFIX = "[upload-static]";
|
|
7359
|
+
async function uploadStatic(options) {
|
|
7360
|
+
try {
|
|
7361
|
+
const {
|
|
7362
|
+
appId,
|
|
7363
|
+
staticDir = "shared/static",
|
|
7364
|
+
tosutilPath = "/workspace/tosutil",
|
|
7365
|
+
endpoint = "tos-cn-beijing.volces.com",
|
|
7366
|
+
region = "cn-beijing"
|
|
7367
|
+
} = options;
|
|
7368
|
+
const resolvedStaticDir = path21.resolve(staticDir);
|
|
7369
|
+
if (!fs25.existsSync(resolvedStaticDir)) {
|
|
7370
|
+
console.error(`${LOG_PREFIX} \u76EE\u5F55\u4E0D\u5B58\u5728: ${resolvedStaticDir}\uFF0C\u8DF3\u8FC7\u4E0A\u4F20`);
|
|
7371
|
+
return;
|
|
7372
|
+
}
|
|
7373
|
+
if (isDirEmpty(resolvedStaticDir)) {
|
|
7374
|
+
console.error(`${LOG_PREFIX} \u76EE\u5F55\u4E3A\u7A7A: ${resolvedStaticDir}\uFF0C\u8DF3\u8FC7\u4E0A\u4F20`);
|
|
7375
|
+
return;
|
|
7376
|
+
}
|
|
7377
|
+
if (!fs25.existsSync(tosutilPath)) {
|
|
7378
|
+
throw new Error(
|
|
7379
|
+
`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`
|
|
7380
|
+
);
|
|
7381
|
+
}
|
|
7382
|
+
let uploadPrefix;
|
|
7383
|
+
let uploadID;
|
|
7384
|
+
let bucketId;
|
|
7385
|
+
let accessKeyID;
|
|
7386
|
+
let secretAccessKey;
|
|
7387
|
+
let sessionToken;
|
|
7388
|
+
const envCredentials = readCredentialsFromEnv();
|
|
7389
|
+
if (envCredentials) {
|
|
7390
|
+
console.error(`${LOG_PREFIX} \u4F7F\u7528\u73AF\u5883\u53D8\u91CF\u4E2D\u7684\u4E0A\u4F20\u51ED\u8BC1`);
|
|
7391
|
+
({ uploadPrefix, uploadID, bucketId, accessKeyID, secretAccessKey, sessionToken } = envCredentials);
|
|
7392
|
+
} else {
|
|
7393
|
+
console.error(`${LOG_PREFIX} \u73AF\u5883\u53D8\u91CF\u672A\u8BBE\u7F6E\uFF0C\u8C03\u7528 preUploadStatic...`);
|
|
7394
|
+
bucketId = await resolveBucketId(appId);
|
|
7395
|
+
const preUploadResp = await fetchPreUpload(appId, bucketId);
|
|
7396
|
+
const { uploadCredential } = preUploadResp.data;
|
|
7397
|
+
({ uploadPrefix, uploadID } = preUploadResp.data);
|
|
7398
|
+
({ AccessKeyID: accessKeyID, SecretAccessKey: secretAccessKey, SessionToken: sessionToken } = uploadCredential);
|
|
7399
|
+
}
|
|
7400
|
+
console.error(`${LOG_PREFIX} \u4E0A\u4F20\u76EE\u6807: ${uploadPrefix}`);
|
|
7401
|
+
console.error(`${LOG_PREFIX} \u914D\u7F6E tosutil...`);
|
|
7402
|
+
configureTosutil(tosutilPath, {
|
|
7403
|
+
endpoint,
|
|
7404
|
+
region,
|
|
7405
|
+
accessKeyID,
|
|
7406
|
+
secretAccessKey,
|
|
7407
|
+
sessionToken
|
|
7408
|
+
});
|
|
7409
|
+
console.error(`${LOG_PREFIX} \u4E0A\u4F20 ${resolvedStaticDir} -> ${uploadPrefix}`);
|
|
7410
|
+
uploadToTos(tosutilPath, resolvedStaticDir, uploadPrefix);
|
|
7411
|
+
console.error(`${LOG_PREFIX} tosutil \u4E0A\u4F20\u5B8C\u6210`);
|
|
7412
|
+
console.error(`${LOG_PREFIX} \u8C03\u7528 callbackStatic (uploadID: ${uploadID})...`);
|
|
7413
|
+
const callbackResp = await uploadStaticAttachmentCallback(appId, bucketId, { uploadID });
|
|
7414
|
+
if (callbackResp.status_code !== "0") {
|
|
7415
|
+
throw new Error(`callbackStatic \u8FD4\u56DE\u5F02\u5E38, status_code: ${callbackResp.status_code}`);
|
|
7416
|
+
}
|
|
7417
|
+
const attachments = callbackResp.data?.attachments || [];
|
|
7418
|
+
console.error(`${LOG_PREFIX} \u4E0A\u4F20\u5B8C\u6210\uFF0C\u5171 ${attachments.length} \u4E2A\u6587\u4EF6`);
|
|
7419
|
+
console.log(JSON.stringify(callbackResp));
|
|
7420
|
+
} catch (error) {
|
|
7421
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7422
|
+
console.error(`${LOG_PREFIX} Error: ${message}`);
|
|
7423
|
+
process.exit(1);
|
|
7424
|
+
}
|
|
7425
|
+
}
|
|
7426
|
+
async function fetchPreUpload(appId, bucketId) {
|
|
7427
|
+
const response = await preUploadStaticAttachment(appId, bucketId);
|
|
7428
|
+
if (response.status_code !== "0") {
|
|
7429
|
+
throw new Error(`preUploadStatic \u8FD4\u56DE\u5F02\u5E38, status_code: ${response.status_code}`);
|
|
7430
|
+
}
|
|
7431
|
+
const { uploadPrefix, uploadID, uploadCredential } = response.data || {};
|
|
7432
|
+
if (!uploadPrefix || !uploadID) {
|
|
7433
|
+
throw new Error("preUploadStatic \u8FD4\u56DE\u6570\u636E\u4E0D\u5B8C\u6574\uFF0C\u7F3A\u5C11 uploadPrefix \u6216 uploadID");
|
|
7434
|
+
}
|
|
7435
|
+
if (!uploadCredential?.AccessKeyID || !uploadCredential?.SecretAccessKey || !uploadCredential?.SessionToken) {
|
|
7436
|
+
throw new Error("preUploadStatic \u8FD4\u56DE\u7684\u51ED\u8BC1\u5B57\u6BB5\u4E0D\u5B8C\u6574");
|
|
7437
|
+
}
|
|
7438
|
+
return response;
|
|
7439
|
+
}
|
|
7440
|
+
function configureTosutil(tosutilPath, config) {
|
|
7441
|
+
const { endpoint, region, accessKeyID, secretAccessKey, sessionToken } = config;
|
|
7442
|
+
execFileSync(
|
|
7443
|
+
tosutilPath,
|
|
7444
|
+
["config", "-e", endpoint, "-i", accessKeyID, "-k", secretAccessKey, "-t", sessionToken, "-re", region],
|
|
7445
|
+
{ stdio: "pipe" }
|
|
7446
|
+
);
|
|
7447
|
+
}
|
|
7448
|
+
function uploadToTos(tosutilPath, sourceDir, destUrl) {
|
|
7449
|
+
execFileSync(
|
|
7450
|
+
tosutilPath,
|
|
7451
|
+
["cp", sourceDir, destUrl, "-r", "-flat", "-j", "5", "-p", "3", "-ps", "10485760", "-f"],
|
|
7452
|
+
{ stdio: "inherit" }
|
|
7453
|
+
);
|
|
7454
|
+
}
|
|
7455
|
+
async function resolveBucketId(appId) {
|
|
7456
|
+
console.error(`${LOG_PREFIX} \u83B7\u53D6\u9ED8\u8BA4\u5B58\u50A8\u6876...`);
|
|
7457
|
+
const bucketId = await getDefaultBucketId(appId);
|
|
7458
|
+
console.error(`${LOG_PREFIX} \u9ED8\u8BA4\u5B58\u50A8\u6876: ${bucketId}`);
|
|
7459
|
+
return bucketId;
|
|
7460
|
+
}
|
|
7461
|
+
function isDirEmpty(dirPath) {
|
|
7462
|
+
const entries = fs25.readdirSync(dirPath);
|
|
7463
|
+
return entries.length === 0;
|
|
7464
|
+
}
|
|
7465
|
+
|
|
7466
|
+
// src/commands/build/pre-upload-static.handler.ts
|
|
7467
|
+
var LOG_PREFIX2 = "[pre-upload-static]";
|
|
7468
|
+
function shellEscape(value) {
|
|
7469
|
+
return value.replace(/["\\`$]/g, "\\$&");
|
|
7470
|
+
}
|
|
7471
|
+
async function preUploadStatic(options) {
|
|
7472
|
+
try {
|
|
7473
|
+
const { appId } = options;
|
|
7474
|
+
console.error(`${LOG_PREFIX2} \u83B7\u53D6\u9ED8\u8BA4\u5B58\u50A8\u6876...`);
|
|
7475
|
+
const bucketId = await getDefaultBucketId(appId);
|
|
7476
|
+
console.error(`${LOG_PREFIX2} \u9ED8\u8BA4\u5B58\u50A8\u6876: ${bucketId}`);
|
|
7477
|
+
console.error(`${LOG_PREFIX2} \u8C03\u7528 preUploadStatic...`);
|
|
7478
|
+
const response = await preUploadStaticAttachment(appId, bucketId);
|
|
7479
|
+
if (response.status_code !== "0") {
|
|
7480
|
+
console.error(`${LOG_PREFIX2} preUploadStatic \u8FD4\u56DE\u5F02\u5E38, status_code: ${response.status_code}`);
|
|
7481
|
+
return;
|
|
7482
|
+
}
|
|
7483
|
+
const { downloadURLPrefix, uploadPrefix, uploadID, uploadCredential } = response.data || {};
|
|
7484
|
+
if (!downloadURLPrefix || !uploadPrefix || !uploadID) {
|
|
7485
|
+
console.error(`${LOG_PREFIX2} preUploadStatic \u8FD4\u56DE\u6570\u636E\u4E0D\u5B8C\u6574`);
|
|
7486
|
+
return;
|
|
7487
|
+
}
|
|
7488
|
+
if (!uploadCredential?.AccessKeyID || !uploadCredential?.SecretAccessKey || !uploadCredential?.SessionToken) {
|
|
7489
|
+
console.error(`${LOG_PREFIX2} preUploadStatic \u8FD4\u56DE\u7684\u51ED\u8BC1\u5B57\u6BB5\u4E0D\u5B8C\u6574`);
|
|
7490
|
+
return;
|
|
7491
|
+
}
|
|
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)}"`);
|
|
7499
|
+
console.error(`${LOG_PREFIX2} \u73AF\u5883\u53D8\u91CF\u5DF2\u8F93\u51FA`);
|
|
7500
|
+
} catch (error) {
|
|
7501
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7502
|
+
console.error(`${LOG_PREFIX2} Warning: ${message}`);
|
|
7503
|
+
}
|
|
7504
|
+
}
|
|
7505
|
+
|
|
7215
7506
|
// src/commands/build/index.ts
|
|
7216
7507
|
var getTokenCommand = {
|
|
7217
7508
|
name: "get-token",
|
|
@@ -7222,10 +7513,28 @@ var getTokenCommand = {
|
|
|
7222
7513
|
});
|
|
7223
7514
|
}
|
|
7224
7515
|
};
|
|
7516
|
+
var uploadStaticCommand = {
|
|
7517
|
+
name: "upload-static",
|
|
7518
|
+
description: "Upload shared/static files to TOS",
|
|
7519
|
+
register(program) {
|
|
7520
|
+
program.command(this.name).description(this.description).requiredOption("--app-id <id>", "Application ID").option("--static-dir <dir>", "Static files directory", "shared/static").option("--tosutil-path <path>", "Path to tosutil binary", "/workspace/tosutil").option("--endpoint <endpoint>", "TOS endpoint", "tos-cn-beijing.volces.com").option("--region <region>", "TOS region", "cn-beijing").action(async (options) => {
|
|
7521
|
+
await uploadStatic(options);
|
|
7522
|
+
});
|
|
7523
|
+
}
|
|
7524
|
+
};
|
|
7525
|
+
var preUploadStaticCommand = {
|
|
7526
|
+
name: "pre-upload-static",
|
|
7527
|
+
description: "Get TOS upload info and output as env vars for build.sh eval",
|
|
7528
|
+
register(program) {
|
|
7529
|
+
program.command(this.name).description(this.description).requiredOption("--app-id <id>", "Application ID").action(async (options) => {
|
|
7530
|
+
await preUploadStatic(options);
|
|
7531
|
+
});
|
|
7532
|
+
}
|
|
7533
|
+
};
|
|
7225
7534
|
var buildCommandGroup = {
|
|
7226
7535
|
name: "build",
|
|
7227
7536
|
description: "Build related commands",
|
|
7228
|
-
commands: [getTokenCommand]
|
|
7537
|
+
commands: [getTokenCommand, uploadStaticCommand, preUploadStaticCommand]
|
|
7229
7538
|
};
|
|
7230
7539
|
|
|
7231
7540
|
// src/commands/index.ts
|
|
@@ -7242,12 +7551,12 @@ var commands = [
|
|
|
7242
7551
|
];
|
|
7243
7552
|
|
|
7244
7553
|
// src/index.ts
|
|
7245
|
-
var envPath =
|
|
7246
|
-
if (
|
|
7554
|
+
var envPath = path22.join(process.cwd(), ".env");
|
|
7555
|
+
if (fs26.existsSync(envPath)) {
|
|
7247
7556
|
dotenvConfig({ path: envPath });
|
|
7248
7557
|
}
|
|
7249
|
-
var __dirname =
|
|
7250
|
-
var pkg = JSON.parse(
|
|
7558
|
+
var __dirname = path22.dirname(fileURLToPath5(import.meta.url));
|
|
7559
|
+
var pkg = JSON.parse(fs26.readFileSync(path22.join(__dirname, "../package.json"), "utf-8"));
|
|
7251
7560
|
var cli = new FullstackCLI(pkg.version);
|
|
7252
7561
|
cli.useAll(commands);
|
|
7253
7562
|
cli.run();
|
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"]
|
package/templates/nest-cli.json
CHANGED
|
@@ -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