@lark-apaas/fullstack-cli 1.1.16-beta.0 → 1.1.16-beta.10
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/README.md +41 -3
- package/dist/gen-dbschema-template/types.ts +54 -0
- package/dist/index.js +2160 -323
- package/package.json +6 -1
- package/templates/.spark_project +16 -0
- package/templates/drizzle.config.ts +55 -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 -240
- package/templates/scripts/prune-smart.js +4 -6
- package/templates/scripts/run.sh +4 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lark-apaas/fullstack-cli",
|
|
3
|
-
"version": "1.1.16-beta.
|
|
3
|
+
"version": "1.1.16-beta.10",
|
|
4
4
|
"description": "CLI tool for fullstack template management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -32,18 +32,23 @@
|
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@lark-apaas/http-client": "^0.1.2",
|
|
35
|
+
"@lydell/node-pty": "1.1.0",
|
|
35
36
|
"@vercel/nft": "^0.30.3",
|
|
36
37
|
"commander": "^13.0.0",
|
|
38
|
+
"debug": "^4.4.3",
|
|
37
39
|
"dotenv": "^16.0.0",
|
|
38
40
|
"drizzle-kit": "0.31.5",
|
|
39
41
|
"drizzle-orm": "0.44.6",
|
|
42
|
+
"es-toolkit": "^1.44.0",
|
|
40
43
|
"inflection": "^3.0.2",
|
|
41
44
|
"pinyin-pro": "^3.27.0",
|
|
42
45
|
"postgres": "^3.4.3",
|
|
46
|
+
"shadcn": "3.8.2",
|
|
43
47
|
"ts-morph": "^27.0.0",
|
|
44
48
|
"zod-to-json-schema": "^3.24.1"
|
|
45
49
|
},
|
|
46
50
|
"devDependencies": {
|
|
51
|
+
"@types/debug": "^4",
|
|
47
52
|
"@types/node": "^22.0.0",
|
|
48
53
|
"tsup": "^8.3.5",
|
|
49
54
|
"typescript": "^5.9.2",
|
|
@@ -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"]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { defineConfig, Config } from 'drizzle-kit';
|
|
2
|
+
require('dotenv').config();
|
|
3
|
+
|
|
4
|
+
const outputDir = process.env.__DRIZZLE_OUT_DIR__ || './server/database/.introspect';
|
|
5
|
+
const schemaPath = process.env.__DRIZZLE_SCHEMA_PATH__ || './server/database/schema.ts';
|
|
6
|
+
|
|
7
|
+
const parsedUrl = new URL(process.env.SUDA_DATABASE_URL || '');
|
|
8
|
+
|
|
9
|
+
const envSchemaFilter = process.env.DRIZZLE_SCHEMA_FILTER;
|
|
10
|
+
const urlSchemaFilter = parsedUrl.searchParams.get('schema');
|
|
11
|
+
|
|
12
|
+
const schemaFilter = (envSchemaFilter ?? urlSchemaFilter ?? '')
|
|
13
|
+
.split(',')
|
|
14
|
+
.map((s) => s.trim())
|
|
15
|
+
.filter(Boolean);
|
|
16
|
+
|
|
17
|
+
parsedUrl.searchParams.delete('schema'); // 移除schema参数,避免 drizzle-kit 解析错误
|
|
18
|
+
|
|
19
|
+
// 默认排除的系统对象(PostgreSQL 扩展和系统视图)
|
|
20
|
+
// 这些对象在 drizzle-kit introspect 时可能导致无效的 JS 代码生成
|
|
21
|
+
const SYSTEM_OBJECTS_EXCLUSIONS = [
|
|
22
|
+
'!spatial_ref_sys', // PostGIS 空间参考系统表
|
|
23
|
+
'!geography_columns', // PostGIS 地理列视图
|
|
24
|
+
'!geometry_columns', // PostGIS 几何列视图
|
|
25
|
+
'!raster_columns', // PostGIS 栅格列视图
|
|
26
|
+
'!raster_overviews', // PostGIS 栅格概览视图
|
|
27
|
+
'!pg_stat_statements', // pg_stat_statements 扩展
|
|
28
|
+
'!pg_stat_statements_info', // pg_stat_statements 扩展
|
|
29
|
+
'!part_config', // pg_partman 分区配置表
|
|
30
|
+
'!part_config_sub', // pg_partman 子分区配置表
|
|
31
|
+
'!table_privs', // 系统权限视图
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const envTablesFilter = process.env.DRIZZLE_TABLES_FILTER;
|
|
35
|
+
const userTablesFilter = (envTablesFilter ?? '*')
|
|
36
|
+
.split(',')
|
|
37
|
+
.map((s) => s.trim())
|
|
38
|
+
.filter(Boolean);
|
|
39
|
+
|
|
40
|
+
// 合并用户过滤器和系统对象排除
|
|
41
|
+
// 用户可以通过设置 DRIZZLE_TABLES_FILTER 来覆盖(如果需要包含某些系统对象)
|
|
42
|
+
const tablesFilter = [...userTablesFilter, ...SYSTEM_OBJECTS_EXCLUSIONS];
|
|
43
|
+
|
|
44
|
+
const config:Config = {
|
|
45
|
+
schema: schemaPath,
|
|
46
|
+
out: outputDir,
|
|
47
|
+
tablesFilter,
|
|
48
|
+
schemaFilter,
|
|
49
|
+
dialect: 'postgresql',
|
|
50
|
+
dbCredentials: {
|
|
51
|
+
url: parsedUrl.toString(),
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export default defineConfig(config);
|
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,241 +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
|
-
# Log event to dev.log with timestamp
|
|
25
|
-
log_event() {
|
|
26
|
-
local level=$1
|
|
27
|
-
local process_name=$2
|
|
28
|
-
local message=$3
|
|
29
|
-
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] [${level}] [${process_name}] ${message}"
|
|
30
|
-
echo "$msg"
|
|
31
|
-
echo "$msg" >> "${DEV_LOG}"
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
# Check if PID is valid (positive integer and process exists)
|
|
35
|
-
is_valid_pid() {
|
|
36
|
-
local pid=$1
|
|
37
|
-
[[ -n "$pid" ]] && [[ "$pid" =~ ^[0-9]+$ ]] && [[ "$pid" -gt 0 ]] && kill -0 "$pid" 2>/dev/null
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
# Check if parent process is still alive
|
|
41
|
-
is_parent_alive() {
|
|
42
|
-
kill -0 "$PARENT_PID" 2>/dev/null
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
# Check if should stop (parent exited or stop flag exists)
|
|
46
|
-
should_stop() {
|
|
47
|
-
[[ -f "$STOP_FLAG_FILE" ]] || ! is_parent_alive
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
# Kill entire process tree (process and all descendants)
|
|
51
|
-
kill_tree() {
|
|
52
|
-
local pid=$1
|
|
53
|
-
local signal=${2:-TERM}
|
|
54
|
-
|
|
55
|
-
# Get all descendant PIDs
|
|
56
|
-
local children
|
|
57
|
-
children=$(pgrep -P "$pid" 2>/dev/null) || true
|
|
58
|
-
|
|
59
|
-
# Recursively kill children first
|
|
60
|
-
for child in $children; do
|
|
61
|
-
kill_tree "$child" "$signal"
|
|
62
|
-
done
|
|
63
|
-
|
|
64
|
-
# Kill the process itself
|
|
65
|
-
if kill -0 "$pid" 2>/dev/null; then
|
|
66
|
-
kill -"$signal" "$pid" 2>/dev/null || true
|
|
67
|
-
fi
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
# Start a process with supervision
|
|
71
|
-
# $1: name
|
|
72
|
-
# $2: command
|
|
73
|
-
# $3: cleanup port for orphan processes (optional)
|
|
74
|
-
start_supervised_process() {
|
|
75
|
-
local name=$1
|
|
76
|
-
local cmd=$2
|
|
77
|
-
local cleanup_port=${3:-""}
|
|
78
|
-
local log_file="${LOG_DIR}/${name}.std.log"
|
|
79
|
-
|
|
80
|
-
(
|
|
81
|
-
local restart_count=0
|
|
82
|
-
local child_pid=""
|
|
83
|
-
local max_delay=60 # Maximum delay in seconds
|
|
84
|
-
|
|
85
|
-
# Handle signals to kill child process tree
|
|
86
|
-
trap 'if [[ -n "$child_pid" ]]; then kill_tree "$child_pid" TERM; fi' TERM INT
|
|
87
|
-
|
|
88
|
-
while true; do
|
|
89
|
-
# Check if we should stop (parent exited or stop flag)
|
|
90
|
-
if should_stop; then
|
|
91
|
-
log_event "INFO" "$name" "Process stopped (parent exited or user requested)"
|
|
92
|
-
break
|
|
93
|
-
fi
|
|
94
|
-
|
|
95
|
-
# Start command in background and capture output with timestamps
|
|
96
|
-
eval "$cmd" > >(
|
|
97
|
-
while IFS= read -r line; do
|
|
98
|
-
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] [${name}] ${line}"
|
|
99
|
-
echo "$msg"
|
|
100
|
-
echo "$msg" >> "$log_file"
|
|
101
|
-
done
|
|
102
|
-
) 2>&1 &
|
|
103
|
-
child_pid=$!
|
|
104
|
-
|
|
105
|
-
log_event "INFO" "$name" "Process started (PID: ${child_pid}): ${cmd}"
|
|
106
|
-
|
|
107
|
-
# Wait for child to exit
|
|
108
|
-
set +e
|
|
109
|
-
wait "$child_pid"
|
|
110
|
-
exit_code=$?
|
|
111
|
-
set -e
|
|
112
|
-
|
|
113
|
-
# Kill entire process tree to avoid orphans
|
|
114
|
-
if [[ -n "$child_pid" ]]; then
|
|
115
|
-
kill_tree "$child_pid" TERM
|
|
116
|
-
sleep 0.5
|
|
117
|
-
kill_tree "$child_pid" KILL
|
|
118
|
-
fi
|
|
119
|
-
child_pid=""
|
|
120
|
-
|
|
121
|
-
# Cleanup orphan processes by port (for processes that escaped kill_tree)
|
|
122
|
-
if [[ -n "$cleanup_port" ]]; then
|
|
123
|
-
local orphan_pids
|
|
124
|
-
orphan_pids=$(lsof -ti ":${cleanup_port}" 2>/dev/null) || true
|
|
125
|
-
if [[ -n "$orphan_pids" ]]; then
|
|
126
|
-
log_event "WARN" "$name" "Killing orphan processes on port ${cleanup_port}: $(echo $orphan_pids | tr '\n' ' ')"
|
|
127
|
-
echo "$orphan_pids" | xargs kill -9 2>/dev/null || true
|
|
128
|
-
sleep 0.5
|
|
129
|
-
fi
|
|
130
|
-
fi
|
|
131
|
-
|
|
132
|
-
# Check if we should stop (parent exited or stop flag)
|
|
133
|
-
if should_stop; then
|
|
134
|
-
log_event "INFO" "$name" "Process stopped (parent exited or user requested)"
|
|
135
|
-
break
|
|
136
|
-
fi
|
|
137
|
-
|
|
138
|
-
# Process exited unexpectedly, restart
|
|
139
|
-
restart_count=$((restart_count + 1))
|
|
140
|
-
|
|
141
|
-
if [[ $restart_count -ge $MAX_RESTART_COUNT ]]; then
|
|
142
|
-
log_event "ERROR" "$name" "Max restart count (${MAX_RESTART_COUNT}) reached, giving up"
|
|
143
|
-
break
|
|
144
|
-
fi
|
|
145
|
-
|
|
146
|
-
# Exponential backoff: delay = RESTART_DELAY * 2^(restart_count-1), capped at max_delay
|
|
147
|
-
local delay=$((RESTART_DELAY * (1 << (restart_count - 1))))
|
|
148
|
-
if [[ $delay -gt $max_delay ]]; then
|
|
149
|
-
delay=$max_delay
|
|
150
|
-
fi
|
|
151
|
-
|
|
152
|
-
log_event "WARN" "$name" "Process exited with code ${exit_code}, restarting (${restart_count}/${MAX_RESTART_COUNT}) in ${delay}s..."
|
|
153
|
-
sleep "$delay"
|
|
154
|
-
done
|
|
155
|
-
) &
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
# Cleanup function
|
|
159
|
-
cleanup() {
|
|
160
|
-
# Prevent multiple cleanup calls
|
|
161
|
-
if [[ "$CLEANUP_DONE" == "true" ]]; then
|
|
162
|
-
return
|
|
163
|
-
fi
|
|
164
|
-
CLEANUP_DONE=true
|
|
165
|
-
|
|
166
|
-
log_event "INFO" "main" "Shutting down all processes..."
|
|
167
|
-
|
|
168
|
-
# Create stop flag to signal child processes
|
|
169
|
-
touch "$STOP_FLAG_FILE"
|
|
170
|
-
|
|
171
|
-
# Kill entire process trees (TERM first)
|
|
172
|
-
for pid in $SERVER_PID $CLIENT_PID; do
|
|
173
|
-
if is_valid_pid "$pid"; then
|
|
174
|
-
log_event "INFO" "main" "Stopping process tree (PID: ${pid})"
|
|
175
|
-
kill_tree "$pid" TERM
|
|
176
|
-
fi
|
|
177
|
-
done
|
|
178
|
-
|
|
179
|
-
# Kill any remaining background jobs
|
|
180
|
-
local bg_pids
|
|
181
|
-
bg_pids=$(jobs -p 2>/dev/null) || true
|
|
182
|
-
if [[ -n "$bg_pids" ]]; then
|
|
183
|
-
for pid in $bg_pids; do
|
|
184
|
-
kill_tree "$pid" TERM
|
|
185
|
-
done
|
|
186
|
-
fi
|
|
187
|
-
|
|
188
|
-
# Wait for processes to terminate
|
|
189
|
-
sleep 1
|
|
190
|
-
|
|
191
|
-
# Force kill if still running
|
|
192
|
-
for pid in $SERVER_PID $CLIENT_PID; do
|
|
193
|
-
if is_valid_pid "$pid"; then
|
|
194
|
-
log_event "WARN" "main" "Force killing process tree (PID: ${pid})"
|
|
195
|
-
kill_tree "$pid" KILL
|
|
196
|
-
fi
|
|
197
|
-
done
|
|
198
|
-
|
|
199
|
-
# Cleanup stop flag
|
|
200
|
-
rm -f "$STOP_FLAG_FILE"
|
|
201
|
-
|
|
202
|
-
log_event "INFO" "main" "All processes stopped"
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
# Set up signal handlers
|
|
206
|
-
trap cleanup EXIT INT TERM HUP
|
|
207
|
-
|
|
208
|
-
# Remove any stale stop flag
|
|
209
|
-
rm -f "$STOP_FLAG_FILE"
|
|
210
|
-
|
|
211
|
-
# Initialize dev.log
|
|
212
|
-
echo "" >> "${DEV_LOG}"
|
|
213
|
-
log_event "INFO" "main" "========== Dev session started =========="
|
|
214
|
-
|
|
215
|
-
# Initialize action plugins before starting dev servers
|
|
216
|
-
echo "🔌 Initializing action plugins..."
|
|
217
|
-
if fullstack-cli action-plugin init; then
|
|
218
|
-
echo "✅ Action plugins initialized"
|
|
219
|
-
else
|
|
220
|
-
echo "⚠️ Action plugin initialization failed, continuing anyway..."
|
|
221
|
-
fi
|
|
222
|
-
echo ""
|
|
223
|
-
|
|
224
|
-
# Start server (cleanup orphan processes on SERVER_PORT)
|
|
225
|
-
start_supervised_process "server" "npm run dev:server" "${SERVER_PORT:-3000}"
|
|
226
|
-
SERVER_PID=$!
|
|
227
|
-
log_event "INFO" "server" "Supervisor started with PID ${SERVER_PID}"
|
|
228
|
-
|
|
229
|
-
# Start client (cleanup orphan processes on CLIENT_DEV_PORT)
|
|
230
|
-
start_supervised_process "client" "npm run dev:client" "${CLIENT_DEV_PORT:-8080}"
|
|
231
|
-
CLIENT_PID=$!
|
|
232
|
-
log_event "INFO" "client" "Supervisor started with PID ${CLIENT_PID}"
|
|
233
|
-
|
|
234
|
-
log_event "INFO" "main" "All processes started, monitoring..."
|
|
235
|
-
echo ""
|
|
236
|
-
echo "📋 Dev processes running. Press Ctrl+C to stop."
|
|
237
|
-
echo "📄 Logs: ${DEV_LOG}"
|
|
238
|
-
echo ""
|
|
239
|
-
|
|
240
|
-
# Wait for all background processes
|
|
241
|
-
wait
|
|
2
|
+
exec node "$(dirname "$0")/dev.js" "$@"
|