@ezetgalaxy/titan 26.7.4 → 26.8.0

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.
Files changed (55) hide show
  1. package/README.md +88 -201
  2. package/index.js +552 -489
  3. package/package.json +6 -5
  4. package/templates/js/_gitignore +37 -0
  5. package/templates/{package.json → js/package.json} +4 -1
  6. package/templates/js/server/action_map.json +3 -0
  7. package/templates/js/server/actions/hello.jsbundle +48 -0
  8. package/templates/js/server/routes.json +16 -0
  9. package/templates/js/server/src/actions_rust/mod.rs +15 -0
  10. package/templates/{server → js/server}/src/extensions.rs +149 -17
  11. package/templates/{titan → js/titan}/bundle.js +22 -9
  12. package/templates/js/titan/dev.js +194 -0
  13. package/templates/{titan → js/titan}/titan.js +25 -1
  14. package/templates/rust/Dockerfile +66 -0
  15. package/templates/rust/_dockerignore +3 -0
  16. package/templates/rust/_gitignore +37 -0
  17. package/templates/rust/app/actions/hello.js +5 -0
  18. package/templates/rust/app/actions/rust_hello.rs +14 -0
  19. package/templates/rust/app/app.js +11 -0
  20. package/templates/rust/app/titan.d.ts +101 -0
  21. package/templates/rust/jsconfig.json +19 -0
  22. package/templates/rust/package.json +13 -0
  23. package/templates/rust/server/Cargo.lock +2869 -0
  24. package/templates/rust/server/Cargo.toml +27 -0
  25. package/templates/rust/server/action_map.json +3 -0
  26. package/templates/rust/server/actions/hello.jsbundle +47 -0
  27. package/templates/rust/server/routes.json +22 -0
  28. package/templates/rust/server/src/action_management.rs +131 -0
  29. package/templates/rust/server/src/actions_rust/mod.rs +19 -0
  30. package/templates/rust/server/src/actions_rust/rust_hello.rs +14 -0
  31. package/templates/rust/server/src/errors.rs +10 -0
  32. package/templates/rust/server/src/extensions.rs +989 -0
  33. package/templates/rust/server/src/main.rs +443 -0
  34. package/templates/rust/server/src/utils.rs +33 -0
  35. package/templates/rust/titan/bundle.js +157 -0
  36. package/templates/rust/titan/dev.js +194 -0
  37. package/templates/rust/titan/titan.js +122 -0
  38. package/titanpl-sdk/package.json +1 -1
  39. package/titanpl-sdk/templates/Dockerfile +4 -17
  40. package/titanpl-sdk/templates/server/src/extensions.rs +218 -423
  41. package/titanpl-sdk/templates/server/src/main.rs +68 -134
  42. package/scripts/make_dist.sh +0 -71
  43. package/templates/titan/dev.js +0 -144
  44. /package/templates/{Dockerfile → js/Dockerfile} +0 -0
  45. /package/templates/{.dockerignore → js/_dockerignore} +0 -0
  46. /package/templates/{app → js/app}/actions/hello.js +0 -0
  47. /package/templates/{app → js/app}/app.js +0 -0
  48. /package/templates/{app → js/app}/titan.d.ts +0 -0
  49. /package/templates/{jsconfig.json → js/jsconfig.json} +0 -0
  50. /package/templates/{server → js/server}/Cargo.lock +0 -0
  51. /package/templates/{server → js/server}/Cargo.toml +0 -0
  52. /package/templates/{server → js/server}/src/action_management.rs +0 -0
  53. /package/templates/{server → js/server}/src/errors.rs +0 -0
  54. /package/templates/{server → js/server}/src/main.rs +0 -0
  55. /package/templates/{server → js/server}/src/utils.rs +0 -0
@@ -1,27 +1,28 @@
1
+ use std::{collections::HashMap, fs, path::PathBuf, sync::Arc};
1
2
  use anyhow::Result;
