@idajs/create-mod 0.2.10 → 0.2.15-dev.19

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.
@@ -0,0 +1,25 @@
1
+ # AGENTS.md
2
+
3
+ ## What is IdaJS
4
+
5
+ IdaJS is a JavaScript-powered modding engine for Little Big Adventure 2 (LBA2). It lets you create mods for the classic LBA2 game using JavaScript or TypeScript — modifying scenes, characters, behaviors, dialogs, and more through a modern scripting API.
6
+
7
+ ## API Reference
8
+
9
+ The full IdaJS TypeScript API with JSDoc documentation is available in `./node_modules/@idajs/types/`, structured by entities. The global objects (such as `scene`, `ida`, `text`, `object`, etc.) are defined in `global.d.ts`. Always consult these type definitions when implementing any feature.
10
+
11
+ ## Mod Execution Phases
12
+
13
+ Different phases of mod execution allow different APIs. Be aware which phase you are in:
14
+
15
+ - **Scene setup phase** (`scene.Events.afterLoadScene` handler): Configure the scene before gameplay starts. Use `scene`, `object`, `text`, zone and waypoint APIs to add/modify objects, zones, waypoints, register life script handlers, register coroutines, and set initial state. Do **not** call life or move script commands here.
16
+ - **Life script handler** (`handleLifeScript`): Runs every frame per actor. Use `ida.life()` and `ida.lifef()` for life script commands/functions (conditions, dialogs, state changes). Can start/pause/stop coroutines from here.
17
+ - **Coroutine (move script)** (generator function registered via `registerCoroutine`): Runs across multiple frames. Use `yield doMove()` for move commands (animations, movement, timing) and other `yield do...()` helpers (`doSceneStore`, `doGameStore`, etc.). Cannot check game conditions directly — that logic belongs in life scripts.
18
+
19
+ ## Mod Examples
20
+
21
+ For examples of working mods, look at `Ida/Samples` in the IdaJS installation folder. The path to the installation folder is written in a JSON file either in the user's home directory: `~/.idajs.json` or in the root directory of this project.
22
+
23
+ ## Web Documentation
24
+
25
+ Full readme, API reference, and guides are available at: https://ida.innerbytes.com
@@ -0,0 +1 @@
1
+ @AGENTS.md
@@ -1,6 +1,4 @@
1
1
  {
2
- "recommendations": [
3
- "prettier.prettier-vscode"
4
- ],
2
+ "recommendations": ["esbenp.prettier-vscode"],
5
3
  "unwantedRecommendations": []
6
4
  }
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "author": "Andriy Tevelyev",
3
- "private": true,
4
- "scripts": {
5
- "setup": "node ../install.js",
6
- "watch": "node watch.js",
7
- "sync": "node sync.js %npm_package_name%",
8
- "start": "powershell -ExecutionPolicy Bypass -File run.ps1 %npm_package_name%",
9
- "build": "node build.js %npm_package_name%",
10
- "update:types": "npm update @idajs/types"
11
- },
3
+ "private": true,
4
+ "scripts": {
5
+ "setup": "node ../install.js",
6
+ "watch": "node watch.js",
7
+ "sync": "node sync.js",
8
+ "start": "node start.js",
9
+ "build": "node build.js %npm_package_name%",
10
+ "update:types": "npm update @idajs/types"
11
+ },
12
12
  "devDependencies": {
13
13
  "archiver": "^7.0.1",
14
14
  "chokidar-cli": "^3.0.0",
@@ -0,0 +1,120 @@
1
+ const fs = require("fs");
2
+ const os = require("os");
3
+ const path = require("path");
4
+
5
+ const DEFAULT_PORT = 7770;
6
+
7
+ function getArgs(argv = process.argv.slice(2)) {
8
+ return argv;
9
+ }
10
+
11
+ function getArgValue(name, argv = getArgs()) {
12
+ const index = argv.findIndex((arg) => arg === name || arg.startsWith(`${name}=`));
13
+ if (index === -1) {
14
+ return null;
15
+ }
16
+
17
+ const arg = argv[index];
18
+ if (arg.includes("=")) {
19
+ return arg.split("=").slice(1).join("=");
20
+ }
21
+
22
+ return argv[index + 1] || null;
23
+ }
24
+
25
+ function getPackageName(projectDir = process.cwd()) {
26
+ const packagePath = path.join(projectDir, "package.json");
27
+
28
+ if (!fs.existsSync(packagePath)) {
29
+ throw new Error(`package.json not found in ${projectDir}`);
30
+ }
31
+
32
+ const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8"));
33
+ if (!packageJson.name) {
34
+ throw new Error("package.json is missing the 'name' field");
35
+ }
36
+
37
+ return packageJson.name;
38
+ }
39
+
40
+ function getIdaJsPath(projectDir = process.cwd()) {
41
+ const configPaths = [path.join(projectDir, ".idajs.json"), path.join(os.homedir(), ".idajs.json")];
42
+
43
+ for (const configPath of configPaths) {
44
+ if (!fs.existsSync(configPath)) {
45
+ continue;
46
+ }
47
+
48
+ try {
49
+ const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
50
+ if (config.installDir) {
51
+ return config.installDir;
52
+ }
53
+ } catch (error) {
54
+ // Ignore parse failures and keep looking.
55
+ }
56
+ }
57
+
58
+ return null;
59
+ }
60
+
61
+ function getIdaJsServer(projectDir = process.cwd()) {
62
+ const configPaths = [path.join(projectDir, ".idajs.json"), path.join(os.homedir(), ".idajs.json")];
63
+
64
+ for (const configPath of configPaths) {
65
+ if (!fs.existsSync(configPath)) {
66
+ continue;
67
+ }
68
+
69
+ try {
70
+ const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
71
+ if (config.server) {
72
+ return config.server;
73
+ }
74
+ } catch (error) {
75
+ // Ignore parse failures and keep looking.
76
+ }
77
+ }
78
+
79
+ return null;
80
+ }
81
+
82
+ function isTypeScriptProject(projectDir = process.cwd()) {
83
+ const tsConfigPath = path.join(projectDir, "tsconfig.json");
84
+ const srcDir = path.join(projectDir, "src");
85
+
86
+ if (!fs.existsSync(tsConfigPath) || !fs.existsSync(srcDir)) {
87
+ return false;
88
+ }
89
+
90
+ const files = fs.readdirSync(srcDir);
91
+ return files.some((file) => file.endsWith(".ts"));
92
+ }
93
+
94
+ function parseServerAddress(serverInput) {
95
+ if (!serverInput) {
96
+ throw new Error("Server address is required. Use --server <host[:port]>.");
97
+ }
98
+
99
+ const trimmedInput = String(serverInput).trim();
100
+ const withProtocol = trimmedInput.includes("://") ? trimmedInput : `http://${trimmedInput}`;
101
+ const url = new URL(withProtocol);
102
+
103
+ return {
104
+ host: url.hostname,
105
+ port: Number(url.port || DEFAULT_PORT),
106
+ origin: `${url.protocol}//${url.hostname}:${url.port || DEFAULT_PORT}`,
107
+ value: `${url.hostname}:${url.port || DEFAULT_PORT}`,
108
+ };
109
+ }
110
+
111
+ module.exports = {
112
+ DEFAULT_PORT,
113
+ getArgValue,
114
+ getArgs,
115
+ getIdaJsPath,
116
+ getIdaJsServer,
117
+ getPackageName,
118
+ isTypeScriptProject,
119
+ parseServerAddress,
120
+ };
@@ -0,0 +1,171 @@
1
+ const archiver = require("archiver");
2
+ const fs = require("fs");
3
+ const http = require("http");
4
+ const https = require("https");
5
+ const path = require("path");
6
+ const { spawn } = require("child_process");
7
+
8
+ const { getPackageName } = require("./project");
9
+
10
+ function runNodeScript(scriptName, args = []) {
11
+ return new Promise((resolve, reject) => {
12
+ const child = spawn(process.execPath, [path.join(__dirname, scriptName), ...args], {
13
+ cwd: process.cwd(),
14
+ stdio: "inherit",
15
+ });
16
+
17
+ child.on("error", reject);
18
+ child.on("exit", (code) => {
19
+ if (code === 0) {
20
+ resolve();
21
+ return;
22
+ }
23
+
24
+ reject(new Error(`${scriptName} exited with code ${code}`));
25
+ });
26
+ });
27
+ }
28
+
29
+ function createZip(zipPath, sourceDir, rootName) {
30
+ return new Promise((resolve, reject) => {
31
+ const output = fs.createWriteStream(zipPath);
32
+ const archive = archiver("zip", {
33
+ store: true,
34
+ });
35
+
36
+ output.on("close", resolve);
37
+ output.on("error", reject);
38
+ archive.on("error", reject);
39
+ archive.on("warning", (error) => {
40
+ if (error.code !== "ENOENT") {
41
+ reject(error);
42
+ }
43
+ });
44
+
45
+ archive.pipe(output);
46
+ archive.directory(sourceDir, rootName);
47
+ archive.finalize();
48
+ });
49
+ }
50
+
51
+ function request(server, method, requestPath, body, headers = {}) {
52
+ return new Promise((resolve, reject) => {
53
+ const client = server.origin.startsWith("https://") ? https : http;
54
+ const options = {
55
+ method,
56
+ hostname: server.host,
57
+ port: server.port,
58
+ path: requestPath,
59
+ headers,
60
+ };
61
+
62
+ const req = client.request(options, (res) => {
63
+ const chunks = [];
64
+ res.on("data", (chunk) => chunks.push(chunk));
65
+ res.on("end", () => {
66
+ const responseBody = Buffer.concat(chunks).toString("utf8");
67
+
68
+ if (res.statusCode >= 200 && res.statusCode < 300) {
69
+ if (!responseBody) {
70
+ resolve(null);
71
+ return;
72
+ }
73
+
74
+ try {
75
+ resolve(JSON.parse(responseBody));
76
+ } catch (error) {
77
+ resolve(responseBody);
78
+ }
79
+ return;
80
+ }
81
+
82
+ reject(
83
+ new Error(
84
+ `${method} ${requestPath} failed with ${res.statusCode}: ${responseBody || "no body"}`
85
+ )
86
+ );
87
+ });
88
+ });
89
+
90
+ req.on("error", (error) => {
91
+ if (error.code === "EHOSTUNREACH" || error.code === "ECONNREFUSED" || error.code === "ETIMEDOUT") {
92
+ reject(
93
+ new Error(
94
+ `Cannot reach remote Ida listener at ${server.host}:${server.port}. ` +
95
+ `Start it on the Windows machine with 'npm run listen' in the Ida folder and verify the host/port is reachable. ` +
96
+ `Original error: ${error.code}`
97
+ )
98
+ );
99
+ return;
100
+ }
101
+
102
+ reject(error);
103
+ });
104
+
105
+ if (body) {
106
+ req.write(body);
107
+ }
108
+
109
+ req.end();
110
+ });
111
+ }
112
+
113
+ async function stageMod(targetRoot) {
114
+ const args = [];
115
+ if (targetRoot) {
116
+ args.push("--target-root", targetRoot);
117
+ }
118
+
119
+ await runNodeScript("sync.js", args);
120
+ }
121
+
122
+ async function stageAndZip(targetRoot) {
123
+ const modName = getPackageName();
124
+ const stagedModDir = path.join(targetRoot, modName);
125
+ const zipPath = path.join(targetRoot, `${modName}.zip`);
126
+
127
+ await stageMod(targetRoot);
128
+ if (fs.existsSync(zipPath)) {
129
+ fs.rmSync(zipPath, { force: true });
130
+ }
131
+ await createZip(zipPath, stagedModDir, modName);
132
+
133
+ return { modName, stagedModDir, zipPath };
134
+ }
135
+
136
+ async function uploadMod(server, modName, zipPath) {
137
+ const body = fs.readFileSync(zipPath);
138
+ return request(server, "POST", `/sync?modName=${encodeURIComponent(modName)}`, body, {
139
+ "Content-Type": "application/zip",
140
+ "Content-Length": Buffer.byteLength(body),
141
+ });
142
+ }
143
+
144
+ async function startRemoteGame(server, modName) {
145
+ return request(
146
+ server,
147
+ "POST",
148
+ "/game/start",
149
+ Buffer.from(JSON.stringify({ modName }), "utf8"),
150
+ {
151
+ "Content-Type": "application/json",
152
+ }
153
+ );
154
+ }
155
+
156
+ async function killRemoteGame(server) {
157
+ return request(server, "POST", "/game/kill");
158
+ }
159
+
160
+ async function getRemoteGameStatus(server) {
161
+ return request(server, "GET", "/game/status");
162
+ }
163
+
164
+ module.exports = {
165
+ getRemoteGameStatus,
166
+ killRemoteGame,
167
+ stageMod,
168
+ stageAndZip,
169
+ startRemoteGame,
170
+ uploadMod,
171
+ };
@@ -0,0 +1,75 @@
1
+ const fs = require("fs");
2
+ const os = require("os");
3
+ const path = require("path");
4
+ const { spawn } = require("child_process");
5
+
6
+ const { getArgValue, parseServerAddress } = require("./project");
7
+ const { getRemoteGameStatus, stageAndZip, startRemoteGame, uploadMod } = require("./remote");
8
+
9
+ async function main() {
10
+ const args = process.argv.slice(2);
11
+ const server = parseServerAddress(getArgValue("--server", args));
12
+ const sessionRoot = fs.mkdtempSync(path.join(os.tmpdir(), "idajs-remote-"));
13
+ let cleanedUp = false;
14
+
15
+ const cleanup = () => {
16
+ if (cleanedUp) {
17
+ return;
18
+ }
19
+
20
+ cleanedUp = true;
21
+ fs.rmSync(sessionRoot, { recursive: true, force: true });
22
+ };
23
+
24
+ try {
25
+ console.log(`Checking remote Ida listener at ${server.origin}...`);
26
+ await getRemoteGameStatus(server);
27
+
28
+ console.log(`Using remote staging directory: ${sessionRoot}`);
29
+ const { modName, zipPath } = await stageAndZip(sessionRoot);
30
+ await uploadMod(server, modName, zipPath);
31
+ await startRemoteGame(server, modName);
32
+
33
+ const child = spawn(
34
+ process.execPath,
35
+ [
36
+ path.join(__dirname, "watch.js"),
37
+ "--server",
38
+ server.value,
39
+ "--session-root",
40
+ sessionRoot,
41
+ ],
42
+ {
43
+ cwd: process.cwd(),
44
+ stdio: "inherit",
45
+ }
46
+ );
47
+
48
+ child.on("exit", (code) => {
49
+ cleanup();
50
+ process.exit(code ?? 0);
51
+ });
52
+
53
+ child.on("error", (error) => {
54
+ cleanup();
55
+ console.error(error.message);
56
+ process.exit(1);
57
+ });
58
+
59
+ process.on("SIGINT", () => {
60
+ child.kill("SIGINT");
61
+ });
62
+
63
+ process.on("SIGTERM", () => {
64
+ child.kill("SIGTERM");
65
+ });
66
+ } catch (error) {
67
+ cleanup();
68
+ throw error;
69
+ }
70
+ }
71
+
72
+ main().catch((error) => {
73
+ console.error(error.message);
74
+ process.exit(1);
75
+ });
@@ -7,14 +7,14 @@
7
7
  "*.ts": "typescript"
8
8
  },
