@titanpl/cli 2.0.3 → 2.0.4
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 +5 -5
- package/src/commands/init.js +23 -2
- package/templates/common/Dockerfile +66 -0
- package/templates/common/_dockerignore +35 -0
- package/templates/common/_gitignore +33 -0
- package/templates/common/app/t.native.d.ts +2043 -0
- package/templates/common/app/t.native.js +39 -0
- package/templates/extension/README.md +69 -0
- package/templates/extension/index.d.ts +27 -0
- package/templates/extension/index.js +17 -0
- package/templates/extension/jsconfig.json +14 -0
- package/templates/extension/native/Cargo.toml +9 -0
- package/templates/extension/native/src/lib.rs +5 -0
- package/templates/extension/package-lock.json +522 -0
- package/templates/extension/package.json +26 -0
- package/templates/extension/titan.json +18 -0
- package/templates/js/app/actions/getuser.js +9 -0
- package/templates/js/app/app.js +7 -0
- package/templates/js/eslint.config.js +5 -0
- package/templates/js/jsconfig.json +27 -0
- package/templates/js/package.json +28 -0
- package/templates/rust-js/app/actions/getuser.js +9 -0
- package/templates/rust-js/app/actions/rust_hello.rs +14 -0
- package/templates/rust-js/app/app.js +9 -0
- package/templates/rust-js/eslint.config.js +5 -0
- package/templates/rust-js/jsconfig.json +27 -0
- package/templates/rust-js/package.json +27 -0
- package/templates/rust-js/titan/bundle.js +157 -0
- package/templates/rust-js/titan/dev.js +323 -0
- package/templates/rust-js/titan/titan.js +126 -0
- package/templates/rust-ts/app/actions/getuser.ts +9 -0
- package/templates/rust-ts/app/actions/rust_hello.rs +14 -0
- package/templates/rust-ts/app/app.ts +9 -0
- package/templates/rust-ts/eslint.config.js +12 -0
- package/templates/rust-ts/package.json +29 -0
- package/templates/rust-ts/titan/bundle.js +163 -0
- package/templates/rust-ts/titan/dev.js +435 -0
- package/templates/rust-ts/titan/titan.d.ts +19 -0
- package/templates/rust-ts/titan/titan.js +124 -0
- package/templates/rust-ts/tsconfig.json +28 -0
- package/templates/ts/app/actions/getuser.ts +9 -0
- package/templates/ts/app/app.ts +7 -0
- package/templates/ts/eslint.config.js +12 -0
- package/templates/ts/package.json +30 -0
- package/templates/ts/tsconfig.json +28 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import t from "@titanpl/route";
|
|
2
|
+
|
|
3
|
+
t.post("/hello").action("hello") // pass a json payload { "name": "titan" }
|
|
4
|
+
|
|
5
|
+
t.get("/rust").action("rust_hello") // This route uses a rust action
|
|
6
|
+
|
|
7
|
+
t.get("/").reply("Ready to land on Titan Planet 🚀");
|
|
8
|
+
|
|
9
|
+
t.start(5100, "Titan Running!");
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "titanpl",
|
|
3
|
+
"version": "2.0.4",
|
|
4
|
+
"description": "A Titan Planet server (Rust + TypeScript)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"titan": {
|
|
7
|
+
"template": "rust-ts"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"@titanpl/cli": "2.0.4",
|
|
11
|
+
"@titanpl/route": "2.0.4",
|
|
12
|
+
"@titanpl/native": "2.0.4",
|
|
13
|
+
"@titanpl/core": "latest",
|
|
14
|
+
"@titanpl/node": "latest",
|
|
15
|
+
"typescript": "^5.0.0"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "titan build",
|
|
19
|
+
"dev": "titan dev",
|
|
20
|
+
"start": "titan start",
|
|
21
|
+
"lint": "eslint .",
|
|
22
|
+
"lint:fix": "eslint . --fix"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"eslint": "^9.39.2",
|
|
26
|
+
"eslint-plugin-titanpl": "latest",
|
|
27
|
+
"@typescript-eslint/parser": "^8.54.0"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import esbuild from "esbuild";
|
|
4
|
+
|
|
5
|
+
export async function bundle() {
|
|
6
|
+
const root = process.cwd();
|
|
7
|
+
const actionsDir = path.join(root, "app", "actions");
|
|
8
|
+
const outDir = path.join(root, "server", "actions");
|
|
9
|
+
const rustOutDir = path.join(root, "server", "src", "actions_rust");
|
|
10
|
+
|
|
11
|
+
const start = Date.now();
|
|
12
|
+
await bundleJs(actionsDir, outDir);
|
|
13
|
+
await bundleRust(rustOutDir, actionsDir);
|
|
14
|
+
// console.log(`[Titan] Bundle finished in ${((Date.now() - start) / 1000).toFixed(2)}s`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function bundleJs(actionsDir, outDir) {
|
|
18
|
+
// console.log("[Titan] Bundling JS actions...");
|
|
19
|
+
|
|
20
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
21
|
+
|
|
22
|
+
// Clean old bundles
|
|
23
|
+
const oldFiles = fs.readdirSync(outDir);
|
|
24
|
+
for (const file of oldFiles) {
|
|
25
|
+
fs.unlinkSync(path.join(outDir, file));
|
|
26
|
+
}
|
|
27
|
+
|
|
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...`);
|
|
32
|
+
|
|
33
|
+
for (const file of files) {
|
|
34
|
+
const actionName = path.basename(file, path.extname(file));
|
|
35
|
+
|
|
36
|
+
const entry = path.join(actionsDir, file);
|
|
37
|
+
|
|
38
|
+
// Rust runtime expects `.jsbundle` extension — consistent with previous design
|
|
39
|
+
const outfile = path.join(outDir, actionName + ".jsbundle");
|
|
40
|
+
|
|
41
|
+
// console.log(`[Titan] Bundling ${entry} → ${outfile}`);
|
|
42
|
+
|
|
43
|
+
await esbuild.build({
|
|
44
|
+
entryPoints: [entry],
|
|
45
|
+
outfile,
|
|
46
|
+
bundle: true,
|
|
47
|
+
format: "iife",
|
|
48
|
+
globalName: "__titan_exports",
|
|
49
|
+
platform: "neutral",
|
|
50
|
+
target: "es2020",
|
|
51
|
+
logLevel: "silent",
|
|
52
|
+
banner: {
|
|
53
|
+
js: "const defineAction = (fn) => fn; const Titan = t;"
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
footer: {
|
|
57
|
+
js: `
|
|
58
|
+
(function () {
|
|
59
|
+
const fn =
|
|
60
|
+
__titan_exports["${actionName}"] ||
|
|
61
|
+
__titan_exports.default;
|
|
62
|
+
|
|
63
|
+
if (typeof fn !== "function") {
|
|
64
|
+
throw new Error("[Titan] Action '${actionName}' not found or not a function");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
globalThis["${actionName}"] = function(request_arg) {
|
|
68
|
+
globalThis.req = request_arg;
|
|
69
|
+
return fn(request_arg);
|
|
70
|
+
};
|
|
71
|
+
})();
|
|
72
|
+
`
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// console.log("[Titan] JS Bundling finished.");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function bundleRust(rustOutDir, actionsDir) {
|
|
81
|
+
// console.log("[Titan] Bundling Rust actions...");
|
|
82
|
+
|
|
83
|
+
// Fallback if called directly (though typically called via bundle)
|
|
84
|
+
const root = process.cwd();
|
|
85
|
+
if (!actionsDir) actionsDir = path.join(root, "app", "actions");
|
|
86
|
+
if (!rustOutDir) rustOutDir = path.join(root, "server", "src", "actions_rust");
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
if (!fs.existsSync(rustOutDir)) {
|
|
90
|
+
fs.mkdirSync(rustOutDir, { recursive: true });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Clean old rust actions
|
|
94
|
+
for (const file of fs.readdirSync(rustOutDir)) {
|
|
95
|
+
fs.unlinkSync(path.join(rustOutDir, file));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const files = fs.readdirSync(actionsDir).filter(f => f.endsWith(".rs"));
|
|
99
|
+
if (files.length > 0) {
|
|
100
|
+
// console.log(`[Titan] Bundling ${files.length} Rust actions...`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const modules = [];
|
|
104
|
+
|
|
105
|
+
for (const file of files) {
|
|
106
|
+
const actionName = path.basename(file, ".rs");
|
|
107
|
+
const entry = path.join(actionsDir, file);
|
|
108
|
+
let outfile = path.join(rustOutDir, file);
|
|
109
|
+
|
|
110
|
+
let content = fs.readFileSync(entry, 'utf-8');
|
|
111
|
+
|
|
112
|
+
// Prepend implicit imports if not present
|
|
113
|
+
let finalContent = content;
|
|
114
|
+
if (!content.includes("use crate::extensions::t;")) {
|
|
115
|
+
finalContent = "use crate::extensions::t;\n" + content;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Basic validation - check if it has a run function
|
|
119
|
+
if (!content.includes("async fn run")) {
|
|
120
|
+
console.warn(`[Titan] Warning: ${file} does not appear to have an 'async fn run'. It might fail to compile.`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
fs.writeFileSync(outfile, finalContent);
|
|
124
|
+
modules.push(actionName);
|
|
125
|
+
// console.log(`[Titan] Copied Rust action ${actionName}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Generate mod.rs
|
|
129
|
+
let modContent = `// Auto-generated by Titan. Do not edit.
|
|
130
|
+
use axum::response::IntoResponse;
|
|
131
|
+
use axum::http::Request;
|
|
132
|
+
use axum::body::Body;
|
|
133
|
+
use std::future::Future;
|
|
134
|
+
use std::pin::Pin;
|
|
135
|
+
|
|
136
|
+
`;
|
|
137
|
+
|
|
138
|
+
// Add mod declarations
|
|
139
|
+
for (const mod of modules) {
|
|
140
|
+
modContent += `pub mod ${mod};\n`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
modContent += `
|
|
144
|
+
pub type ActionFn = fn(Request<Body>) -> Pin<Box<dyn Future<Output = axum::response::Response> + Send>>;
|
|
145
|
+
|
|
146
|
+
pub fn get_action(name: &str) -> Option<ActionFn> {
|
|
147
|
+
match name {
|
|
148
|
+
`;
|
|
149
|
+
|
|
150
|
+
for (const mod of modules) {
|
|
151
|
+
modContent += ` "${mod}" => Some(|req| Box::pin(async move {
|
|
152
|
+
${mod}::run(req).await.into_response()
|
|
153
|
+
})),\n`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
modContent += ` _ => None
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
`;
|
|
160
|
+
|
|
161
|
+
fs.writeFileSync(path.join(rustOutDir, "mod.rs"), modContent);
|
|
162
|
+
// console.log("[Titan] Rust Bundling finished.");
|
|
163
|
+
}
|
|
@@ -0,0 +1,435 @@
|
|
|
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 esbuild from "esbuild";
|
|
7
|
+
import { createRequire } from "module";
|
|
8
|
+
|
|
9
|
+
// Required for __dirname in ES modules
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = path.dirname(__filename);
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
// Colors
|
|
15
|
+
const cyan = (t) => `\x1b[36m${t}\x1b[0m`;
|
|
16
|
+
const green = (t) => `\x1b[32m${t}\x1b[0m`;
|
|
17
|
+
const yellow = (t) => `\x1b[33m${t}\x1b[0m`;
|
|
18
|
+
const red = (t) => `\x1b[31m${t}\x1b[0m`;
|
|
19
|
+
const gray = (t) => `\x1b[90m${t}\x1b[0m`;
|
|
20
|
+
const bold = (t) => `\x1b[1m${t}\x1b[0m`;
|
|
21
|
+
|
|
22
|
+
function getTitanVersion() {
|
|
23
|
+
try {
|
|
24
|
+
const require = createRequire(import.meta.url);
|
|
25
|
+
const pkgPath = require.resolve("titanpl/package.json");
|
|
26
|
+
return JSON.parse(fs.readFileSync(pkgPath, "utf-8")).version;
|
|
27
|
+
} catch (e) {
|
|
28
|
+
try {
|
|
29
|
+
// Check levels up to find the framework root
|
|
30
|
+
let cur = __dirname;
|
|
31
|
+
for (let i = 0; i < 5; i++) {
|
|
32
|
+
const pkgPath = path.join(cur, "package.json");
|
|
33
|
+
if (fs.existsSync(pkgPath)) {
|
|
34
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
35
|
+
if (pkg.name === "titanpl") return pkg.version;
|
|
36
|
+
}
|
|
37
|
+
cur = path.join(cur, "..");
|
|
38
|
+
}
|
|
39
|
+
} catch (e2) { }
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
// Fallback to calling tit --version
|
|
43
|
+
const output = execSync("tit --version", { encoding: "utf-8" }).trim();
|
|
44
|
+
const match = output.match(/v(\d+\.\d+\.\d+)/);
|
|
45
|
+
if (match) return match[1];
|
|
46
|
+
} catch (e3) { }
|
|
47
|
+
}
|
|
48
|
+
return "0.1.0";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let serverProcess = null;
|
|
52
|
+
let isKilling = false;
|
|
53
|
+
let isFirstBoot = true;
|
|
54
|
+
|
|
55
|
+
async function killServer() {
|
|
56
|
+
if (!serverProcess) return;
|
|
57
|
+
|
|
58
|
+
isKilling = true;
|
|
59
|
+
const pid = serverProcess.pid;
|
|
60
|
+
const killPromise = new Promise((resolve) => {
|
|
61
|
+
if (serverProcess.exitCode !== null) return resolve();
|
|
62
|
+
serverProcess.once("close", resolve);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (process.platform === "win32") {
|
|
66
|
+
try {
|
|
67
|
+
execSync(`taskkill /pid ${pid} /f /t`, { stdio: 'ignore' });
|
|
68
|
+
} catch (e) {
|
|
69
|
+
// Ignore errors if process is already dead
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
serverProcess.kill();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
await killPromise;
|
|
77
|
+
} catch (e) { }
|
|
78
|
+
serverProcess = null;
|
|
79
|
+
isKilling = false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const delay = (ms) => new Promise(res => setTimeout(res, ms));
|
|
83
|
+
|
|
84
|
+
let spinnerTimer = null;
|
|
85
|
+
const frames = ["⏣", "⟐", "⟡", "⟠", "⟡", "⟐"];
|
|
86
|
+
let frameIdx = 0;
|
|
87
|
+
|
|
88
|
+
function startSpinner(text) {
|
|
89
|
+
if (spinnerTimer) clearInterval(spinnerTimer);
|
|
90
|
+
process.stdout.write("\x1B[?25l"); // Hide cursor
|
|
91
|
+
spinnerTimer = setInterval(() => {
|
|
92
|
+
process.stdout.write(`\r ${cyan(frames[frameIdx])} ${gray(text)}`);
|
|
93
|
+
frameIdx = (frameIdx + 1) % frames.length;
|
|
94
|
+
}, 80);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function stopSpinner(success = true, text = "") {
|
|
98
|
+
if (spinnerTimer) {
|
|
99
|
+
clearInterval(spinnerTimer);
|
|
100
|
+
spinnerTimer = null;
|
|
101
|
+
}
|
|
102
|
+
process.stdout.write("\r\x1B[K"); // Clear line
|
|
103
|
+
process.stdout.write("\x1B[?25h"); // Show cursor
|
|
104
|
+
if (text) {
|
|
105
|
+
if (success) {
|
|
106
|
+
console.log(` ${green("✔")} ${green(text)}`);
|
|
107
|
+
} else {
|
|
108
|
+
console.log(` ${red("✖")} ${red(text)}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function startRustServer(retryCount = 0) {
|
|
114
|
+
// If TS is broken, don't start
|
|
115
|
+
if (isTs && !isTsHealthy) {
|
|
116
|
+
stopSpinner(false, "Waiting for TypeScript errors to be fixed...");
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const waitTime = retryCount > 0 ? 1000 : 500;
|
|
121
|
+
|
|
122
|
+
await killServer();
|
|
123
|
+
await delay(waitTime);
|
|
124
|
+
|
|
125
|
+
const serverPath = path.join(process.cwd(), "server");
|
|
126
|
+
const startTime = Date.now();
|
|
127
|
+
|
|
128
|
+
startSpinner("Stabilizing your app on its orbit...");
|
|
129
|
+
|
|
130
|
+
let isReady = false;
|
|
131
|
+
let stdoutBuffer = "";
|
|
132
|
+
let buildLogs = "";
|
|
133
|
+
|
|
134
|
+
// If it takes more than 30s, update the message
|
|
135
|
+
const slowTimer = setTimeout(() => {
|
|
136
|
+
if (!isReady && !isKilling) {
|
|
137
|
+
startSpinner("Still stabilizing... (the first orbit takes longer)");
|
|
138
|
+
}
|
|
139
|
+
}, 30000);
|
|
140
|
+
|
|
141
|
+
serverProcess = spawn("cargo", ["run", "--quiet"], {
|
|
142
|
+
cwd: serverPath,
|
|
143
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
144
|
+
env: { ...process.env, CARGO_INCREMENTAL: "1" }
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
serverProcess.on("error", (err) => {
|
|
148
|
+
stopSpinner(false, "Failed to start orbit");
|
|
149
|
+
console.error(red(`[Titan] Error: ${err.message}`));
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
serverProcess.stderr.on("data", (data) => {
|
|
153
|
+
const str = data.toString();
|
|
154
|
+
if (isReady) {
|
|
155
|
+
process.stderr.write(data);
|
|
156
|
+
} else {
|
|
157
|
+
buildLogs += str;
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
serverProcess.stdout.on("data", (data) => {
|
|
162
|
+
const out = data.toString();
|
|
163
|
+
|
|
164
|
+
if (!isReady) {
|
|
165
|
+
stdoutBuffer += out;
|
|
166
|
+
if (stdoutBuffer.includes("Titan server running") || stdoutBuffer.includes("████████╗")) {
|
|
167
|
+
isReady = true;
|
|
168
|
+
clearTimeout(slowTimer);
|
|
169
|
+
stopSpinner(true, "Your app is now orbiting Titan Planet");
|
|
170
|
+
|
|
171
|
+
if (isFirstBoot) {
|
|
172
|
+
process.stdout.write(stdoutBuffer);
|
|
173
|
+
isFirstBoot = false;
|
|
174
|
+
} else {
|
|
175
|
+
// On subsequent reloads, only print non-banner lines from the buffer
|
|
176
|
+
const lines = stdoutBuffer.split("\n");
|
|
177
|
+
for (const line of lines) {
|
|
178
|
+
const isBanner = line.includes("Titan server running") ||
|
|
179
|
+
line.includes("████████╗") ||
|
|
180
|
+
line.includes("╚══") ||
|
|
181
|
+
line.includes(" ██║") ||
|
|
182
|
+
line.includes(" ╚═╝");
|
|
183
|
+
if (!isBanner && line.trim()) {
|
|
184
|
+
process.stdout.write(line + "\n");
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
stdoutBuffer = "";
|
|
189
|
+
}
|
|
190
|
+
} else {
|
|
191
|
+
process.stdout.write(data);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Monitor stderr for port binding errors
|
|
196
|
+
serverProcess.stderr.on("data", (data) => {
|
|
197
|
+
stderrBuffer += data.toString();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
serverProcess.on("close", async (code) => {
|
|
201
|
+
clearTimeout(slowTimer);
|
|
202
|
+
if (isKilling) return;
|
|
203
|
+
const runTime = Date.now() - startTime;
|
|
204
|
+
|
|
205
|
+
if (code !== 0 && code !== null) {
|
|
206
|
+
// Check for port binding errors
|
|
207
|
+
const isPortError = stderrBuffer.includes("Address already in use") ||
|
|
208
|
+
stderrBuffer.includes("address in use") ||
|
|
209
|
+
stderrBuffer.includes("os error 10048") || // Windows
|
|
210
|
+
stderrBuffer.includes("EADDRINUSE") ||
|
|
211
|
+
stderrBuffer.includes("AddrInUse");
|
|
212
|
+
|
|
213
|
+
if (isPortError) {
|
|
214
|
+
stopSpinner(false, "Orbit stabilization failed");
|
|
215
|
+
console.log("");
|
|
216
|
+
|
|
217
|
+
console.log(red("⏣ Your application cannot enter this orbit"));
|
|
218
|
+
console.log(red("↳ Another application is already bound to this port."));
|
|
219
|
+
console.log("");
|
|
220
|
+
|
|
221
|
+
console.log(yellow("Recommended Actions:"));
|
|
222
|
+
console.log(yellow(" 1.") + " Release the occupied orbit (stop the other service).");
|
|
223
|
+
console.log(yellow(" 2.") + " Assign your application to a new orbit in " + cyan("app/app.js"));
|
|
224
|
+
console.log(yellow(" Example: ") + cyan('t.start(3001, "Titan Running!")'));
|
|
225
|
+
console.log("");
|
|
226
|
+
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
stopSpinner(false, "Orbit stabilization failed");
|
|
232
|
+
|
|
233
|
+
// Debug: Show stderr if it's not empty and not a port error
|
|
234
|
+
if (stderrBuffer && stderrBuffer.trim()) {
|
|
235
|
+
console.log(gray("\n[Debug] Cargo stderr:"));
|
|
236
|
+
console.log(gray(stderrBuffer.substring(0, 500))); // Show first 500 chars
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (runTime < 15000 && retryCount < maxRetries) {
|
|
240
|
+
await delay(2000);
|
|
241
|
+
await startRustServer(retryCount + 1);
|
|
242
|
+
} else if (retryCount >= maxRetries) {
|
|
243
|
+
console.log(gray("\n[Titan] Waiting for changes to retry..."));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function rebuild() {
|
|
250
|
+
if (isTs && !isTsHealthy) return; // Don't rebuild if TS is broken
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
const root = process.cwd();
|
|
254
|
+
const appTs = path.join(root, "app", "app.ts");
|
|
255
|
+
const dotTitan = path.join(root, ".titan");
|
|
256
|
+
const compiledApp = path.join(dotTitan, "app.js");
|
|
257
|
+
|
|
258
|
+
if (fs.existsSync(appTs)) {
|
|
259
|
+
if (!fs.existsSync(dotTitan)) fs.mkdirSync(dotTitan, { recursive: true });
|
|
260
|
+
|
|
261
|
+
await esbuild.build({
|
|
262
|
+
entryPoints: [appTs],
|
|
263
|
+
outfile: compiledApp,
|
|
264
|
+
bundle: true,
|
|
265
|
+
platform: "node",
|
|
266
|
+
format: "esm",
|
|
267
|
+
external: ["fs", "path", "esbuild", "chokidar", "typescript"],
|
|
268
|
+
logLevel: "silent"
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
execSync(`node "${compiledApp}"`, { stdio: "inherit" });
|
|
272
|
+
} else {
|
|
273
|
+
execSync("node app/app.js", { stdio: "ignore" });
|
|
274
|
+
}
|
|
275
|
+
} catch (e) {
|
|
276
|
+
stopSpinner(false, "Failed to prepare runtime");
|
|
277
|
+
console.log(red(`[Titan] Error: ${e.message}`));
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
let tsProcess = null;
|
|
282
|
+
let isTsHealthy = false; // STRICT: Assume unhealthy until checked
|
|
283
|
+
|
|
284
|
+
function startTypeChecker() {
|
|
285
|
+
const root = process.cwd();
|
|
286
|
+
if (!fs.existsSync(path.join(root, "tsconfig.json"))) return;
|
|
287
|
+
|
|
288
|
+
let tscPath;
|
|
289
|
+
try {
|
|
290
|
+
const require = createRequire(import.meta.url);
|
|
291
|
+
tscPath = require.resolve("typescript/bin/tsc");
|
|
292
|
+
} catch (e) {
|
|
293
|
+
tscPath = path.join(root, "node_modules", "typescript", "bin", "tsc");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (!fs.existsSync(tscPath)) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const args = [tscPath, "--noEmit", "--watch", "--preserveWatchOutput", "--pretty"];
|
|
301
|
+
|
|
302
|
+
tsProcess = spawn(process.execPath, args, {
|
|
303
|
+
cwd: root,
|
|
304
|
+
stdio: "pipe",
|
|
305
|
+
shell: false
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
tsProcess.stdout.on("data", (data) => {
|
|
309
|
+
const lines = data.toString().split("\n");
|
|
310
|
+
for (const line of lines) {
|
|
311
|
+
if (line.trim().includes("File change detected") || line.trim().includes("Starting compilation")) {
|
|
312
|
+
isTsHealthy = false;
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
if (line.includes("Found 0 errors")) {
|
|
316
|
+
isTsHealthy = true;
|
|
317
|
+
// TS is happy, so we rebuild and restart (or start) the server
|
|
318
|
+
rebuild().then(startRustServer);
|
|
319
|
+
|
|
320
|
+
} else if (line.includes("error TS")) {
|
|
321
|
+
isTsHealthy = false;
|
|
322
|
+
if (serverProcess) {
|
|
323
|
+
console.log(red(`[Titan] TypeScript error detected. Stopping server...`));
|
|
324
|
+
killServer();
|
|
325
|
+
}
|
|
326
|
+
process.stdout.write(line + "\n");
|
|
327
|
+
} else if (line.match(/Found [1-9]\d* error/)) {
|
|
328
|
+
isTsHealthy = false;
|
|
329
|
+
if (serverProcess) {
|
|
330
|
+
console.log(red(`[Titan] TypeScript compilation failed. Stopping server...`));
|
|
331
|
+
killServer();
|
|
332
|
+
}
|
|
333
|
+
process.stdout.write(line + "\n");
|
|
334
|
+
} else if (line.trim()) {
|
|
335
|
+
process.stdout.write(gray(`[TS] ${line}\n`));
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
tsProcess.stderr.on("data", (data) => {
|
|
341
|
+
process.stdout.write(data);
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
let isTs = false;
|
|
346
|
+
|
|
347
|
+
async function startDev() {
|
|
348
|
+
const root = process.cwd();
|
|
349
|
+
const actionsDir = path.join(root, "app", "actions");
|
|
350
|
+
let hasRust = false;
|
|
351
|
+
if (fs.existsSync(actionsDir)) {
|
|
352
|
+
hasRust = fs.readdirSync(actionsDir).some(f => f.endsWith(".rs"));
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
isTs = fs.existsSync(path.join(root, "tsconfig.json")) ||
|
|
356
|
+
fs.existsSync(path.join(root, "app", "app.ts"));
|
|
357
|
+
|
|
358
|
+
let mode = "";
|
|
359
|
+
if (hasRust) {
|
|
360
|
+
mode = isTs ? "Rust + TS Actions" : "Rust + JS Actions";
|
|
361
|
+
} else {
|
|
362
|
+
mode = isTs ? "TS Actions" : "JS Actions";
|
|
363
|
+
}
|
|
364
|
+
const version = getTitanVersion();
|
|
365
|
+
|
|
366
|
+
console.clear();
|
|
367
|
+
console.log("");
|
|
368
|
+
console.log(` ${bold(cyan("⏣ Titan Planet"))} ${gray("v" + version)} ${yellow("[ Dev Mode ]")}`);
|
|
369
|
+
console.log("");
|
|
370
|
+
console.log(` ${gray("Type: ")} ${mode}`);
|
|
371
|
+
console.log(` ${gray("Hot Reload: ")} ${green("Enabled")}`);
|
|
372
|
+
|
|
373
|
+
if (fs.existsSync(path.join(root, ".env"))) {
|
|
374
|
+
console.log(` ${gray("Env: ")} ${yellow("Loaded")}`);
|
|
375
|
+
}
|
|
376
|
+
console.log("");
|
|
377
|
+
|
|
378
|
+
if (isTs) {
|
|
379
|
+
startTypeChecker();
|
|
380
|
+
} else {
|
|
381
|
+
// If no TS, start immediately
|
|
382
|
+
try {
|
|
383
|
+
await rebuild();
|
|
384
|
+
await startRustServer();
|
|
385
|
+
} catch (e) {
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const watcher = chokidar.watch(["app", ".env"], {
|
|
390
|
+
ignoreInitial: true,
|
|
391
|
+
awaitWriteFinish: { stabilityThreshold: 500, pollInterval: 100 }
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
let timer = null;
|
|
395
|
+
watcher.on("all", async (event, file) => {
|
|
396
|
+
if (timer) clearTimeout(timer);
|
|
397
|
+
timer = setTimeout(async () => {
|
|
398
|
+
// If TS, we rely on TCS to trigger the rebuild (via Found 0 errors)
|
|
399
|
+
// We verify path safety using absolute/relative calculations
|
|
400
|
+
const relPath = path.relative(root, file);
|
|
401
|
+
if (isTs && (relPath.startsWith("app") || relPath.startsWith("app" + path.sep))) return;
|
|
402
|
+
|
|
403
|
+
// If TS is broken, rebuild() checks will prevent update, keeping server dead
|
|
404
|
+
// If TS is healthy, we proceed
|
|
405
|
+
if (isTs && !isTsHealthy) return;
|
|
406
|
+
|
|
407
|
+
try {
|
|
408
|
+
await killServer();
|
|
409
|
+
await rebuild();
|
|
410
|
+
await startRustServer();
|
|
411
|
+
} catch (e) {
|
|
412
|
+
// console.log(red("[Titan] Build failed -- waiting for changes..."));
|
|
413
|
+
}
|
|
414
|
+
}, 300);
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
async function handleExit() {
|
|
419
|
+
stopSpinner();
|
|
420
|
+
console.log(gray("\n[Titan] Stopping server..."));
|
|
421
|
+
await killServer();
|
|
422
|
+
if (tsProcess) {
|
|
423
|
+
if (process.platform === "win32") {
|
|
424
|
+
try { execSync(`taskkill /pid ${tsProcess.pid} /f /t`, { stdio: 'ignore' }); } catch (e) { }
|
|
425
|
+
} else {
|
|
426
|
+
tsProcess.kill();
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
process.exit(0);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
process.on("SIGINT", handleExit);
|
|
433
|
+
process.on("SIGTERM", handleExit);
|
|
434
|
+
|
|
435
|
+
startDev();
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// -- Module Definitions (for imports from "titan") --
|
|
2
|
+
|
|
3
|
+
export * from "./runtime.js";
|
|
4
|
+
|
|
5
|
+
export interface RouteHandler {
|
|
6
|
+
reply(value: any): void;
|
|
7
|
+
action(name: string): void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface TitanBuilder {
|
|
11
|
+
get(route: string): RouteHandler;
|
|
12
|
+
post(route: string): RouteHandler;
|
|
13
|
+
log(module: string, msg: string): void;
|
|
14
|
+
start(port?: number, msg?: string, threads?: number): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
declare const builder: TitanBuilder;
|
|
18
|
+
export const Titan: TitanBuilder;
|
|
19
|
+
export default builder;
|