factorio-test-cli 2.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/run.js +118 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "factorio-test-cli",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "A CLI to run FactorioTest.",
5
5
  "license": "MIT",
6
6
  "repository": "https://github.com/GlassBricks/FactorioTest",
package/run.js CHANGED
@@ -7,6 +7,9 @@ import { spawn, spawnSync } from "child_process";
7
7
  import BufferLineSplitter from "./buffer-line-splitter.js";
8
8
  import chalk from "chalk";
9
9
  import * as process from "node:process";
10
+ import * as https from "https";
11
+ import * as readline from "readline";
12
+ const FACTORIO_TEST_MOD_VERSION = "2.0.1";
10
13
  const thisCommand = program
11
14
  .command("run")
12
15
  .summary("Runs tests with Factorio test.")
@@ -117,12 +120,121 @@ async function checkModExists(modsDir, modName) {
117
120
  }
118
121
  async function installFactorioTest(modsDir) {
119
122
  await fsp.mkdir(modsDir, { recursive: true });
120
- const testModName = "factorio-test";
121
- // if not found, install it
122
- const alreadyExists = await checkModExists(modsDir, testModName);
123
- if (!alreadyExists) {
124
- console.log(`Downloading ${testModName} from mod portal using fmtk. This may ask for factorio login credentials.`);
125
- await runScript("fmtk mods install", "--modsPath", modsDir, testModName);
123
+ const modName = "factorio-test";
124
+ const version = FACTORIO_TEST_MOD_VERSION;
125
+ const expectedZipName = `${modName}_${version}.zip`;
126
+ const expectedZipPath = path.join(modsDir, expectedZipName);
127
+ if (fs.existsSync(expectedZipPath)) {
128
+ if (thisCommand.opts().verbose)
129
+ console.log(`${modName} version ${version} already installed`);
130
+ return;
131
+ }
132
+ console.log(`Downloading ${modName} version ${version} from mod portal...`);
133
+ await downloadModVersion(modName, version, expectedZipPath);
134
+ }
135
+ async function downloadModVersion(modName, version, destPath) {
136
+ const modInfo = await fetchJson(`https://mods.factorio.com/api/mods/${modName}`);
137
+ const release = modInfo.releases.find((r) => r.version === version);
138
+ if (!release) {
139
+ const availableVersions = modInfo.releases.map((r) => r.version).join(", ");
140
+ throw new Error(`Version ${version} not found for mod ${modName}. Available: ${availableVersions}`);
141
+ }
142
+ const credentials = await getFactorioCredentials();
143
+ const downloadUrl = `https://mods.factorio.com${release.download_url}?username=${encodeURIComponent(credentials.username)}&token=${encodeURIComponent(credentials.token)}`;
144
+ await downloadFile(downloadUrl, destPath);
145
+ }
146
+ async function fetchJson(url) {
147
+ return new Promise((resolve, reject) => {
148
+ https.get(url, (res) => {
149
+ if (res.statusCode !== 200) {
150
+ reject(new Error(`HTTP ${res.statusCode} fetching ${url}`));
151
+ return;
152
+ }
153
+ let data = "";
154
+ res.on("data", (chunk) => (data += chunk));
155
+ res.on("end", () => {
156
+ try {
157
+ resolve(JSON.parse(data));
158
+ }
159
+ catch (e) {
160
+ reject(new Error(`Failed to parse JSON from ${url}`, { cause: e }));
161
+ }
162
+ });
163
+ res.on("error", reject);
164
+ }).on("error", reject);
165
+ });
166
+ }
167
+ async function downloadFile(url, destPath) {
168
+ return new Promise((resolve, reject) => {
169
+ https.get(url, (res) => {
170
+ if (res.statusCode === 302 || res.statusCode === 301) {
171
+ const redirectUrl = res.headers.location;
172
+ if (!redirectUrl) {
173
+ reject(new Error("Redirect without location header"));
174
+ return;
175
+ }
176
+ downloadFile(redirectUrl, destPath).then(resolve, reject);
177
+ return;
178
+ }
179
+ if (res.statusCode !== 200) {
180
+ reject(new Error(`HTTP ${res.statusCode} downloading mod`));
181
+ return;
182
+ }
183
+ const fileStream = fs.createWriteStream(destPath);
184
+ res.pipe(fileStream);
185
+ fileStream.on("close", () => resolve());
186
+ fileStream.on("error", (err) => {
187
+ fs.unlink(destPath, () => { });
188
+ reject(err);
189
+ });
190
+ }).on("error", reject);
191
+ });
192
+ }
193
+ async function getFactorioCredentials() {
194
+ const playerDataPath = getPlayerDataPath();
195
+ if (playerDataPath && fs.existsSync(playerDataPath)) {
196
+ try {
197
+ const playerData = JSON.parse(await fsp.readFile(playerDataPath, "utf8"));
198
+ if (playerData["service-username"] && playerData["service-token"]) {
199
+ return {
200
+ username: playerData["service-username"],
201
+ token: playerData["service-token"],
202
+ };
203
+ }
204
+ }
205
+ catch {
206
+ // Fall through to prompt
207
+ }
208
+ }
209
+ console.log("Factorio credentials required for mod portal download.");
210
+ return promptForCredentials();
211
+ }
212
+ function getPlayerDataPath() {
213
+ const platform = os.platform();
214
+ if (platform === "linux") {
215
+ return path.join(os.homedir(), ".factorio", "player-data.json");
216
+ }
217
+ else if (platform === "darwin") {
218
+ return path.join(os.homedir(), "Library", "Application Support", "factorio", "player-data.json");
219
+ }
220
+ else if (platform === "win32") {
221
+ return path.join(os.homedir(), "AppData", "Roaming", "Factorio", "player-data.json");
222
+ }
223
+ return undefined;
224
+ }
225
+ async function promptForCredentials() {
226
+ const rl = readline.createInterface({
227
+ input: process.stdin,
228
+ output: process.stdout,
229
+ });
230
+ const question = (prompt) => new Promise((resolve) => rl.question(prompt, resolve));
231
+ try {
232
+ const username = await question("Factorio username: ");
233
+ const token = await question("Factorio token (from https://factorio.com/profile): ");
234
+ return { username, token };
235
+ }
236
+ finally {
237
+ rl.close();
126
238
  }
127
239
  }
128
240
  async function ensureConfigIni(dataDir) {