2
3
  use axum::{
3
- Router,
4
- body::{Body, to_bytes},
4
+ body::{to_bytes, Body},
5
5
  extract::State,
6
6
  http::{Request, StatusCode},
7
7
  response::{IntoResponse, Json},
8
8
  routing::any,
9
+ Router,
9
10
  };
10
11
  use serde_json::Value;
11
- use std::time::Instant;
12
- use std::{collections::HashMap, fs, path::PathBuf, sync::Arc};
13
12
  use tokio::net::TcpListener;
13
+ use std::time::Instant;
14
14
 
15
15
  mod utils;
16
16
 
17
- mod action_management;
18
17
  mod extensions;
18
+ mod action_management;
19
19
 
20
+ use utils::{blue, white, yellow, green, gray, red};
21
+ use extensions::{init_v8, inject_extensions};
20
22
  use action_management::{
21
- DynamicRoute, RouteVal, find_actions_dir, match_dynamic_route, resolve_actions_dir,
23
+ resolve_actions_dir, find_actions_dir, match_dynamic_route,
24
+ DynamicRoute, RouteVal
22
25
  };
23
- use extensions::{init_v8, inject_extensions};
24
- use utils::{blue, gray, green, red, white, yellow};
25
26
 
26
27
  #[derive(Clone)]
