@idajs/create-mod 0.2.11 → 0.2.15-dev.37

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.
@@ -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
@@ -4,6 +4,7 @@ const fs = require("fs");
4
4
  const path = require("path");
5
5
  const os = require("os");
6
6
  const { execSync } = require("child_process");
7
+ const readline = require("readline");
7
8
  const IdaSync = require("@idajs/sync");
8
9
 
9
10
  // Parse command line arguments
@@ -26,7 +27,9 @@ const idajsDir = getArgValue("--idajs-dir") || getArgValue("--idajs");
26
27
  const useTypeScript = args.includes("--typescript") || args.includes("--ts");
27
28
  const useJavaScript = args.includes("--javascript") || args.includes("--js");
28
29
  const skipInstall = args.includes("--skip-install");
30
+ const updateMode = args.includes("--update");
29
31
  const helpRequested = args.includes("--help") || args.includes("-h");
32
+ const requiredUpdatePackages = ["@idajs/types", "@idajs/sync"];
30
33
 
31
34
  // Show help if requested
32
35
  if (helpRequested) {
@@ -41,7 +44,8 @@ Options:
41
44
  --idajs-dir, --idajs <path> IdaJS installation directory
42
45
  --typescript, --ts Use TypeScript
43
46
  --javascript, --js Use JavaScript
44
- --skip-install Skip npm install
47
+ --update Refresh scaffolder-managed files in the current mod folder
48
+ --skip-install Skip npm install (and type update in --update mode)
45
49
  -h, --help Show this help message
46
50
 
47
51
  Examples:
@@ -50,6 +54,7 @@ Examples:
50
54
  npx @idajs/create-mod my-mod --typescript
51
55
  npx @idajs/create-mod my-mod --dir ./projects/my-mod --idajs C:/Projects/idajs --js
52
56
  npx @idajs/create-mod my-mod --skip-install
57
+ npx @idajs/create-mod --update
53
58
  `);
54
59
  process.exit(0);
55
60
  }
@@ -70,9 +75,124 @@ function getIdaJsDirFromConfig() {
70
75
  return null;
71
76
  }
72
77
 
78
+ function normalizeServerAddress(value) {
79
+ const trimmed = String(value || "").trim();
80
+ const input = trimmed.includes("://") ? trimmed : `http://${trimmed}`;
81
+ const url = new URL(input);
82
+ return `${url.hostname}:${url.port || 7770}`;
83
+ }
84
+
85
+ function parseIdaConnection(value) {
86
+ const trimmed = String(value || "").trim();
87
+
88
+ if (!trimmed) {
89
+ throw new Error("IdaJS installation directory or host[:port] is required");
90
+ }
91
+
92
+ if (fs.existsSync(trimmed)) {
93
+ return {
94
+ installDir: trimmed,
95
+ server: null,
96
+ };
97
+ }
98
+
99
+ if (trimmed.includes("/") || trimmed.includes("\\") || /^[A-Za-z]:/.test(trimmed)) {
100
+ throw new Error(`Directory does not exist: ${trimmed}`);
101
+ }
102
+
103
+ return {
104
+ installDir: null,
105
+ server: normalizeServerAddress(trimmed),
106
+ };
107
+ }
108
+
109
+ function loadJson(filePath) {
110
+ try {
111
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
112
+ } catch (error) {
113
+ throw new Error(`Failed to parse JSON file: ${filePath}`);
114
+ }
115
+ }
116
+
117
+ function isInteractiveTerminal() {
118
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
119
+ }
120
+
121
+ function askYesNo(question) {
122
+ return new Promise((resolve) => {
123
+ const rl = readline.createInterface({
124
+ input: process.stdin,
125
+ output: process.stdout,
126
+ });
127
+
128
+ rl.question(question, (answer) => {
129
+ rl.close();
130
+ const normalized = String(answer || "").trim().toLowerCase();
131
+ resolve(normalized === "y" || normalized === "yes");
132
+ });
133
+ });
134
+ }
135
+
136
+ async function runUpdateMode() {
137
+ const targetDir = process.cwd();
138
+ const packageJsonPath = path.join(targetDir, "package.json");
139
+
140
+ if (!fs.existsSync(packageJsonPath)) {
141
+ console.error(`\n❌ Error: No package.json found in ${targetDir}`);
142
+ console.error("Run this command from inside an existing IdaJS mod folder.");
143
+ process.exit(1);
144
+ }
145
+
146
+ const packageJson = loadJson(packageJsonPath);
147
+ const devDependencies = packageJson.devDependencies || {};
148
+ const missingPackages = requiredUpdatePackages.filter((name) => !devDependencies[name]);
149
+
150
+ if (missingPackages.length > 0) {
151
+ console.warn(
152
+ `\n⚠️ Warning: Missing IdaJS devDependencies: ${missingPackages.join(", ")}.`
153
+ );
154
+ console.warn("This may not be an IdaJS mod, or the project may differ significantly from the scaffold.");
155
+
156
+ if (!isInteractiveTerminal()) {
157
+ console.error("\n❌ Error: Confirmation required. Rerun this command in an interactive terminal to continue.");
158
+ process.exit(1);
159
+ }
160
+
161
+ const confirmed = await askYesNo("Continue with scaffolder update? [y/N] ");
162
+ if (!confirmed) {
163
+ console.log("\nUpdate cancelled.");
164
+ process.exit(1);
165
+ }
166
+ }
167
+
168
+ console.log(`\nUpdating IdaJS mod in ${targetDir}...`);
169
+
170
+ const installScript = path.join(__dirname, "install.js");
171
+ const installArgs = [installScript, targetDir, "--update"];
172
+
173
+ if (skipInstall) {
174
+ installArgs.push("--skip-install");
175
+ }
176
+
177
+ try {
178
+ execSync(`node ${installArgs.map((arg) => `"${arg}"`).join(" ")}`, {
179
+ stdio: "inherit",
180
+ });
181
+ console.log("\n✅ Scaffolder update completed successfully.");
182
+ } catch (error) {
183
+ console.error(`\n❌ Error updating project: ${error.message}`);
184
+ process.exit(1);
185
+ }
186
+ }
187
+
73
188
  async function main() {
74
189
  console.log("Welcome to IdaJS Mod Creator!\n");
75
190
 
191
+ if (updateMode) {
192
+ await runUpdateMode();
193
+ return;
194
+ }
195
+
76
196
  // Determine if we need prompts
77
197
  const needsPrompts =
78
198
  !projectName || (!useTypeScript && !useJavaScript) || (!idajsDir && !getIdaJsDirFromConfig());
@@ -124,16 +244,15 @@ async function main() {
124
244
  if (!config.idajsDir) {
125
245
  questions.push({
126
246
  type: "text",
127
- name: "idajsDir",
128
- message: "IdaJS installation directory:",
247
+ name: "idajsConnection",
248
+ message: "IdaJS installation directory or host[:port]:",
129
249
  validate: (value) => {
130
- if (!value || value.length === 0) {
131
- return "IdaJS installation directory is required";
250
+ try {
251
+ parseIdaConnection(value);
252
+ return true;
253
+ } catch (error) {
254
+ return error.message;
132
255
  }
133
- if (!fs.existsSync(value)) {
134
- return `Directory does not exist: ${value}`;
135
- }
136
- return true;
137
256
  },
138
257
  });
139
258
  }
@@ -159,6 +278,13 @@ async function main() {
159
278
  });
160
279
 
161
280
  config = { ...config, ...response };
281
+
282
+ if (response.idajsConnection) {
283
+ const parsedConnection = parseIdaConnection(response.idajsConnection);
284
+ config.idajsDir = parsedConnection.installDir;
285
+ config.server = parsedConnection.server;
286
+ delete config.idajsConnection;
287
+ }
162
288
  }
163
289
 
164
290
  // Set target directory to project name if not specified
@@ -171,7 +297,7 @@ async function main() {
171
297
  : path.join(process.cwd(), config.targetDirectory);
172
298
 
173
299
  // Validate IdaJS directory exists
174
- if (!fs.existsSync(config.idajsDir)) {
300
+ if (config.idajsDir && !fs.existsSync(config.idajsDir)) {
175
301
  console.error(`\n❌ Error: IdaJS installation directory does not exist: ${config.idajsDir}`);
176
302
  process.exit(1);
177
303
  }
@@ -184,7 +310,7 @@ async function main() {
184
310
 
185
311
  console.log(`\nCreating IdaJS mod in ${targetDir}...`);
186
312
  console.log(`Language: ${config.language === "js" ? "JavaScript" : "TypeScript"}`);
187
- console.log(`IdaJS: ${config.idajsDir}\n`);
313
+ console.log(`IdaJS: ${config.idajsDir || config.server}\n`);
188
314
 
189
315
  try {
190
316
  // Create project directory
@@ -218,14 +344,14 @@ async function main() {
218
344
  fs.writeFileSync(packageTemplatePath, JSON.stringify(packageTemplate, null, 2) + "\n");
219
345
  console.log("✓ Updated package.template.json");
220
346
 
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
- );
347
+ const idajsConfig = {};
348
+ if (config.idajsDir) {
349
+ idajsConfig.installDir = config.idajsDir;
350
+ }
351
+ if (config.server) {
352
+ idajsConfig.server = config.server;
353
+ }
354
+ fs.writeFileSync(path.join(targetDir, ".idajs.json"), JSON.stringify(idajsConfig, null, 2) + "\n");
229
355
  console.log("✓ Created .idajs.json");
230
356
 
231
357
  // Call Samples/install.js to set up the project