9
9
  "[javascript]": {
10
- "editor.defaultFormatter": "prettier.prettier-vscode",
10
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
11
11
  "editor.tabSize": 2,
12
12
  "editor.wordWrap": "wordWrapColumn",
13
13
  "editor.wordWrapColumn": 100,
14
14
  "editor.rulers": [100]
15
15
  },
16
16
  "[typescript]": {
17
- "editor.defaultFormatter": "prettier.prettier-vscode",
17
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
18
18
  "editor.tabSize": 2,
19
19
  "editor.wordWrap": "wordWrapColumn",
20
20
  "editor.wordWrapColumn": 100,
@@ -0,0 +1,60 @@
1
+ const path = require("path");
2
+ const { execFileSync, spawn } = require("child_process");
3
+
4
+ const { getArgValue, getIdaJsPath, getIdaJsServer, getPackageName } = require("./project");
5
+
6
+ function runRemote(args) {
7
+ const child = spawn(process.execPath, [path.join(__dirname, "run-remote.js"), ...args], {
8
+ cwd: process.cwd(),
9
+ stdio: "inherit",
10
+ });
11
+
12
+ child.on("exit", (code) => {
13
+ process.exit(code ?? 0);
14
+ });
15
+
16
+ child.on("error", (error) => {
17
+ console.error(error.message);
18
+ process.exit(1);
19
+ });
20
+ }
21
+
22
+ function runLocal() {
23
+ try {
24
+ execFileSync(
25
+ "powershell",
26
+ [
27
+ "-ExecutionPolicy",
28
+ "Bypass",
29
+ "-File",
30
+ path.join(__dirname, "run.ps1"),
31
+ getPackageName(),
32
+ ],
33
+ {
34
+ cwd: process.cwd(),
35
+ stdio: "inherit",
36
+ }
37
+ );
38
+ process.exit(0);
39
+ } catch (error) {
40
+ if (typeof error.status === "number") {
41
+ process.exit(error.status);
42
+ }
43
+
44
+ console.error(error.message);
45
+ process.exit(1);
46
+ }
47
+ }
48
+
49
+ const args = process.argv.slice(2);
50
+ const explicitServer = getArgValue("--server", args);
51
+ const installDir = getIdaJsPath();
52
+ const configuredServer = getIdaJsServer();
53
+
54
+ if (explicitServer) {
55
+ runRemote(args);
56
+ } else if (!installDir && configuredServer) {
57
+ runRemote(["--server", configuredServer]);
58
+ } else {
59
+ runLocal();
60
+ }
@@ -1,88 +1,50 @@
1
1
  const { execSync } = require("child_process");
2
2
  const path = require("path");
3
3
  const fs = require("fs");
4
- const os = require("os");
5
4
 
6
- // Function to read IdaJS installation directory from .idajs.json
7
- function getIdaJsPath() {
8
- // Try local config first, then user home config
9
- const configPaths = [path.join(__dirname, ".idajs.json"), path.join(os.homedir(), ".idajs.json")];
5
+ const { getArgValue, getIdaJsPath, getPackageName, isTypeScriptProject } = require("./project");
10
6
 
11
- for (const configPath of configPaths) {
12
- if (fs.existsSync(configPath)) {
13
- try {
14
- const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
15
- if (config.installDir) {
16
- return config.installDir;
17
- }
18
- } catch (err) {
19
- // Ignore parse errors and try next config
20
- }
21
- }
22
- }
23
-
24
- return null;
25
- }
26
-
27
- // Get package name from command line argument
28
- const packageName = process.argv[2];
29
-
30
- if (!packageName) {
31
- console.error("Error: Package name is required as an argument");
32
- console.error("Usage: node sync.js <package-name>");
33
- process.exit(1);
34
- }
7
+ const args = process.argv.slice(2);
8
+ const packageName = getPackageName();
9
+ const targetRoot = getArgValue("--target-root", args);
35
10
 
36
- // Get IdaJS installation path
37
- const idaJsPath = getIdaJsPath();
38
- if (!idaJsPath) {
39
- console.error("Error: .idajs.json not found in current directory or user home directory.");
40
- console.error("Please build IdaJS first or create .idajs.json with 'installDir' property.");
41
- process.exit(1);
42
- }
11
+ let resolvedTargetRoot = targetRoot;
12
+ const typeScriptProject = isTypeScriptProject();
13
+ if (!resolvedTargetRoot) {
14
+ const idaJsPath = getIdaJsPath();
43
15
 
44
- if (!fs.existsSync(idaJsPath)) {
45
- console.error(`Error: IdaJS installation directory does not exist: ${idaJsPath}`);
46
- console.error("Please build IdaJS first.");
47
- process.exit(1);
48
- }
49
-
50
- const isTypeScriptProject = (() => {
51
- // Check if tsconfig.json exists
52
- if (!fs.existsSync("tsconfig.json")) {
53
- return false;
16
+ if (!idaJsPath) {
17
+ console.error("Error: .idajs.json not found in current directory or user home directory.");
18
+ console.error("Please build IdaJS first or create .idajs.json with 'installDir' property.");
19
+ process.exit(1);
54
20
  }
55
21
 
56
- // Check if there are any .ts files in src/
57
- if (!fs.existsSync("src")) {
58
- return false;
22
+ if (!fs.existsSync(idaJsPath)) {
23
+ console.error(`Error: IdaJS installation directory does not exist: ${idaJsPath}`);
24
+ console.error("Please build IdaJS first.");
25
+ process.exit(1);
59
26
  }
60
27
 
61
- const files = fs.readdirSync("src");
62
- return files.some((file) => file.endsWith(".ts"));
63
- })();
28
+ resolvedTargetRoot = path.join(idaJsPath, "GameRun", "mods");
29
+ }
64
30
 
65
- const targetBase = path.join(idaJsPath, "GameRun", "mods", packageName);
31
+ const targetBase = path.join(resolvedTargetRoot, packageName);
66
32
  const deleteExcludeArgs = '--delete-exclude "*.ida" --delete-exclude "*.md5"';
67
33
 
68
34
  console.log(`Syncing ${packageName}...`);
69
35
 
70
- // If TypeScript project, compile first
71
- if (isTypeScriptProject) {
36
+ if (typeScriptProject) {
72
37
  console.log("TypeScript project detected, compiling...");
73
38
 
74
- // Clean dist folder
75
39
  if (fs.existsSync("dist")) {
76
40
  console.log("Cleaning dist folder...");
77
41
  fs.rmSync("dist", { recursive: true, force: true });
78
42
  }
79
43
 
80
- // Compile TypeScript
81
44
  execSync("tsc", { stdio: "inherit" });
82
45
  console.log("Compilation complete!");
83
46
  }
84
47
 
85
- // Sync media folder if it exists
86
48
  if (fs.existsSync("media")) {
87
49
  const mediaSyncCmd = `idasync media "${path.join(targetBase, "media")}" ${deleteExcludeArgs}`;
88
50
  console.log(`Running: ${mediaSyncCmd}`);
@@ -91,8 +53,7 @@ if (fs.existsSync("media")) {
91
53
  console.log("No media folder found, skipping media sync");
92
54
  }
93
55
 
94
- // Sync src folder
95
- const sourceFolder = isTypeScriptProject ? "dist" : "src";
56
+ const sourceFolder = typeScriptProject ? "dist" : "src";
96
57
  const srcSyncCmd = `idasync "${sourceFolder}" "${targetBase}" ${deleteExcludeArgs} --delete-exclude "media/*"`;
97
58
  console.log(`Running: ${srcSyncCmd}`);
98
59
  execSync(srcSyncCmd, { stdio: "inherit" });
@@ -1,54 +1,121 @@
1
1
  /**
2
- * Watches for file changes in the src/ and media/ directories
3
- * and runs the sync script to copy changed files to the game mod directory.
4
- * Exits when the LBA2.exe process is no longer running.
2
+ * Watches for file changes in the src/ and media/ directories.
3
+ * Local mode syncs directly into the game mod directory.
4
+ * Remote mode stages the mod into a session temp folder and uploads a full zip.
5
5
  */
6
6
 
7
7
  const chokidar = require("chokidar");
8
- const { spawn, exec } = require("child_process");
8
+ const { exec } = require("child_process");
9
9
  const { promisify } = require("util");
10
10
 
11
+ const { getArgValue, parseServerAddress } = require("./project");
12
+ const { getRemoteGameStatus, killRemoteGame, stageAndZip, stageMod, uploadMod } = require("./remote");
13
+
11
14
  const execAsync = promisify(exec);
12
15
  const PROC_NAME = "LBA2.exe";
16
+ const args = process.argv.slice(2);
17
+ const serverArg = getArgValue("--server", args);
18
+ const sessionRoot = getArgValue("--session-root", args);
19
+ const server = serverArg ? parseServerAddress(serverArg) : null;
20
+
21
+ if (server && !sessionRoot) {
22
+ console.error("Remote watch mode requires --session-root <path>.");
23
+ process.exit(1);
24
+ }
25
+
26
+ let syncing = false;
27
+ let syncQueued = false;
13
28
 
14
- let syncing = null;
15
- function runSync() {
16
- if (syncing) return; // prevent overlapping syncs
17
- syncing = spawn("npm", ["run", "sync"], { stdio: "inherit", shell: true });
18
- syncing.on("exit", () => (syncing = null));
29
+ async function runLocalSync() {
30
+ if (syncing) {
31
+ syncQueued = true;
32
+ return;
33
+ }
34
+
35
+ syncing = true;
36
+ do {
37
+ syncQueued = false;
38
+ try {
39
+ await stageMod();
40
+ } catch (error) {
41
+ console.error(error.message);
42
+ }
43
+ } while (syncQueued);
44
+ syncing = false;
19
45
  }
20
46
 
21
- const watcher = chokidar.watch(["src/**/*.js", "media/**/*.png"], {
47
+ async function runRemoteSync() {
48
+ if (syncing) {
49
+ syncQueued = true;
50
+ return;
51
+ }
52
+
53
+ syncing = true;
54
+ do {
55
+ syncQueued = false;
56
+ try {
57
+ const { modName, zipPath } = await stageAndZip(sessionRoot);
58
+ await uploadMod(server, modName, zipPath);
59
+ } catch (error) {
60
+ console.error(error.message);
61
+ }
62
+ } while (syncQueued);
63
+ syncing = false;
64
+ }
65
+
66
+ const watcher = chokidar.watch(["src/**/*.js", "src/**/*.ts", "media/**/*.png"], {
22
67
  ignoreInitial: true,
23
68
  });
24
69
 
25
- watcher.on("all", () => runSync());
70
+ watcher.on("all", async () => {
71
+ if (server) {
72
+ await runRemoteSync();
73
+ return;
74
+ }
26
75
 
27
- async function isProcRunning() {
76
+ await runLocalSync();
77
+ });
78
+
79
+ async function isLocalProcRunning() {
28
80
  try {
29
81
  const { stdout } = await execAsync(
30
- `powershell -Command "Get-Process | Where-Object {$_.ProcessName -eq '${PROC_NAME.replace(".exe", "")}'} | Select-Object -First 1"`,
82
+ `tasklist /FI "IMAGENAME eq ${PROC_NAME}" /FO CSV /NH`,
31
83
  {
32
84
  timeout: 5000,
33
85
  }
34
86
  );
35
- return stdout.trim().length > 0;
87
+ return stdout.toLowerCase().includes(`"${PROC_NAME.toLowerCase()}"`);
36
88
  } catch (error) {
37
- // Process not found or command failed
38
89
  return false;
39
90
  }
40
91
  }
41
92
 
42
- async function killGameProc() {
93
+ async function killLocalGameProc() {
43
94
  try {
44
- await execAsync(
45
- `powershell -Command "Stop-Process -Name '${PROC_NAME.replace(".exe", "")}' -Force -ErrorAction SilentlyContinue"`
46
- );
47
- } catch (e) {
48
- // Process may already be stopped
95
+ await execAsync(`taskkill /IM "${PROC_NAME}" /F /T`);
96
+ } catch (error) {
97
+ // Process may already be stopped.
49
98
  }
50
99
  }
51
100
 
101
+ async function isProcRunning() {
102
+ if (server) {
103
+ const status = await getRemoteGameStatus(server);
104
+ return Boolean(status && status.running);
105
+ }
106
+
107
+ return isLocalProcRunning();
108
+ }
109
+
110
+ async function killGameProc() {
111
+ if (server) {
112
+ await killRemoteGame(server);
113
+ return;
114
+ }
115
+
116
+ await killLocalGameProc();
117
+ }
118
+
52
119
  const interval = setInterval(async () => {
53
120
  try {
54
121
  const alive = await isProcRunning();
@@ -59,12 +126,11 @@ const interval = setInterval(async () => {
59
126
  await watcher.close();
60
127
  process.exit(0);
61
128
  }
62
- } catch (e) {
63
- // if process listing fails, you can decide to ignore or stop
129
+ } catch (error) {
130
+ console.error(error.message);
64
131
  }
65
132
  }, 1000);
66
133
 
67
- // Ctrl+C cleanup
68
134
  process.on("SIGINT", async () => {
69
135
  console.log("Caught interrupt signal. Exiting watcher.");
70
136
 
package/index.js CHANGED
@@ -70,6 +70,37 @@ function getIdaJsDirFromConfig() {
70
70
  return null;
71
71
  }
72
72
 
73
+ function normalizeServerAddress(value) {
74
+ const trimmed = String(value || "").trim();
75
+ const input = trimmed.includes("://") ? trimmed : `http://${trimmed}`;
76
+ const url = new URL(input);
77
+ return `${url.hostname}:${url.port || 7770}`;
78
+ }
79
+
80
+ function parseIdaConnection(value) {
81
+ const trimmed = String(value || "").trim();
82
+
83
+ if (!trimmed) {
84
+ throw new Error("IdaJS installation directory or host[:port] is required");
85
+ }
86
+
87
+ if (fs.existsSync(trimmed)) {
88
+ return {
89
+ installDir: trimmed,
90
+ server: null,
91
+ };
92
+ }
93
+
94
+ if (trimmed.includes("/") || trimmed.includes("\\") || /^[A-Za-z]:/.test(trimmed)) {
95
+ throw new Error(`Directory does not exist: ${trimmed}`);
96
+ }
97
+
98
+ return {
99
+ installDir: null,
100
+ server: normalizeServerAddress(trimmed),
101
+ };
102
+ }
103
+
73
104
  async function main() {
74
105
  console.log("Welcome to IdaJS Mod Creator!\n");
75
106
 
@@ -124,16 +155,15 @@ async function main() {
124
155
  if (!config.idajsDir) {
125
156
  questions.push({
126
157
  type: "text",
127
- name: "idajsDir",
128
- message: "IdaJS installation directory:",
158
+ name: "idajsConnection",
159
+ message: "IdaJS installation directory or host[:port]:",
129
160
  validate: (value) => {
130
- if (!value || value.length === 0) {
131
- return "IdaJS installation directory is required";
161
+ try {
162
+ parseIdaConnection(value);
163
+ return true;
164
+ } catch (error) {
165
+ return error.message;
132
166
  }
133
- if (!fs.existsSync(value)) {
134
- return `Directory does not exist: ${value}`;
135
- }
136
- return true;
137
167
  },
138
168
  });
139
169
  }
@@ -159,6 +189,13 @@ async function main() {
159
189
  });
160
190
 
161
191
  config = { ...config, ...response };
192
+
193
+ if (response.idajsConnection) {
194
+ const parsedConnection = parseIdaConnection(response.idajsConnection);
195
+ config.idajsDir = parsedConnection.installDir;
196
+ config.server = parsedConnection.server;
197
+ delete config.idajsConnection;
198
+ }
162
199
  }
163
200
 
164
201
  // Set target directory to project name if not specified
@@ -171,7 +208,7 @@ async function main() {
171
208
  : path.join(process.cwd(), config.targetDirectory);
172
209
 
173
210
  // Validate IdaJS directory exists
174
- if (!fs.existsSync(config.idajsDir)) {
211
+ if (config.idajsDir && !fs.existsSync(config.idajsDir)) {
175
212
  console.error(`\n❌ Error: IdaJS installation directory does not exist: ${config.idajsDir}`);
176
213
  process.exit(1);
177
214
  }
@@ -184,7 +221,7 @@ async function main() {
184
221
 
185
222
  console.log(`\nCreating IdaJS mod in ${targetDir}...`);
186
223
  console.log(`Language: ${config.language === "js" ? "JavaScript" : "TypeScript"}`);
187
- console.log(`IdaJS: ${config.idajsDir}\n`);
224
+ console.log(`IdaJS: ${config.idajsDir || config.server}\n`);
188
225
 
189
226
  try {
190
227
  // Create project directory
@@ -218,14 +255,14 @@ async function main() {
218
255
  fs.writeFileSync(packageTemplatePath, JSON.stringify(packageTemplate, null, 2) + "\n");
219
256
  console.log("✓ Updated package.template.json");
220
257
 
221
- // Create .idajs.json config file
222
- const idajsConfig = {
223
- installDir: config.idajsDir,
224
- };
225
- fs.writeFileSync(
226
- path.join(targetDir, ".idajs.json"),
227
- JSON.stringify(idajsConfig, null, 2) + "\n"
228
- );
258
+ const idajsConfig = {};
259
+ if (config.idajsDir) {
260
+ idajsConfig.installDir = config.idajsDir;
261
+ }
262
+ if (config.server) {
263
+ idajsConfig.server = config.server;
264
+ }
265
+ fs.writeFileSync(path.join(targetDir, ".idajs.json"), JSON.stringify(idajsConfig, null, 2) + "\n");
229
266
  console.log("✓ Created .idajs.json");
230
267
 
231
268
  // Call Samples/install.js to set up the project
package/install.js CHANGED
@@ -141,10 +141,16 @@ function mergePackageJson() {
141
141
  // Step 2: Copy necessary files
142
142
  function copyFiles() {
143
143
  const filesToCopy = [
144
+ { source: "project.js" },
145
+ { source: "remote.js" },
144
146
  { source: "run.ps1" },
147
+ { source: "run-remote.js" },
148
+ { source: "start.js" },
145
149
  { source: "watch.js" },
146
150
  { source: "sync.js" },
147
151
  { source: "build.js" },
152
+ { source: "AGENTS.md" },
153
+ { source: "CLAUDE.md" },
148
154
  { source: isTypeScriptProject ? "tsconfig.json" : "jsconfig.json" },
149
155
  { source: "settings.json", target: ".vscode/settings.json" },
150
156
  { source: "extensions.json", target: ".vscode/extensions.json" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@idajs/create-mod",
3
- "version": "0.2.10",
3
+ "version": "0.2.15-dev.19",
4
4
  "description": "Scaffolding tool for creating IdaJS game mods for Little Big Adventure 2. Use with: npx @idajs/create-mod",
5
5
  "author": "Andriy Tevelyev",
6
6
  "license": "GPL-2.0",