27
28
  struct AppState {
@@ -44,6 +45,7 @@ async fn dynamic_handler_inner(
44
45
  State(state): State<AppState>,
45
46
  req: Request<Body>,
46
47
  ) -> impl IntoResponse {
48
+
47
49
  // ---------------------------
48
50
  // BASIC REQUEST INFO
49
51
  // ---------------------------
@@ -68,7 +70,10 @@ async fn dynamic_handler_inner(
68
70
  q.split('&')
69
71
  .filter_map(|pair| {
70
72
  let mut it = pair.splitn(2, '=');
71
- Some((it.next()?.to_string(), it.next().unwrap_or("").to_string()))
73
+ Some((
74
+ it.next()?.to_string(),
75
+ it.next().unwrap_or("").to_string(),
76
+ ))
72
77
  })
73
78
  .collect()
74
79
  })
@@ -78,7 +83,7 @@ async fn dynamic_handler_inner(
78
83
  // HEADERS & BODY
79
84
  // ---------------------------
80
85
  let (parts, body) = req.into_parts();
81
-
86
+
82
87
  let headers = parts
83
88
  .headers
84
89
  .iter()
@@ -87,7 +92,9 @@ async fn dynamic_handler_inner(
87
92
 
88
93
  let body_bytes = match to_bytes(body, usize::MAX).await {
89
94
  Ok(b) => b,
90
- Err(_) => return (StatusCode::BAD_REQUEST, "Failed to read request body").into_response(),
95
+ Err(_) => {
96
+ return (StatusCode::BAD_REQUEST, "Failed to read request body").into_response()
97
+ }
91
98
  };
92
99
 
93
100
  let body_str = String::from_utf8_lossy(&body_bytes).to_string();
@@ -112,32 +119,18 @@ async fn dynamic_handler_inner(
112
119
  action_name = Some(name);
113
120
  } else if route.r#type == "json" {
114
121
  let elapsed = start.elapsed();
115
- println!(
116
- "{} {} {} {}",
117
- blue("[Titan]"),
118
- white(&format!("{} {}", method, path)),
119
- white("→ json"),
120
- gray(&format!("in {:.2?}", elapsed))
121
- );
122
+ println!("{} {} {} {}", blue("[Titan]"), white(&format!("{} {}", method, path)), white("→ json"), gray(&format!("in {:.2?}", elapsed)));
122
123
  return Json(route.value.clone()).into_response();
123
124
  } else if let Some(s) = route.value.as_str() {
124
125
  let elapsed = start.elapsed();
125
- println!(
126
- "{} {} {} {}",
127
- blue("[Titan]"),
128
- white(&format!("{} {}", method, path)),
129
- white("→ reply"),
130
- gray(&format!("in {:.2?}", elapsed))
131
- );
126
+ println!("{} {} {} {}", blue("[Titan]"), white(&format!("{} {}", method, path)), white("→ reply"), gray(&format!("in {:.2?}", elapsed)));
132
127
  return s.to_string().into_response();
133
128
  }
134
129
  }
135
130
 
136
131
  // Dynamic route
137
132
  if action_name.is_none() {
138
- if let Some((action, p)) =
139
- match_dynamic_route(&method, &path, state.dynamic_routes.as_slice())
140
- {
133
+ if let Some((action, p)) = match_dynamic_route(&method, &path, state.dynamic_routes.as_slice()) {
141
134
  route_kind = "dynamic";
142
135
  route_label = action.clone();
143
136
  action_name = Some(action);
@@ -149,13 +142,7 @@ async fn dynamic_handler_inner(
149
142
  Some(a) => a,
150
143
  None => {
151
144
  let elapsed = start.elapsed();
152
- println!(
153
- "{} {} {} {}",
154
- blue("[Titan]"),
155
- white(&format!("{} {}", method, path)),
156
- white("→ 404"),
157
- gray(&format!("in {:.2?}", elapsed))
158
- );
145
+ println!("{} {} {} {}", blue("[Titan]"), white(&format!("{} {}", method, path)), white("→ 404"), gray(&format!("in {:.2?}", elapsed)));
159
146
  return (StatusCode::NOT_FOUND, "Not Found").into_response();
160
147
  }
161
148
  };
@@ -164,8 +151,7 @@ async fn dynamic_handler_inner(
164
151
  // LOAD ACTION
165
152
  // ---------------------------
166
153
  let resolved = resolve_actions_dir();
167
- let actions_dir = resolved
168
- .exists()
154
+ let actions_dir = resolved.exists()
169
155
  .then(|| resolved)
170
156
  .or_else(|| find_actions_dir(&state.project_root))
171
157
  .unwrap();
@@ -178,24 +164,17 @@ async fn dynamic_handler_inner(
178
164
  }
179
165
  }
180
166
 
181
- let js_code =
182
- match fs::read_to_string(&action_path) {
183
- Ok(c) => c,
184
- Err(_) => return (
185
- StatusCode::INTERNAL_SERVER_ERROR,
186
- Json(
187
- serde_json::json!({"error": "Action bundle not found", "action": action_name}),
188
- ),
189
- )
190
- .into_response(),
191
- };
167
+ let js_code = match fs::read_to_string(&action_path) {
168
+ Ok(c) => c,
169
+ Err(_) => {
170
+ return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": "Action bundle not found", "action": action_name}))).into_response()
171
+ }
172
+ };
192
173
 
193
174
  // ---------------------------
194
175
  // EXECUTE IN V8
195
176
  // ---------------------------
196
- let env_json = std::env::vars()
197
- .map(|(k, v)| (k, Value::String(v)))
198
- .collect::<serde_json::Map<_, _>>();
177
+ let env_json = std::env::vars().map(|(k, v)| (k, Value::String(v))).collect::<serde_json::Map<_, _>>();
199
178
 
200
179
  let injected = format!(
201
180
  r#"
@@ -236,42 +215,43 @@ async fn dynamic_handler_inner(
236
215
  // We can use `task::spawn_blocking`.
237
216
  let root = state.project_root.clone();
238
217
  let action_name_for_v8 = action_name.clone();
239
-
218
+
240
219
  let result_json: Value = tokio::task::spawn_blocking(move || {
241
220
  let isolate = &mut v8::Isolate::new(v8::CreateParams::default());
242
221
  let handle_scope = &mut v8::HandleScope::new(isolate);
243
222
  let context = v8::Context::new(handle_scope, v8::ContextOptions::default());
244
223
  let scope = &mut v8::ContextScope::new(handle_scope, context);
245
-
224
+
246
225
  let global = context.global(scope);
247
226
 
248
227
  // Inject extensions (t.read, etc)
249
228
  inject_extensions(scope, global);
250
-
229
+
230
+ // Set metadata globals
251
231
  // Set metadata globals
252
232
  let root_str = v8::String::new(scope, root.to_str().unwrap_or(".")).unwrap();
253
233
  let root_key = v8::String::new(scope, "__titan_root").unwrap();
254
234
  global.set(scope, root_key.into(), root_str.into());
255
-
235
+
256
236
  let action_str = v8::String::new(scope, &action_name_for_v8).unwrap();
257
237
  let action_key = v8::String::new(scope, "__titan_action").unwrap();
258
238
  global.set(scope, action_key.into(), action_str.into());
259
-
239
+
260
240
  let source = v8::String::new(scope, &injected).unwrap();
261
-
241
+
262
242
  let try_catch = &mut v8::TryCatch::new(scope);
263
-
243
+
264
244
  let script = match v8::Script::compile(try_catch, source, None) {
265
- Some(s) => s,
266
- None => {
267
- let err = try_catch.message().unwrap();
268
- let msg = err.get(try_catch).to_rust_string_lossy(try_catch);
269
- return serde_json::json!({ "error": msg, "phase": "compile" });
270
- }
245
+ Some(s) => s,
246
+ None => {
247
+ let err = try_catch.message().unwrap();
248
+ let msg = err.get(try_catch).to_rust_string_lossy(try_catch);
249
+ return serde_json::json!({ "error": msg, "phase": "compile" });
250
+ }
271
251
  };
272
-
252
+
273
253
  let result = script.run(try_catch);
274
-
254
+
275
255
  match result {
276
256
  Some(val) => {
277
257
  // Convert v8 Value to Serde JSON
@@ -279,59 +259,37 @@ async fn dynamic_handler_inner(
279
259
  let json_obj = v8::json::stringify(try_catch, val).unwrap();
280
260
  let json_str = json_obj.to_rust_string_lossy(try_catch);
281
261
  serde_json::from_str(&json_str).unwrap_or(Value::Null)
282
- }
262
+ },
283
263
  None => {
284
- let err = try_catch.message().unwrap();
285
- let msg = err.get(try_catch).to_rust_string_lossy(try_catch);
286
- serde_json::json!({ "error": msg, "phase": "execution" })
264
+ let err = try_catch.message().unwrap();
265
+ let msg = err.get(try_catch).to_rust_string_lossy(try_catch);
266
+ serde_json::json!({ "error": msg, "phase": "execution" })
287
267
  }
288
268
  }
289
- })
290
- .await
291
- .unwrap_or(serde_json::json!({"error": "V8 task failed"}));
269
+ }).await.unwrap_or(serde_json::json!({"error": "V8 task failed"}));
292
270
 
293
271
  // ---------------------------
294
272
  // FINAL LOG
295
273
  // ---------------------------
296
274
  let elapsed = start.elapsed();
297
-
275
+
298
276
  // Check for errors in result
299
277
  if let Some(err) = result_json.get("error") {
300
- println!(
301
- "{} {} {} {}",
302
- blue("[Titan]"),
303
- red(&format!("{} {}", method, path)),
304
- red("→ error"),
305
- gray(&format!("in {:.2?}", elapsed))
306
- );
278
+ println!("{} {} {} {}", blue("[Titan]"), red(&format!("{} {}", method, path)), red("→ error"), gray(&format!("in {:.2?}", elapsed)));
307
279
  println!("{}", red(err.as_str().unwrap_or("Unknown")));
308
280
  return (StatusCode::INTERNAL_SERVER_ERROR, Json(result_json)).into_response();
309
281
  }
310
282
 
311
283
  match route_kind {
312
- "dynamic" => println!(
313
- "{} {} {} {} {} {}",
314
- blue("[Titan]"),
315
- green(&format!("{} {}", method, path)),
316
- white("→"),
317
- green(&route_label),
318
- white("(dynamic)"),
319
- gray(&format!("in {:.2?}", elapsed))
320
- ),
321
- "exact" => println!(
322
- "{} {} {} {} {}",
323
- blue("[Titan]"),
324
- white(&format!("{} {}", method, path)),
325
- white("→"),
326
- yellow(&route_label),
327
- gray(&format!("in {:.2?}", elapsed))
328
- ),
284
+ "dynamic" => println!("{} {} {} {} {} {}", blue("[Titan]"), green(&format!("{} {}", method, path)), white("→"), green(&route_label), white("(dynamic)"), gray(&format!("in {:.2?}", elapsed))),
285
+ "exact" => println!("{} {} {} {} {}", blue("[Titan]"), white(&format!("{} {}", method, path)), white("→"), yellow(&route_label), gray(&format!("in {:.2?}", elapsed))),
329
286
  _ => {}
330
287
  }
331
288
 
332
289
  Json(result_json).into_response()
333
290
  }
334
291
 
292
+
335
293
  // Entrypoint ---------------------------------------------------------------
336
294
 
337
295
  #[tokio::main]
@@ -346,22 +304,28 @@ async fn main() -> Result<()> {
346
304
  let port = json["__config"]["port"].as_u64().unwrap_or(3000);
347
305
  let routes_json = json["routes"].clone();
348
306
  let map: HashMap<String, RouteVal> = serde_json::from_value(routes_json).unwrap_or_default();
349
- let dynamic_routes: Vec<DynamicRoute> =
350
- serde_json::from_value(json["__dynamic_routes"].clone()).unwrap_or_default();
307
+ let dynamic_routes: Vec<DynamicRoute> = serde_json::from_value(json["__dynamic_routes"].clone()).unwrap_or_default();
351
308
 
352
- // Identify project root (where .ext or node_modules lives)
353
- let project_root = resolve_project_root();
309
+ // Project root heuristics
310
+ let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
311
+ // Ensure we find the 'app' directory as sibling if running from 'server'
312
+ let project_root = if cwd.join("../app").exists() {
313
+ cwd.join("../app")
314
+ } else {
315
+ cwd
316
+ };
354
317
 
355
318
  let state = AppState {
356
319
  routes: Arc::new(map),
357
320
  dynamic_routes: Arc::new(dynamic_routes),
358
321
  project_root: project_root.clone(),
359
322
  };
360
-
323
+
361
324
  // Load extensions
362
325
  extensions::load_project_extensions(project_root.clone());
363
326
 
364
327
  let app = Router::new()
328
+
365
329
  .route("/", any(root_route))
366
330
  .fallback(any(dynamic_route))
367
331
  .with_state(state);
@@ -374,38 +338,8 @@ async fn main() -> Result<()> {
374
338
  println!(" ██║ ██║ ██║ ██╔══██║██║╚██╗██║");
375
339
  println!(" ██║ ██║ ██║ ██║ ██║██║ ╚████║");
376
340
  println!(" ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝\x1b[0m\n");
377
- println!(
378
- "\x1b[38;5;39mTitan server running at:\x1b[0m http://localhost:{}",
379
- port
380
- );
341
+ println!("\x1b[38;5;39mTitan server running at:\x1b[0m http://localhost:{}", port);
381
342
 
382
343
  axum::serve(listener, app).await?;
383
344
  Ok(())
384
345
  }
385
-
386
- fn resolve_project_root() -> PathBuf {
387
- // 1. Check CWD (preferred for local dev/tooling)
388
- if let Ok(cwd) = std::env::current_dir() {
389
- if cwd.join("node_modules").exists()
390
- || cwd.join("package.json").exists()
391
- || cwd.join(".ext").exists()
392
- {
393
- return cwd;
394
- }
395
- }
396
-
397
- // 2. Check executable persistence (Docker / Production)
398
- // Walk up from the executable to find .ext or node_modules
399
- if let Ok(exe) = std::env::current_exe() {
400
- let mut current = exe.parent();
401
- while let Some(dir) = current {
402
- if dir.join(".ext").exists() || dir.join("node_modules").exists() {
403
- return dir.to_path_buf();
404
- }
405
- current = dir.parent();
406
- }
407
- }
408
-
409
- // 3. Fallback to CWD
410
- std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
411
- }
@@ -1,71 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -e
3
-
4
- echo "Building Titan distribution..."
5
-
6
- # ---------------------------------------------
7
- # Resolve directories
8
- # ---------------------------------------------
9
- ROOT="$(cd "$(dirname "$0")/.." && pwd)"
10
- SERVER_DIR="$ROOT/server"
11
- DIST_DIR="$ROOT/dist"
12
-
13
- # Clean and recreate dist/
14
- rm -rf "$DIST_DIR"
15
- mkdir -p "$DIST_DIR"
16
-
17
- # ---------------------------------------------
18
- # Copy release binary titan-server
19
- # ---------------------------------------------
20
- RELEASE_PATH="$SERVER_DIR/target/release"
21
-
22
- echo "Looking for titan-server binary..."
23
-
24
- if [ -f "$RELEASE_PATH/titan-server" ]; then
25
- echo "✓ Found titan-server"
26
- cp "$RELEASE_PATH/titan-server" "$DIST_DIR/"
27
- else
28
- echo "Binary not found directly, searching..."
29
- BIN=$(ls "$RELEASE_PATH" | grep 'titan-server' || true)
30
-
31
- if [ -n "$BIN" ]; then
32
- echo "✓ Found matching binary: $BIN"
33
- cp "$RELEASE_PATH/$BIN" "$DIST_DIR/titan-server"
34
- else
35
- echo "✗ titan-server binary not found in release folder."
36
- echo "Did you run: cargo build --release ?"
37
- exit 1
38
- fi
39
- fi
40
-
41
- # ---------------------------------------------
42
- # routes.json (JS bundler should generate routes.build.json)
43
- # ---------------------------------------------
44
- if [ -f "$ROOT/routes.build.json" ]; then
45
- echo "✓ Using routes.build.json"
46
- cp "$ROOT/routes.build.json" "$DIST_DIR/routes.json"
47
- else
48
- echo "⚠ No routes.build.json found. Creating empty routes.json"
49
- echo "{}" > "$DIST_DIR/routes.json"
50
- fi
51
-
52
- # ---------------------------------------------
53
- # Copy handlers if they exist
54
- # ---------------------------------------------
55
- mkdir -p "$DIST_DIR/handlers"
56
-
57
- if [ -d "$ROOT/handlers" ]; then
58
- echo "✓ Copying handlers/"
59
- cp -r "$ROOT/handlers/"* "$DIST_DIR/handlers/" 2>/dev/null || true
60
- else
61
- echo "⚠ No handlers/ directory found."
62
- fi
63
-
64
- echo ""
65
- echo "-------------------------------------------"
66
- echo " ✔ Titan dist/ build complete"
67
- echo "-------------------------------------------"
68
- echo "Binary: dist/titan-server"
69
- echo "Routes: dist/routes.json"
70
- echo "Handlers: dist/handlers/"
71
- echo ""
@@ -1,144 +0,0 @@
1
- import chokidar from "chokidar";
2
- import { spawn, execSync } from "child_process";
3
- import path from "path";
4
- import { fileURLToPath } from "url";
5
- import fs from "fs";
6
- import { bundle } from "./bundle.js";
7
-
8
- // Required for __dirname in ES modules
9
- const __filename = fileURLToPath(import.meta.url);
10
- const __dirname = path.dirname(__filename);
11
-
12
- let serverProcess = null;
13
- let isKilling = false;
14
-
15
- async function killServer() {
16
- if (!serverProcess) return;
17
-
18
- isKilling = true;
19
- const pid = serverProcess.pid;
20
- const killPromise = new Promise((resolve) => {
21
- if (serverProcess.exitCode !== null) return resolve();
22
- serverProcess.once("close", resolve);
23
- });
24
-
25
- if (process.platform === "win32") {
26
- try {
27
- execSync(`taskkill /pid ${pid} /f /t`, { stdio: 'ignore' });
28
- } catch (e) {
29
- // Ignore errors if process is already dead
30
- }
31
- } else {
32
- serverProcess.kill();
33
- }
34
-
35
- try {
36
- await killPromise;
37
- } catch (e) { }
38
- serverProcess = null;
39
- isKilling = false;
40
- }
41
-
42
- async function startRustServer(retryCount = 0) {
43
- // If we are retrying, give it more time (2s), otherwise standard 1s (increased from 500ms)
44
- const waitTime = retryCount > 0 ? 2000 : 1000;
45
-
46
- // Ensure any previous instance is killed
47
- await killServer();
48
-
49
- // Give the OS a moment to release file locks on the binary
50
- await new Promise(r => setTimeout(r, waitTime));
51
-
52
- const serverPath = path.join(process.cwd(), "server");
53
- const startTime = Date.now();
54
-
55
- if (retryCount > 0) {
56
- console.log(`\x1b[33m[Titan] Retrying Rust server (Attempt ${retryCount})...\x1b[0m`);
57
- }
58
-
59
- // Windows often has file locking issues during concurrent linking/metadata generation
60
- // We force 1 job and disable incremental compilation to be safe.
61
- serverProcess = spawn("cargo", ["run", "--jobs", "1"], {
62
- cwd: serverPath,
63
- stdio: "inherit",
64
- shell: true,
65
- env: { ...process.env, CARGO_INCREMENTAL: "0" }
66
- });
67
-
68
- serverProcess.on("close", async (code) => {
69
- if (isKilling) return;
70
-
71
- console.log(`[Titan] Rust server exited: ${code}`);
72
-
73
- // If exited with error and it was a short run (< 10s), likely a start-up error/lock
74
- // Retry up to 3 times
75
- const runTime = Date.now() - startTime;
76
- if (code !== 0 && code !== null && runTime < 10000 && retryCount < 3) {
77
- console.log(`\x1b[31m[Titan] Server crash detected (possibly file lock). Retrying automatically...\x1b[0m`);
78
- await startRustServer(retryCount + 1);
79
- }
80
- });
81
- }
82
-
83
- async function rebuild() {
84
- console.log("[Titan] Regenerating routes.json & action_map.json...");
85
- execSync("node app/app.js", { stdio: "inherit" });
86
-
87
- console.log("[Titan] Bundling JS actions...");
88
- await bundle();
89
- }
90
-
91
- async function startDev() {
92
- console.log("[Titan] Dev mode starting...");
93
-
94
- if (fs.existsSync(path.join(process.cwd(), ".env"))) {
95
- console.log("\x1b[33m[Titan] Env Configured\x1b[0m");
96
- }
97
-
98
- // FIRST BUILD
99
- try {
100
- await rebuild();
101
- await startRustServer();
102
- } catch (e) {
103
- console.log("\x1b[31m[Titan] Initial build failed. Waiting for changes...\x1b[0m");
104
- }
105
-
106
- const watcher = chokidar.watch(["app", ".env"], {
107
- ignoreInitial: true
108
- });
109
-
110
- let timer = null;
111
-
112
- watcher.on("all", async (event, file) => {
113
- if (timer) clearTimeout(timer);
114
-
115
- timer = setTimeout(async () => {
116
- if (file.includes(".env")) {
117
- console.log("\x1b[33m[Titan] Env Refreshed\x1b[0m");
118
- } else {
119
- console.log(`[Titan] Change detected: ${file}`);
120
- }
121
-
122
- try {
123
- await rebuild();
124
- console.log("[Titan] Restarting Rust server...");
125
- await startRustServer();
126
- } catch (e) {
127
- console.log("\x1b[31m[Titan] Build failed -- waiting for changes...\x1b[0m");
128
- }
129
-
130
- }, 500);
131
- });
132
- }
133
-
134
- // Handle graceful exit to release file locks
135
- async function handleExit() {
136
- console.log("\n[Titan] Stopping server...");
137
- await killServer();
138
- process.exit(0);
139
- }
140
-
141
- process.on("SIGINT", handleExit);
142
- process.on("SIGTERM", handleExit);
143
-
144
- startDev();
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes