@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ezetgalaxy/titan",
3
- "version": "26.7.4",
3
+ "version": "26.8.0",
4
4
  "description": "Titan Planet is a JavaScript-first backend framework that embeds JS actions into a Rust + Axum server and ships as a single native binary. Routes are compiled to static metadata; only actions run in the embedded JS runtime. No Node.js. No event loop in production.",
5
5
  "license": "ISC",
6
6
  "author": "ezetgalaxy",
@@ -12,7 +12,6 @@
12
12
  },
13
13
  "files": [
14
14
  "index.js",
15
- "scripts/",
16
15
  "templates/",
17
16
  "titan",
18
17
  "titanpl-sdk",
@@ -45,10 +44,12 @@
45
44
  ],
46
45
  "scripts": {
47
46
  "build": "echo \"No build step\"",
48
- "test": "echo \"No tests specified\""
47
+ "test": "echo \"No tests specified\"",
48
+ "test_titan_init": "node scripts/test_titan_init.js"
49
49
  },
50
50
  "dependencies": {
51
51
  "chokidar": "^5.0.0",
52
- "esbuild": "^0.27.2"
52
+ "esbuild": "^0.27.2",
53
+ "prompts": "^2.4.2"
53
54
  }
54
- }
55
+ }
@@ -0,0 +1,37 @@
1
+ # Node & Packages
2
+ node_modules/
3
+ npm-debug.log*
4
+ yarn-debug.log*
5
+ yarn-error.log*
6
+ package-lock.json
7
+ yarn.lock
8
+
9
+ # Titan Runtime (Auto-generated - DO NOT COMMIT)
10
+ titan/server-bin*
11
+ .ext/
12
+ server/routes.json
13
+ server/action_map.json
14
+ server/actions/
15
+ server/titan/
16
+ server/src/actions_rust/
17
+
18
+ # Rust Build Artifacts
19
+ server/target/
20
+ Cargo.lock
21
+
22
+ # OS Files
23
+ .DS_Store
24
+ Thumbs.db
25
+ *.tmp
26
+ *.bak
27
+
28
+ # Environment & Secrets
29
+ .env
30
+ .env.local
31
+ .env.*.local
32
+
33
+ # IDEs
34
+ .vscode/
35
+ .idea/
36
+ *.swp
37
+ *.swo
@@ -1,8 +1,11 @@
1
1
  {
2
- "name": "{{name}}",
2
+ "name": "titanpl",
3
3
  "version": "1.0.0",
4
4
  "description": "A Titan Planet server",
5
5
  "type": "module",
6
+ "titan": {
7
+ "template": "js"
8
+ },
6
9
  "dependencies": {
7
10
  "chokidar": "^5.0.0",
8
11
  "esbuild": "^0.27.2"
@@ -0,0 +1,3 @@
1
+ {
2
+ "POST:/hello": "hello"
3
+ }
@@ -0,0 +1,48 @@
1
+ const defineAction = (fn) => fn;
2
+ var __titan_exports = (() => {
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // app/actions/hello.js
22
+ var hello_exports = {};
23
+ __export(hello_exports, {
24
+ hello: () => hello
25
+ });
26
+ var hello = (req) => {
27
+ return {
28
+ message: `Hello from Titan ${req.body.name}`
29
+ };
30
+ };
31
+ return __toCommonJS(hello_exports);
32
+ })();
33
+
34
+ (function () {
35
+ const fn =
36
+ __titan_exports["hello"] ||
37
+ __titan_exports.default;
38
+
39
+ if (typeof fn !== "function") {
40
+ throw new Error("[Titan] Action 'hello' not found or not a function");
41
+ }
42
+
43
+ globalThis["hello"] = function(request_arg) {
44
+ globalThis.req = request_arg;
45
+ return fn(request_arg);
46
+ };
47
+ })();
48
+
@@ -0,0 +1,16 @@
1
+ {
2
+ "__config": {
3
+ "port": 3000
4
+ },
5
+ "routes": {
6
+ "POST:/hello": {
7
+ "type": "action",
8
+ "value": "hello"
9
+ },
10
+ "GET:/": {
11
+ "type": "text",
12
+ "value": "Ready to land on Titan Planet 🚀"
13
+ }
14
+ },
15
+ "__dynamic_routes": []
16
+ }
@@ -0,0 +1,15 @@
1
+ // Auto-generated by Titan. Do not edit.
2
+ use axum::response::IntoResponse;
3
+ use axum::http::Request;
4
+ use axum::body::Body;
5
+ use std::future::Future;
6
+ use std::pin::Pin;
7
+
8
+
9
+ pub type ActionFn = fn(Request<Body>) -> Pin<Box<dyn Future<Output = axum::response::Response> + Send>>;
10
+
11
+ pub fn get_action(name: &str) -> Option<ActionFn> {
12
+ match name {
13
+ _ => None
14
+ }
15
+ }
@@ -3,6 +3,7 @@ use bcrypt::{DEFAULT_COST, hash, verify};
3
3
  use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation, decode, encode};
4
4
  use reqwest::{
5
5
  blocking::Client,
6
+ Method,
6
7
  header::{HeaderMap, HeaderName, HeaderValue},
7
8
  };
8
9
  use serde_json::Value;
@@ -18,6 +19,148 @@ use std::fs;
18
19
  use std::sync::Mutex;
19
20
  use walkdir::WalkDir;
20
21
 
22
+
23
+ // ----------------------------------------------------------------------------
24
+ // RUST ACTION API
25
+ // ----------------------------------------------------------------------------
26
+
27
+ pub struct T {
28
+ pub jwt: Jwt,
29
+ pub password: Password,
30
+ }
31
+
32
+ #[allow(non_upper_case_globals)]
33
+ pub static t: T = T {
34
+ jwt: Jwt,
35
+ password: Password,
36
+ };
37
+
38
+ pub struct Jwt;
39
+ impl Jwt {
40
+ pub fn sign(&self, payload: Value, secret: &str, options: Option<Value>) -> anyhow::Result<String> {
41
+ let mut final_payload = match payload {
42
+ Value::Object(map) => map,
43
+ _ => serde_json::Map::new(), // Should probably error or handle string payload like JS
44
+ };
45
+
46
+ if let Some(opts) = options {
47
+ if let Some(exp_val) = opts.get("expiresIn") {
48
+ // Handle both number (seconds) and string ("1h")
49
+ let seconds = if let Some(n) = exp_val.as_u64() {
50
+ Some(n)
51
+ } else if let Some(s) = exp_val.as_str() {
52
+ parse_expires_in(s)
53
+ } else {
54
+ None
55
+ };
56
+
57
+ if let Some(sec) = seconds {
58
+ let now = SystemTime::now()
59
+ .duration_since(UNIX_EPOCH)
60
+ .unwrap()
61
+ .as_secs();
62
+ final_payload.insert("exp".to_string(), Value::Number(serde_json::Number::from(now + sec)));
63
+ }
64
+ }
65
+ }
66
+
67
+ let token = encode(
68
+ &Header::default(),
69
+ &Value::Object(final_payload),
70
+ &EncodingKey::from_secret(secret.as_bytes()),
71
+ )?;
72
+ Ok(token)
73
+ }
74
+
75
+ pub fn verify(&self, token: &str, secret: &str) -> anyhow::Result<Value> {
76
+ let mut validation = Validation::default();
77
+ validation.validate_exp = true;
78
+
79
+ let data = decode::<Value>(
80
+ token,
81
+ &DecodingKey::from_secret(secret.as_bytes()),
82
+ &validation,
83
+ )?;
84
+ Ok(data.claims)
85
+ }
86
+ }
87
+
88
+ pub struct Password;
89
+ impl Password {
90
+ pub fn hash(&self, password: &str) -> anyhow::Result<String> {
91
+ let h = hash(password, DEFAULT_COST)?;
92
+ Ok(h)
93
+ }
94
+
95
+ pub fn verify(&self, password: &str, hash_str: &str) -> bool {
96
+ verify(password, hash_str).unwrap_or(false)
97
+ }
98
+ }
99
+
100
+ impl T {
101
+ pub fn log(&self, msg: impl std::fmt::Display) {
102
+ println!(
103
+ "{} {}",
104
+ blue("[Titan]"),
105
+ gray(&format!("\x1b[90mlog(rust)\x1b[0m\x1b[97m: {}\x1b[0m", msg))
106
+ );
107
+ }
108
+
109
+ pub fn read(&self, path: &str) -> anyhow::Result<String> {
110
+ let root = std::env::current_dir()?;
111
+ let target = root.join(path);
112
+ let target = target.canonicalize()?;
113
+ Ok(fs::read_to_string(target)?)
114
+ }
115
+
116
+ pub async fn fetch(&self, url: &str, options: Option<FetchOptions>) -> anyhow::Result<FetchResponse> {
117
+ let client = reqwest::Client::new();
118
+ let opts = options.unwrap_or_default();
119
+
120
+ let mut req = client.request(opts.method.parse().unwrap_or(Method::GET), url);
121
+
122
+ if let Some(headers) = opts.headers {
123
+ let mut map = HeaderMap::new();
124
+ for (k, v) in headers {
125
+ if let (Ok(name), Ok(val)) = (
126
+ HeaderName::from_bytes(k.as_bytes()),
127
+ HeaderValue::from_str(&v),
128
+ ) {
129
+ map.insert(name, val);
130
+ }
131
+ }
132
+ req = req.headers(map);
133
+ }
134
+
135
+ if let Some(body) = opts.body {
136
+ req = req.body(body);
137
+ }
138
+
139
+ let res = req.send().await?;
140
+ let status = res.status().as_u16();
141
+ let text = res.text().await?;
142
+
143
+ Ok(FetchResponse {
144
+ status,
145
+ body: text,
146
+ ok: status >= 200 && status < 300
147
+ })
148
+ }
149
+ }
150
+
151
+ #[derive(Default)]
152
+ pub struct FetchOptions {
153
+ pub method: String,
154
+ pub headers: Option<std::collections::HashMap<String, String>>,
155
+ pub body: Option<String>,
156
+ }
157
+
158
+ pub struct FetchResponse {
159
+ pub status: u16,
160
+ pub body: String,
161
+ pub ok: bool,
162
+ }
163
+
21
164
  // ----------------------------------------------------------------------------
22
165
  // GLOBAL REGISTRY
23
166
  // ----------------------------------------------------------------------------
@@ -109,14 +252,16 @@ pub fn load_project_extensions(root: PathBuf) {
109
252
  search_dirs.sort();
110
253
  search_dirs.dedup();
111
254
 
112
- println!("{} Scanning extension directories:", blue("[Titan]"));
255
+ // println!("{} Scanning extension directories:", blue("[Titan]"));
113
256
  for d in &search_dirs {
257
+ /*
114
258
  let label = if d.to_string_lossy().contains(".ext") {
115
259
  crate::utils::green("(Production)")
116
260
  } else {
117
261
  crate::utils::yellow("(Development)")
118
262
  };
119
263
  println!(" • {} {}", d.display(), label);
264
+ */
120
265
  }
121
266
 
122
267
  // =====================================================
@@ -245,20 +390,7 @@ pub fn load_project_extensions(root: PathBuf) {
245
390
  // 5. Store registry globally
246
391
  // =====================================================
247
392
  if modules.is_empty() {
248
- println!("{} {}", blue("[Titan]"), crate::utils::yellow("No extensions loaded."));
249
- // Debug: list files in search dirs to assist debugging
250
- for dir in &search_dirs {
251
- if dir.exists() {
252
- println!("{} Listing contents of {}:", blue("[Titan]"), dir.display());
253
- for entry in WalkDir::new(dir).max_depth(5) {
254
- if let Ok(e) = entry {
255
- println!(" - {}", e.path().display());
256
- }
257
- }
258
- } else {
259
- println!("{} Directory not found: {}", blue("[Titan]"), dir.display());
260
- }
261
- }
393
+ // println!("{} {}", blue("[Titan]"), crate::utils::yellow("No extensions loaded."));
262
394
  }
263
395
 
264
396
  *REGISTRY.lock().unwrap() = Some(Registry {
@@ -333,7 +465,7 @@ fn native_read(
333
465
 
334
466
  // 3. Canonicalize (resolves ../)
335
467
  let target = match joined.canonicalize() {
336
- Ok(t) => t,
468
+ Ok(target) => target,
337
469
  Err(_) => {
338
470
  throw(scope, &format!("t.read: file not found: {}", path_str));
339
471
  return;
@@ -554,7 +686,7 @@ fn native_jwt_sign(
554
686
  );
555
687
 
556
688
  match token {
557
- Ok(t) => retval.set(v8_str(scope, &t).into()),
689
+ Ok(tok) => retval.set(v8_str(scope, &tok).into()),
558
690
  Err(e) => throw(scope, &e.to_string()),
559
691
  }
560
692
  }
@@ -7,16 +7,28 @@ const actionsDir = path.join(root, "app", "actions");
7
7
  const outDir = path.join(root, "server", "actions");
8
8
 
9
9
  export async function bundle() {
10
- console.log("[Titan] Bundling actions...");
10
+ const start = Date.now();
11
+ await bundleJs();
12
+ // console.log(`[Titan] Bundle finished in ${((Date.now() - start) / 1000).toFixed(2)}s`);
13
+ }
14
+
15
+ async function bundleJs() {
16
+ // console.log("[Titan] Bundling JS actions...");
11
17
 
12
18
  fs.mkdirSync(outDir, { recursive: true });
13
19
 
14
20
  // Clean old bundles
15
- for (const file of fs.readdirSync(outDir)) {
16
- fs.unlinkSync(path.join(outDir, file));
21
+ if (fs.existsSync(outDir)) {
22
+ const oldFiles = fs.readdirSync(outDir);
23
+ for (const file of oldFiles) {
24
+ fs.unlinkSync(path.join(outDir, file));
25
+ }
17
26
  }
18
27
 
19
28
  const files = fs.readdirSync(actionsDir).filter(f => f.endsWith(".js") || f.endsWith(".ts"));
29
+ if (files.length === 0) return;
30
+
31
+ console.log(`[Titan] Bundling ${files.length} JS actions...`);
20
32
 
21
33
  for (const file of files) {
22
34
  const actionName = path.basename(file, path.extname(file));
@@ -26,7 +38,7 @@ export async function bundle() {
26
38
  // Rust runtime expects `.jsbundle` extension — consistent with previous design
27
39
  const outfile = path.join(outDir, actionName + ".jsbundle");
28
40
 
29
- console.log(`[Titan] Bundling ${entry} → ${outfile}`);
41
+ // console.log(`[Titan] Bundling ${entry} → ${outfile}`);
30
42
 
31
43
  await esbuild.build({
32
44
  entryPoints: [entry],
@@ -36,6 +48,7 @@ export async function bundle() {
36
48
  globalName: "__titan_exports",
37
49
  platform: "neutral",
38
50
  target: "es2020",
51
+ logLevel: "silent",
39
52
  banner: {
40
53
  js: "const defineAction = (fn) => fn;"
41
54
  },
@@ -51,15 +64,15 @@ export async function bundle() {
51
64
  throw new Error("[Titan] Action '${actionName}' not found or not a function");
52
65
  }
53
66
 
54
- globalThis["${actionName}"] = fn;
67
+ globalThis["${actionName}"] = function(request_arg) {
68
+ globalThis.req = request_arg;
69
+ return fn(request_arg);
70
+ };
55
71
  })();
56
72
  `
57
73
  }
58
74
  });
59
-
60
-
61
-
62
75
  }
63
76
 
64
- console.log("[Titan] Bundling finished.");
77
+ // console.log("[Titan] JS Bundling finished.");
65
78
  }
@@ -0,0 +1,194 @@
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
+
13
+ // Colors
14
+ import { createRequire } from "module";
15
+
16
+ // Colors
17
+ const cyan = (t) => `\x1b[36m${t}\x1b[0m`;
18
+ const green = (t) => `\x1b[32m${t}\x1b[0m`;
19
+ const yellow = (t) => `\x1b[33m${t}\x1b[0m`;
20
+ const red = (t) => `\x1b[31m${t}\x1b[0m`;
21
+ const gray = (t) => `\x1b[90m${t}\x1b[0m`;
22
+ const bold = (t) => `\x1b[1m${t}\x1b[0m`;
23
+
24
+ function getTitanVersion() {
25
+ try {
26
+ // 1. Try resolving from node_modules (standard user case)
27
+ const require = createRequire(import.meta.url);
28
+ // We look for @ezetgalaxy/titan/package.json
29
+ const pkgPath = require.resolve("@ezetgalaxy/titan/package.json");
30
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
31
+ return pkg.version;
32
+ } catch (e) {
33
+ try {
34
+ // 2. Fallback for local dev (path to repo root)
35
+ const localPath = path.join(__dirname, "..", "..", "..", "package.json");
36
+ if (fs.existsSync(localPath)) {
37
+ const pkg = JSON.parse(fs.readFileSync(localPath, "utf-8"));
38
+ if (pkg.name === "@ezetgalaxy/titan") {
39
+ return pkg.version;
40
+ }
41
+ }
42
+ } catch (e2) { }
43
+ }
44
+ return "0.1.0"; // Fallback
45
+ }
46
+
47
+ let serverProcess = null;
48
+ let isKilling = false;
49
+
50
+ // ... (killServer same as before)
51
+ async function killServer() {
52
+ if (!serverProcess) return;
53
+
54
+ isKilling = true;
55
+ const pid = serverProcess.pid;
56
+ const killPromise = new Promise((resolve) => {
57
+ if (serverProcess.exitCode !== null) return resolve();
58
+ serverProcess.once("close", resolve);
59
+ });
60
+
61
+ if (process.platform === "win32") {
62
+ try {
63
+ execSync(`taskkill /pid ${pid} /f /t`, { stdio: 'ignore' });
64
+ } catch (e) {
65
+ // Ignore errors if process is already dead
66
+ }
67
+ } else {
68
+ serverProcess.kill();
69
+ }
70
+
71
+ try {
72
+ await killPromise;
73
+ } catch (e) { }
74
+ serverProcess = null;
75
+ isKilling = false;
76
+ }
77
+
78
+ async function startRustServer(retryCount = 0) {
79
+ const waitTime = retryCount > 0 ? 2000 : 1000;
80
+
81
+ await killServer();
82
+ await new Promise(r => setTimeout(r, waitTime));
83
+
84
+ const serverPath = path.join(process.cwd(), "server");
85
+ const startTime = Date.now();
86
+
87
+ if (retryCount > 0) {
88
+ console.log(yellow(`[Titan] Retrying Rust server (Attempt ${retryCount})...`));
89
+ }
90
+
91
+ serverProcess = spawn("cargo", ["run", "--jobs", "1"], {
92
+ cwd: serverPath,
93
+ stdio: "inherit",
94
+ shell: true,
95
+ env: { ...process.env, CARGO_INCREMENTAL: "0" }
96
+ });
97
+
98
+ serverProcess.on("close", async (code) => {
99
+ if (isKilling) return;
100
+ const runTime = Date.now() - startTime;
101
+ if (code !== 0 && code !== null && runTime < 15000 && retryCount < 5) {
102
+ await startRustServer(retryCount + 1);
103
+ } else if (code !== 0 && code !== null && retryCount >= 5) {
104
+ console.log(red(`[Titan] Server failed to start after multiple attempts.`));
105
+ }
106
+ });
107
+ }
108
+
109
+ async function rebuild() {
110
+ // process.stdout.write(gray("[Titan] Preparing runtime... "));
111
+ const start = Date.now();
112
+ try {
113
+ execSync("node app/app.js", { stdio: "ignore" });
114
+ await bundle();
115
+ // console.log(green("Done"));
116
+ const elapsed = ((Date.now() - start) / 1000).toFixed(1);
117
+ console.log(gray(` A new orbit is ready for your app in ${elapsed}s`));
118
+ console.log(green(` Your app is now orbiting Titan Planet`));
119
+ } catch (e) {
120
+ console.log(red("Failed"));
121
+ console.log(red("[Titan] Failed to prepare runtime. Check your app/app.js"));
122
+ }
123
+ }
124
+
125
+ async function startDev() {
126
+ const root = process.cwd();
127
+ // Check if Rust actions exist by looking for .rs files in app/actions
128
+ const actionsDir = path.join(root, "app", "actions");
129
+ let hasRust = false;
130
+ if (fs.existsSync(actionsDir)) {
131
+ hasRust = fs.readdirSync(actionsDir).some(f => f.endsWith(".rs"));
132
+ }
133
+
134
+ const mode = hasRust ? "Rust + JS Actions" : "JS Actions";
135
+ const version = getTitanVersion();
136
+
137
+ console.clear();
138
+ console.log("");
139
+ console.log(` ${bold(cyan("Titan Planet"))} ${gray("v" + version)} ${yellow("[ Dev Mode ]")}`);
140
+ console.log("");
141
+ console.log(` ${gray("Type: ")} ${mode}`);
142
+ console.log(` ${gray("Hot Reload: ")} ${green("Enabled")}`);
143
+
144
+ if (fs.existsSync(path.join(root, ".env"))) {
145
+ console.log(` ${gray("Env: ")} ${yellow("Loaded")}`);
146
+ }
147
+ console.log(""); // Spacer
148
+
149
+ // FIRST BUILD
150
+ try {
151
+ await rebuild();
152
+ await startRustServer();
153
+ } catch (e) {
154
+ console.log(red("[Titan] Initial build failed. Waiting for changes..."));
155
+ }
156
+
157
+ // ... watcher logic same as before but using color vars ...
158
+ const watcher = chokidar.watch(["app", ".env"], {
159
+ ignoreInitial: true,
160
+ awaitWriteFinish: { stabilityThreshold: 500, pollInterval: 100 }
161
+ });
162
+
163
+ let timer = null;
164
+ watcher.on("all", async (event, file) => {
165
+ if (timer) clearTimeout(timer);
166
+ timer = setTimeout(async () => {
167
+ console.log(""); // Spacer before reload logs
168
+ if (file.includes(".env")) {
169
+ console.log(yellow("[Titan] Env Refreshed"));
170
+ } else {
171
+ console.log(cyan(`[Titan] Change: ${path.basename(file)}`));
172
+ }
173
+ try {
174
+ await killServer();
175
+ await rebuild();
176
+ await startRustServer();
177
+ } catch (e) {
178
+ console.log(red("[Titan] Build failed -- waiting for changes..."));
179
+ }
180
+ }, 1000);
181
+ });
182
+ }
183
+
184
+ // Handle graceful exit to release file locks
185
+ async function handleExit() {
186
+ console.log("\n[Titan] Stopping server...");
187
+ await killServer();
188
+ process.exit(0);
189
+ }
190
+
191
+ process.on("SIGINT", handleExit);
192
+ process.on("SIGTERM", handleExit);
193
+
194
+ startDev();
@@ -40,11 +40,30 @@ function addRoute(method, route) {
40
40
  };
41
41
  }
42
42
 
43
+ /**
44
+ * @typedef {Object} RouteHandler
45
+ * @property {(value: any) => void} reply - Send a direct response
46
+ * @property {(name: string) => void} action - Bind to a server-side action
47
+ */
48
+
49
+ /**
50
+ * Titan App Builder
51
+ */
43
52
  const t = {
53
+ /**
54
+ * Define a GET route
55
+ * @param {string} route
56
+ * @returns {RouteHandler}
57
+ */
44
58
  get(route) {
45
59
  return addRoute("GET", route);
46
60
  },
47
61
 
62
+ /**
63
+ * Define a POST route
64
+ * @param {string} route
65
+ * @returns {RouteHandler}
66
+ */
48
67
  post(route) {
49
68
  return addRoute("POST", route);
50
69
  },
@@ -53,6 +72,11 @@ const t = {
53
72
  console.log(`[\x1b[35m${module}\x1b[0m] ${msg}`);
54
73
  },
55
74
 
75
+ /**
76
+ * Start the Titan Server
77
+ * @param {number} [port=3000]
78
+ * @param {string} [msg=""]
79
+ */
56
80
  async start(port = 3000, msg = "") {
57
81
  try {
58
82
  console.log(cyan("[Titan] Preparing runtime..."));
@@ -94,5 +118,5 @@ const t = {
94
118
  }
95
119
  };
96
120
 
97
- globalThis.t = t;
121
+
98
122
  export default t;