@ezetgalaxy/titan 26.7.5 → 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.
- package/README.md +86 -200
- package/index.js +87 -23
- package/package.json +4 -3
- package/templates/js/_gitignore +37 -0
- package/templates/{package.json → js/package.json} +3 -0
- package/templates/{server → js/server}/actions/hello.jsbundle +4 -1
- package/templates/js/server/src/actions_rust/mod.rs +15 -0
- package/templates/{server → js/server}/src/extensions.rs +149 -17
- package/templates/{titan → js/titan}/bundle.js +22 -9
- package/templates/js/titan/dev.js +194 -0
- package/templates/{titan → js/titan}/titan.js +25 -1
- package/templates/rust/Dockerfile +66 -0
- package/templates/rust/_dockerignore +3 -0
- package/templates/rust/_gitignore +37 -0
- package/templates/rust/app/actions/hello.js +5 -0
- package/templates/rust/app/actions/rust_hello.rs +14 -0
- package/templates/rust/app/app.js +11 -0
- package/templates/rust/app/titan.d.ts +101 -0
- package/templates/rust/jsconfig.json +19 -0
- package/templates/rust/package.json +13 -0
- package/templates/rust/server/Cargo.lock +2869 -0
- package/templates/rust/server/Cargo.toml +27 -0
- package/templates/rust/server/action_map.json +3 -0
- package/templates/rust/server/actions/hello.jsbundle +47 -0
- package/templates/rust/server/routes.json +22 -0
- package/templates/rust/server/src/action_management.rs +131 -0
- package/templates/rust/server/src/actions_rust/mod.rs +19 -0
- package/templates/rust/server/src/actions_rust/rust_hello.rs +14 -0
- package/templates/rust/server/src/errors.rs +10 -0
- package/templates/rust/server/src/extensions.rs +989 -0
- package/templates/rust/server/src/main.rs +443 -0
- package/templates/rust/server/src/utils.rs +33 -0
- package/templates/rust/titan/bundle.js +157 -0
- package/templates/rust/titan/dev.js +194 -0
- package/templates/rust/titan/titan.js +122 -0
- package/titanpl-sdk/templates/Dockerfile +4 -17
- package/titanpl-sdk/templates/server/src/extensions.rs +218 -423
- package/titanpl-sdk/templates/server/src/main.rs +68 -134
- package/templates/_gitignore +0 -25
- package/templates/titan/dev.js +0 -144
- /package/templates/{Dockerfile → js/Dockerfile} +0 -0
- /package/templates/{.dockerignore → js/_dockerignore} +0 -0
- /package/templates/{app → js/app}/actions/hello.js +0 -0
- /package/templates/{app → js/app}/app.js +0 -0
- /package/templates/{app → js/app}/titan.d.ts +0 -0
- /package/templates/{jsconfig.json → js/jsconfig.json} +0 -0
- /package/templates/{server → js/server}/Cargo.lock +0 -0
- /package/templates/{server → js/server}/Cargo.toml +0 -0
- /package/templates/{server → js/server}/action_map.json +0 -0
- /package/templates/{server → js/server}/routes.json +0 -0
- /package/templates/{server → js/server}/src/action_management.rs +0 -0
- /package/templates/{server → js/server}/src/errors.rs +0 -0
- /package/templates/{server → js/server}/src/main.rs +0 -0
- /package/templates/{server → js/server}/src/utils.rs +0 -0
|
@@ -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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
16
|
-
fs.
|
|
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}"] =
|
|
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
|
-
|
|
121
|
+
|
|
98
122
|
export default t;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# ================================================================
|
|
2
|
+
# STAGE 1 — Build Titan (JS → Rust)
|
|
3
|
+
# ================================================================
|
|
4
|
+
FROM rust:1.91.1 AS builder
|
|
5
|
+
|
|
6
|
+
# Install Node for Titan CLI + bundler
|
|
7
|
+
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
|
8
|
+
&& apt-get install -y nodejs
|
|
9
|
+
|
|
10
|
+
# Install Titan CLI (latest)
|
|
11
|
+
RUN npm install -g @ezetgalaxy/titan@latest
|
|
12
|
+
|
|
13
|
+
WORKDIR /app
|
|
14
|
+
|
|
15
|
+
# Copy project files
|
|
16
|
+
COPY . .
|
|
17
|
+
|
|
18
|
+
# Install JS dependencies (needed for Titan DSL + bundler)
|
|
19
|
+
RUN npm install
|
|
20
|
+
|
|
21
|
+
SHELL ["/bin/bash", "-c"]
|
|
22
|
+
|
|
23
|
+
# Extract Titan extensions into .ext
|
|
24
|
+
RUN mkdir -p /app/.ext && \
|
|
25
|
+
find /app/node_modules -maxdepth 5 -type f -name "titan.json" -print0 | \
|
|
26
|
+
while IFS= read -r -d '' file; do \
|
|
27
|
+
pkg_dir="$(dirname "$file")"; \
|
|
28
|
+
pkg_name="$(basename "$pkg_dir")"; \
|
|
29
|
+
echo "Copying Titan extension: $pkg_name from $pkg_dir"; \
|
|
30
|
+
cp -r "$pkg_dir" "/app/.ext/$pkg_name"; \
|
|
31
|
+
done && \
|
|
32
|
+
echo "Extensions in .ext:" && \
|
|
33
|
+
ls -R /app/.ext
|
|
34
|
+
|
|
35
|
+
# Build Titan metadata + bundle JS actions
|
|
36
|
+
RUN titan build
|
|
37
|
+
|
|
38
|
+
# Build Rust binary
|
|
39
|
+
RUN cd server && cargo build --release
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ================================================================
|
|
44
|
+
# STAGE 2 — Runtime Image (Lightweight)
|
|
45
|
+
# ================================================================
|
|
46
|
+
FROM debian:stable-slim
|
|
47
|
+
|
|
48
|
+
WORKDIR /app
|
|
49
|
+
|
|
50
|
+
# Copy Rust binary from builder stage
|
|
51
|
+
COPY --from=builder /app/server/target/release/titan-server ./titan-server
|
|
52
|
+
|
|
53
|
+
# Copy Titan routing metadata
|
|
54
|
+
COPY --from=builder /app/server/routes.json ./routes.json
|
|
55
|
+
COPY --from=builder /app/server/action_map.json ./action_map.json
|
|
56
|
+
|
|
57
|
+
# Copy Titan JS bundles
|
|
58
|
+
RUN mkdir -p /app/actions
|
|
59
|
+
COPY --from=builder /app/server/actions /app/actions
|
|
60
|
+
|
|
61
|
+
# Copy only Titan extensions
|
|
62
|
+
COPY --from=builder /app/.ext ./.ext
|
|
63
|
+
|
|
64
|
+
EXPOSE 3000
|
|
65
|
+
|
|
66
|
+
CMD ["./titan-server"]
|
|
@@ -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
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
use axum::{response::{IntoResponse, Json}, http::Request, body::Body};
|
|
2
|
+
use serde_json::json;
|
|
3
|
+
|
|
4
|
+
pub async fn run(_req: Request<Body>) -> impl IntoResponse {
|
|
5
|
+
// let _token = t.jwt.sign(json!({"id": 1}), "secret", Some(json!({"expiresIn": "1h"}))).unwrap_or_default();
|
|
6
|
+
// let decoded = t.jwt.verify(&_token, "secret").unwrap_or_default();
|
|
7
|
+
|
|
8
|
+
Json(json!({
|
|
9
|
+
"message": "Hello from Rust Action! 🦀",
|
|
10
|
+
"status": "blazing fast test",
|
|
11
|
+
// "token": _token,
|
|
12
|
+
// "decoded": decoded
|
|
13
|
+
}))
|
|
14
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import t from "../titan/titan.js";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
t.post("/hello").action("hello") // pass a json payload { "name": "titan" }
|
|
7
|
+
t.get("/rust").action("rust_hello") // This route uses a rust action
|
|
8
|
+
|
|
9
|
+
t.get("/").reply("Ready to land on Titan Planet 🚀");
|
|
10
|
+
|
|
11
|
+
t.start(3000, "Titan Running!");
|