@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/fullstack-cli",
3
- "version": "1.1.16-beta.0",
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);
@@ -3,7 +3,7 @@
3
3
  "collection": "@nestjs/schematics",
4
4
  "sourceRoot": "server",
5
5
  "compilerOptions": {
6
- "deleteOutDir": true,
6
+ "deleteOutDir": false,
7
7
  "tsConfigPath": "tsconfig.node.json",
8
8
  "assets": [
9
9
  {
@@ -3,7 +3,7 @@
3
3
  set -euo pipefail
4
4
 
5
5
  ROOT_DIR="$(pwd)"
6
- OUT_DIR="$ROOT_DIR/dist/server"
6
+ DIST_DIR="$ROOT_DIR/dist"
7
7
 
8
8
  # 记录总开始时间
9
9
  TOTAL_START=$(node -e "console.log(Date.now())")
@@ -78,20 +78,26 @@ print_time $STEP_START
78
78
  echo ""
79
79
 
80
80
  # ==================== 步骤 4 ====================
81
- echo "📦 [4/5] 准备 server 依赖产物"
81
+ echo "📦 [4/5] 准备产物"
82
82
  STEP_START=$(node -e "console.log(Date.now())")
83
83
 
84
- mkdir -p "$OUT_DIR/dist/client"
84
+ # 拷贝 run.sh 到 dist/(prod 从 dist/ 启动,确保 cwd 一致性)
85
+ cp "$ROOT_DIR/scripts/run.sh" "$DIST_DIR/"
85
86
 
86
- # 拷贝 HTML
87
- cp "$ROOT_DIR/dist/client/"*.html "$OUT_DIR/dist/client/" || true
87
+ # 拷贝 .env 文件(如果存在)
88
+ if [ -f "$ROOT_DIR/.env" ]; then
89
+ cp "$ROOT_DIR/.env" "$DIST_DIR/"
90
+ fi
88
91
 
89
- # 拷贝 run.sh 文件
90
- cp "$ROOT_DIR/scripts/run.sh" "$OUT_DIR/"
92
+ # 移动 client 下的 HTML 文件到 dist/dist/client,保证 views 路径在 dev/prod 下一致
93
+ if [ -d "$DIST_DIR/client" ]; then
94
+ mkdir -p "$DIST_DIR/dist/client"
95
+ find "$DIST_DIR/client" -maxdepth 1 -name "*.html" -exec mv {} "$DIST_DIR/dist/client/" \;
96
+ fi
91
97
 
92
98
  # 清理无用文件
93
- rm -rf "$ROOT_DIR/dist/scripts"
94
- rm -rf "$ROOT_DIR/dist/tsconfig.node.tsbuildinfo"
99
+ rm -rf "$DIST_DIR/scripts"
100
+ rm -rf "$DIST_DIR/tsconfig.node.tsbuildinfo"
95
101
 
96
102
  print_time $STEP_START
97
103
  echo ""
@@ -111,8 +117,8 @@ echo "构建完成"
111
117
  print_time $TOTAL_START
112
118
 
113
119
  # 输出产物信息
114
- DIST_SIZE=$(du -sh "$OUT_DIR" | cut -f1)
115
- NODE_MODULES_SIZE=$(du -sh "$ROOT_DIR/dist/node_modules" | cut -f1)
120
+ DIST_SIZE=$(du -sh "$DIST_DIR" | cut -f1)
121
+ NODE_MODULES_SIZE=$(du -sh "$DIST_DIR/node_modules" | cut -f1)
116
122
  echo ""
117
123
  echo "📊 构建产物统计:"
118
124
  echo " 产物大小: $DIST_SIZE"
@@ -0,0 +1,275 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const { spawn, execSync } = require('child_process');
7
+ const readline = require('readline');
8
+
9
+ // ── Project root ──────────────────────────────────────────────────────────────
10
+ const PROJECT_ROOT = path.resolve(__dirname, '..');
11
+ process.chdir(PROJECT_ROOT);
12
+
13
+ // ── Load .env ─────────────────────────────────────────────────────────────────
14
+ function loadEnv() {
15
+ const envPath = path.join(PROJECT_ROOT, '.env');
16
+ if (!fs.existsSync(envPath)) return;
17
+ const lines = fs.readFileSync(envPath, 'utf8').split('\n');
18
+ for (const line of lines) {
19
+ const trimmed = line.trim();
20
+ if (!trimmed || trimmed.startsWith('#')) continue;
21
+ const eqIdx = trimmed.indexOf('=');
22
+ if (eqIdx === -1) continue;
23
+ const key = trimmed.slice(0, eqIdx).trim();
24
+ const value = trimmed.slice(eqIdx + 1).trim();
25
+ if (!(key in process.env)) {
26
+ process.env[key] = value;
27
+ }
28
+ }
29
+ }
30
+ loadEnv();
31
+
32
+ // ── Configuration ─────────────────────────────────────────────────────────────
33
+ const LOG_DIR = process.env.LOG_DIR || 'logs';
34
+ const MAX_RESTART_COUNT = parseInt(process.env.MAX_RESTART_COUNT, 10) || 10;
35
+ const RESTART_DELAY = parseInt(process.env.RESTART_DELAY, 10) || 2;
36
+ const MAX_DELAY = 60;
37
+ const SERVER_PORT = process.env.SERVER_PORT || '3000';
38
+ const CLIENT_DEV_PORT = process.env.CLIENT_DEV_PORT || '8080';
39
+
40
+ fs.mkdirSync(LOG_DIR, { recursive: true });
41
+
42
+ // ── Logging infrastructure ────────────────────────────────────────────────────
43
+ const devStdLogPath = path.join(LOG_DIR, 'dev.std.log');
44
+ const devLogPath = path.join(LOG_DIR, 'dev.log');
45
+ const devStdLogFd = fs.openSync(devStdLogPath, 'a');
46
+ const devLogFd = fs.openSync(devLogPath, 'a');
47
+
48
+ function timestamp() {
49
+ const now = new Date();
50
+ return (
51
+ now.getFullYear() + '-' +
52
+ String(now.getMonth() + 1).padStart(2, '0') + '-' +
53
+ String(now.getDate()).padStart(2, '0') + ' ' +
54
+ String(now.getHours()).padStart(2, '0') + ':' +
55
+ String(now.getMinutes()).padStart(2, '0') + ':' +
56
+ String(now.getSeconds()).padStart(2, '0')
57
+ );
58
+ }
59
+
60
+ /** Write to terminal + dev.std.log */
61
+ function writeOutput(msg) {
62
+ try { process.stdout.write(msg); } catch {}
63
+ try { fs.writeSync(devStdLogFd, msg); } catch {}
64
+ }
65
+
66
+ /** Structured event log → terminal + dev.std.log + dev.log */
67
+ function logEvent(level, name, message) {
68
+ const msg = `[${timestamp()}] [${level}] [${name}] ${message}\n`;
69
+ writeOutput(msg);
70
+ try { fs.writeSync(devLogFd, msg); } catch {}
71
+ }
72
+
73
+ // ── Process group management ──────────────────────────────────────────────────
74
+ function killProcessGroup(pid, signal) {
75
+ try {
76
+ process.kill(-pid, signal);
77
+ } catch {}
78
+ }
79
+
80
+ function killOrphansByPort(port) {
81
+ try {
82
+ const pids = execSync(`lsof -ti :${port}`, { encoding: 'utf8', timeout: 5000 }).trim();
83
+ if (pids) {
84
+ const pidList = pids.split('\n').filter(Boolean);
85
+ for (const p of pidList) {
86
+ try { process.kill(parseInt(p, 10), 'SIGKILL'); } catch {}
87
+ }
88
+ return pidList;
89
+ }
90
+ } catch {}
91
+ return [];
92
+ }
93
+
94
+ // ── Process supervision ───────────────────────────────────────────────────────
95
+ let stopping = false;
96
+ const managedProcesses = []; // { name, pid, child }
97
+
98
+ function sleep(ms) {
99
+ return new Promise((resolve) => setTimeout(resolve, ms));
100
+ }
101
+
102
+ /**
103
+ * Start and supervise a process with auto-restart and log piping.
104
+ * Returns a promise that resolves when the process loop ends.
105
+ */
106
+ function startProcess({ name, command, args, cleanupPort }) {
107
+ const logFilePath = path.join(LOG_DIR, `${name}.std.log`);
108
+ const logFd = fs.openSync(logFilePath, 'a');
109
+
110
+ const entry = { name, pid: null, child: null };
111
+ managedProcesses.push(entry);
112
+
113
+ const run = async () => {
114
+ let restartCount = 0;
115
+
116
+ while (!stopping) {
117
+ const child = spawn(command, args, {
118
+ detached: true,
119
+ stdio: ['ignore', 'pipe', 'pipe'],
120
+ shell: true,
121
+ cwd: PROJECT_ROOT,
122
+ env: { ...process.env },
123
+ });
124
+
125
+ entry.pid = child.pid;
126
+ entry.child = child;
127
+
128
+ logEvent('INFO', name, `Process started (PGID: ${child.pid}): ${command} ${args.join(' ')}`);
129
+
130
+ // Pipe stdout and stderr through readline for timestamped logging
131
+ const pipeLines = (stream) => {
132
+ const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
133
+ rl.on('line', (line) => {
134
+ const msg = `[${timestamp()}] [${name}] ${line}\n`;
135
+ try { fs.writeSync(logFd, msg); } catch {}
136
+ writeOutput(msg);
137
+ });
138
+ };
139
+ if (child.stdout) pipeLines(child.stdout);
140
+ if (child.stderr) pipeLines(child.stderr);
141
+
142
+ // Wait for the direct child to exit.
143
+ // NOTE: must use 'exit', not 'close'. With shell:true, grandchild processes
144
+ // (e.g. nest's server) inherit stdout pipes. 'close' won't fire until ALL
145
+ // pipe holders exit, causing dev.js to hang when npm/nest dies but server survives.
146
+ const exitCode = await new Promise((resolve) => {
147
+ child.on('exit', (code) => resolve(code ?? 1));
148
+ child.on('error', () => resolve(1));
149
+ });
150
+
151
+ // Kill the entire process group
152
+ if (entry.pid) {
153
+ killProcessGroup(entry.pid, 'SIGTERM');
154
+ await sleep(2000);
155
+ killProcessGroup(entry.pid, 'SIGKILL');
156
+ }
157
+ entry.pid = null;
158
+ entry.child = null;
159
+
160
+ // Port cleanup fallback
161
+ if (cleanupPort) {
162
+ const orphans = killOrphansByPort(cleanupPort);
163
+ if (orphans.length > 0) {
164
+ logEvent('WARN', name, `Killed orphan processes on port ${cleanupPort}: ${orphans.join(' ')}`);
165
+ await sleep(500);
166
+ }
167
+ }
168
+
169
+ if (stopping) break;
170
+
171
+ restartCount++;
172
+ if (restartCount >= MAX_RESTART_COUNT) {
173
+ logEvent('ERROR', name, `Max restart count (${MAX_RESTART_COUNT}) reached, giving up`);
174
+ break;
175
+ }
176
+
177
+ const delay = Math.min(RESTART_DELAY * (1 << (restartCount - 1)), MAX_DELAY);
178
+ logEvent('WARN', name, `Process exited with code ${exitCode}, restarting (${restartCount}/${MAX_RESTART_COUNT}) in ${delay}s...`);
179
+ await sleep(delay * 1000);
180
+ }
181
+
182
+ try { fs.closeSync(logFd); } catch {}
183
+ };
184
+
185
+ return run();
186
+ }
187
+
188
+ // ── Cleanup ───────────────────────────────────────────────────────────────────
189
+ let cleanupDone = false;
190
+
191
+ async function cleanup() {
192
+ if (cleanupDone) return;
193
+ cleanupDone = true;
194
+ stopping = true;
195
+
196
+ logEvent('INFO', 'main', 'Shutting down all processes...');
197
+
198
+ // Kill all managed process groups
199
+ for (const entry of managedProcesses) {
200
+ if (entry.pid) {
201
+ logEvent('INFO', 'main', `Stopping process group (PGID: ${entry.pid})`);
202
+ killProcessGroup(entry.pid, 'SIGTERM');
203
+ }
204
+ }
205
+
206
+ // Wait for graceful shutdown
207
+ await sleep(2000);
208
+
209
+ // Force kill any remaining
210
+ for (const entry of managedProcesses) {
211
+ if (entry.pid) {
212
+ logEvent('WARN', 'main', `Force killing process group (PGID: ${entry.pid})`);
213
+ killProcessGroup(entry.pid, 'SIGKILL');
214
+ }
215
+ }
216
+
217
+ // Port cleanup fallback
218
+ killOrphansByPort(SERVER_PORT);
219
+ killOrphansByPort(CLIENT_DEV_PORT);
220
+
221
+ logEvent('INFO', 'main', 'All processes stopped');
222
+
223
+ try { fs.closeSync(devStdLogFd); } catch {}
224
+ try { fs.closeSync(devLogFd); } catch {}
225
+
226
+ process.exit(0);
227
+ }
228
+
229
+ process.on('SIGTERM', cleanup);
230
+ process.on('SIGINT', cleanup);
231
+ process.on('SIGHUP', cleanup);
232
+
233
+ // ── Main ──────────────────────────────────────────────────────────────────────
234
+ async function main() {
235
+ logEvent('INFO', 'main', '========== Dev session started ==========');
236
+
237
+ // Initialize action plugins
238
+ writeOutput('\n🔌 Initializing action plugins...\n');
239
+ try {
240
+ execSync('fullstack-cli action-plugin init', { cwd: PROJECT_ROOT, stdio: 'inherit' });
241
+ writeOutput('✅ Action plugins initialized\n\n');
242
+ } catch {
243
+ writeOutput('⚠️ Action plugin initialization failed, continuing anyway...\n\n');
244
+ }
245
+
246
+ // Start server and client
247
+ const serverPromise = startProcess({
248
+ name: 'server',
249
+ command: 'npm',
250
+ args: ['run', 'dev:server'],
251
+ cleanupPort: SERVER_PORT,
252
+ });
253
+
254
+ const clientPromise = startProcess({
255
+ name: 'client',
256
+ command: 'npm',
257
+ args: ['run', 'dev:client'],
258
+ cleanupPort: CLIENT_DEV_PORT,
259
+ });
260
+
261
+ writeOutput(`📋 Dev processes running. Press Ctrl+C to stop.\n`);
262
+ writeOutput(`📄 Logs: ${devStdLogPath}\n\n`);
263
+
264
+ // Wait for both (they loop until stopping or max restarts)
265
+ await Promise.all([serverPromise, clientPromise]);
266
+
267
+ if (!cleanupDone) {
268
+ await cleanup();
269
+ }
270
+ }
271
+
272
+ main().catch((err) => {
273
+ console.error('Fatal error:', err);
274
+ process.exit(1);
275
+ });
@@ -1,241 +1,2 @@
1
1
  #!/usr/bin/env bash
2
- # This file is auto-generated by @lark-apaas/fullstack-cli
3
-
4
- set -uo pipefail
5
-
6
- # Ensure the script always runs from the project root
7
- cd "$(dirname "${BASH_SOURCE[0]}")/.."
8
-
9
- # Configuration
10
- LOG_DIR=${LOG_DIR:-logs}
11
- DEV_LOG="${LOG_DIR}/dev.log"
12
- MAX_RESTART_COUNT=${MAX_RESTART_COUNT:-10}
13
- RESTART_DELAY=${RESTART_DELAY:-2}
14
-
15
- # Process tracking
16
- SERVER_PID=""
17
- CLIENT_PID=""
18
- PARENT_PID=$$
19
- STOP_FLAG_FILE="/tmp/dev_sh_stop_$$"
20
- CLEANUP_DONE=false
21
-
22
- mkdir -p "${LOG_DIR}"
23
-
24
- # 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" "$@"