@bf6mods/cli 1.1.2 → 1.2.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.
package/dist/cli/index.js CHANGED
@@ -1,853 +1,752 @@
1
1
  #!/usr/bin/env node
2
-
3
- // src/cli/index.ts
4
2
  import { Command } from "commander";
5
-
6
- // package.json
7
- var package_default = {
8
- name: "@bf6mods/cli",
9
- version: "1.0.2",
10
- description: "CLI and library for bundling BF6 mods",
11
- license: "MIT",
12
- type: "module",
13
- bin: {
14
- bf6mods: "./dist/cli/index.js"
15
- },
16
- main: "./dist/index.js",
17
- types: "./dist/index.d.ts",
18
- scripts: {
19
- build: "tsup",
20
- start: "cross-env EXIT_CODE=none tsx ./src/cli/index.ts"
21
- },
22
- exports: {
23
- ".": {
24
- types: "./dist/index.d.ts",
25
- import: "./dist/index.js"
26
- }
27
- },
28
- files: [
29
- "dist/"
30
- ],
31
- repository: {
32
- type: "git",
33
- url: "git+https://github.com/bf6mods/bf6mods.git"
34
- },
35
- dependencies: {
36
- "@bf6mods/sdk": "1.0.3",
37
- "@clack/prompts": "^0.11.0",
38
- chokidar: "^4.0.3",
39
- colors: "^1.4.0",
40
- commander: "^14.0.1",
41
- jiti: "^2.6.1",
42
- knitwork: "^1.2.0",
43
- rolldown: "^1.0.0-beta.43",
44
- "stringify-object": "^6.0.0"
45
- },
46
- publishConfig: {
47
- access: "public"
48
- },
49
- devDependencies: {
50
- "@oxc-project/types": "^0.95.0",
51
- "@types/stringify-object": "^4.0.5",
52
- "cross-env": "^10.1.0",
53
- tsx: "^4.20.6"
54
- }
55
- };
56
-
57
- // src/cli/build/index.ts
58
- import fs from "fs";
59
- import path from "path";
60
- import { fileURLToPath } from "url";
61
- import {
62
- AttachmentType
63
- } from "@bf6mods/sdk";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { AttachmentType } from "@bf6mods/sdk";
7
+ import colors from "colors";
64
8
  import { createJiti } from "jiti";
65
9
  import { rolldown } from "rolldown";
10
+ import stripAnsi from "strip-ansi";
11
+ import { glob } from "node:fs/promises";
12
+ import chokidar from "chokidar";
13
+ import stringifyObject from "stringify-object";
14
+ import child_process from "node:child_process";
15
+ import * as prompts from "@clack/prompts";
16
+ import { genExport, genInlineTypeImport } from "knitwork";
66
17
 
67
- // src/resources/prepare/types/config.ts
68
- var MapId = /* @__PURE__ */ ((MapId2) => {
69
- MapId2["FireStorm"] = "MP_FireStorm-ModBuilderCustom0";
70
- MapId2["SiegeOfCairo"] = "MP_Abbasid-ModBuilderCustom0";
71
- MapId2["EmpireState"] = "MP_Aftermath-ModBuilderCustom0";
72
- MapId2["IberianOffensive"] = "MP_Battery-ModBuilderCustom0";
73
- MapId2["LiberationPeak"] = "MP_Capstone-ModBuilderCustom0";
74
- MapId2["ManhattanBridge"] = "MP_Dumbo-ModBuilderCustom0";
75
- MapId2["SaintsQuarter"] = "MP_Limestone-ModBuilderCustom0";
76
- MapId2["NewSobekCity"] = "MP_Outskirts-ModBuilderCustom0";
77
- MapId2["MirakValley"] = "MP_Tungsten-ModBuilderCustom0";
78
- return MapId2;
79
- })(MapId || {});
80
-
81
- // src/cli/build/generated-strings.ts
82
- function extractBf6Strings(bf6Strings, generateFromLiterals) {
83
- return {
84
- name: "extract-bf6-strings",
85
- generateBundle(_options, bundle) {
86
- if (!generateFromLiterals) return;
87
- let id = 0;
88
- const existingStrings = new Set(Object.values(bf6Strings));
89
- for (const [_file, output] of Object.entries(bundle)) {
90
- let walk2 = function(node) {
91
- if (node.type === "Literal") {
92
- if (typeof node.value !== "string" || node.value === "" || node.value === "----uniquename----")
93
- return;
94
- if (!(node.value in bf6Strings) && !existingStrings.has(node.value)) {
95
- existingStrings.add(node.value);
96
- bf6Strings[`__auto__${id++}`] = node.value;
97
- }
98
- }
99
- for (const value of Object.values(node)) {
100
- if (!value) continue;
101
- if (Array.isArray(value)) {
102
- for (const v of value) {
103
- if (v && typeof v === "object" && "type" in v) walk2(v);
104
- }
105
- } else if (typeof value === "object" && "type" in value) {
106
- walk2(value);
107
- }
108
- }
109
- };
110
- var walk = walk2;
111
- if (output.type !== "chunk") continue;
112
- const code = output.code;
113
- const program2 = this.parse(code, {
114
- lang: "ts",
115
- astType: "js",
116
- range: true
117
- });
118
- for (const item of program2.body) {
119
- walk2(item);
120
- }
121
- }
122
- },
123
- moduleParsed(moduleInfo) {
124
- if (moduleInfo.code && moduleInfo.exports.includes("bf6Strings")) {
125
- const code = moduleInfo.code;
126
- const getSnippet = (start, end, linesBefore = 2, linesAfter = 2) => {
127
- const lines = code.split(/\r?\n/);
128
- const startLine = Math.max(
129
- 0,
130
- code.slice(0, start).split(/\r?\n/).length - 1 - linesBefore
131
- );
132
- const endLine = Math.min(
133
- lines.length,
134
- code.slice(0, end).split(/\r?\n/).length - 1 + linesAfter
135
- );
136
- return lines.slice(startLine, endLine).map((line, i) => {
137
- const actualLine = startLine + i + 1;
138
- return `${actualLine.toString().padStart(3)} | ${line}`;
139
- }).join("\n");
140
- };
141
- const program2 = this.parse(code, {
142
- lang: "ts",
143
- astType: "js",
144
- range: true
145
- });
146
- const extractObjectLiteral = (declaration) => {
147
- if (declaration.init?.type !== "ObjectExpression") {
148
- const snippet = getSnippet(...declaration.range ?? [0, 0]);
149
- this.error(
150
- `\u274C Invalid bf6Strings export: expected an object literal.
151
-
152
- ${snippet}`
153
- );
154
- }
155
- for (const property of declaration.init.properties) {
156
- if (property.type !== "Property") {
157
- const snippet = getSnippet(...property.range ?? [0, 0]);
158
- this.error(
159
- `\u274C Invalid bf6Strings property: expected a key/value pair.
160
- Found type: ${property.type}
161
-
162
- ${snippet}`
163
- );
164
- }
165
- let key = void 0;
166
- if (property.key.type === "Literal") {
167
- key = property.key.value;
168
- } else if (property.key.type === "Identifier") {
169
- key = property.key.name;
170
- } else {
171
- const snippet = getSnippet(...property.range ?? [0, 0]);
172
- this.error(
173
- `\u274C Invalid bf6Strings key: expected a string literal or identifier.
174
- Found type: ${property.key.type}
175
-
176
- ${snippet}`
177
- );
178
- continue;
179
- }
180
- if (property.value.type !== "Literal") {
181
- const snippet = getSnippet(...property.range ?? [0, 0]);
182
- this.error(
183
- `\u274C Invalid bf6Strings value: expected a string literal.
184
- Found type: ${property.value.type}
18
+ //#region package.json
19
+ var version = "1.2.1";
20
+ var description = "CLI and library for bundling BF6 mods";
21
+ var bin = { "bf6mods": "./dist/cli/index.js" };
185
22
 
186
- ${snippet}`
187
- );
188
- }
189
- const value = property.value.value;
190
- if (typeof key !== "string" && typeof key !== "number" && typeof key !== "bigint" && typeof key !== "boolean") {
191
- const snippet = getSnippet(...property.range ?? [0, 0]);
192
- this.error(
193
- `\u274C bf6Strings key must be a string, found: ${typeof key}
23
+ //#endregion
24
+ //#region src/resources/prepare/types/config.ts
25
+ let MapId = /* @__PURE__ */ function(MapId$1) {
26
+ MapId$1["FireStorm"] = "MP_FireStorm-ModBuilderCustom0";
27
+ MapId$1["SiegeOfCairo"] = "MP_Abbasid-ModBuilderCustom0";
28
+ MapId$1["EmpireState"] = "MP_Aftermath-ModBuilderCustom0";
29
+ MapId$1["IberianOffensive"] = "MP_Battery-ModBuilderCustom0";
30
+ MapId$1["LiberationPeak"] = "MP_Capstone-ModBuilderCustom0";
31
+ MapId$1["ManhattanBridge"] = "MP_Dumbo-ModBuilderCustom0";
32
+ MapId$1["SaintsQuarter"] = "MP_Limestone-ModBuilderCustom0";
33
+ MapId$1["NewSobekCity"] = "MP_Outskirts-ModBuilderCustom0";
34
+ MapId$1["MirakValley"] = "MP_Tungsten-ModBuilderCustom0";
35
+ return MapId$1;
36
+ }({});
194
37
 
195
- ${snippet}`
196
- );
197
- }
198
- if (typeof value !== "string" && typeof value !== "number" && typeof value !== "bigint" && typeof value !== "boolean") {
199
- const snippet = getSnippet(...property.range ?? [0, 0]);
200
- this.error(
201
- `\u274C bf6Strings value must be a string, found: ${typeof value}
38
+ //#endregion
39
+ //#region src/cli/utils.ts
40
+ /**
41
+ * Safely prints a message with a right-aligned timestamp.
42
+ */
43
+ const printToConsole = (message, error = false) => {
44
+ const now = /* @__PURE__ */ new Date();
45
+ const formattedTime = colors.grey(now.toLocaleTimeString());
46
+ const terminalWidth = process.stdout.columns || 80;
47
+ const timeLength = stripAnsi(formattedTime).length;
48
+ const available = terminalWidth - (stripAnsi(message).length + timeLength);
49
+ const spacing = available > 1 ? " ".repeat(available) : " ";
50
+ if (error) console.error(`${message}${spacing}${formattedTime}`);
51
+ else console.log(`${message}${spacing}${formattedTime}`);
52
+ };
202
53
 
203
- ${snippet}`
204
- );
205
- }
206
- bf6Strings[`${key}`] = `${value}`;
207
- }
208
- };
209
- for (const item of program2.body) {
210
- if (item.type === "VariableDeclaration") {
211
- for (const declaration of item.declarations) {
212
- if (declaration.type === "VariableDeclarator" && declaration.id.type === "Identifier" && declaration.id.name === "bf6Strings") {
213
- extractObjectLiteral(declaration);
214
- }
215
- }
216
- }
217
- if (item.type === "ExportNamedDeclaration" && item.declaration?.type === "VariableDeclaration") {
218
- for (const declaration of item.declaration.declarations) {
219
- if (declaration.type === "VariableDeclarator" && declaration.id.type === "Identifier" && declaration.id.name === "bf6Strings") {
220
- extractObjectLiteral(declaration);
221
- }
222
- }
223
- }
224
- }
225
- }
226
- }
227
- };
54
+ //#endregion
55
+ //#region src/cli/build/generated-strings.ts
56
+ function extractBf6Strings(bf6Strings, generateFromLiterals) {
57
+ return {
58
+ name: "extract-bf6-strings",
59
+ generateBundle(_options, bundle) {
60
+ if (!generateFromLiterals) return;
61
+ let id = 0;
62
+ const existingStrings = new Set(Object.values(bf6Strings));
63
+ for (const [_file, output] of Object.entries(bundle)) {
64
+ if (output.type !== "chunk") continue;
65
+ const code = output.code;
66
+ const program$1 = this.parse(code, {
67
+ lang: "ts",
68
+ astType: "js",
69
+ range: true
70
+ });
71
+ function walk(node) {
72
+ if (node.type === "Literal") {
73
+ if (typeof node.value !== "string" || node.value === "" || node.value === "----uniquename----") return;
74
+ if (!(node.value in bf6Strings) && !existingStrings.has(node.value)) {
75
+ existingStrings.add(node.value);
76
+ bf6Strings[`__auto__${id++}`] = node.value;
77
+ }
78
+ }
79
+ for (const value of Object.values(node)) {
80
+ if (!value) continue;
81
+ if (Array.isArray(value)) {
82
+ for (const v of value) if (v && typeof v === "object" && "type" in v) walk(v);
83
+ } else if (typeof value === "object" && "type" in value) walk(value);
84
+ }
85
+ }
86
+ for (const item of program$1.body) walk(item);
87
+ }
88
+ },
89
+ moduleParsed(moduleInfo) {
90
+ if (moduleInfo.code && moduleInfo.exports.includes("bf6Strings")) {
91
+ const code = moduleInfo.code;
92
+ const getSnippet = (start, end, linesBefore = 2, linesAfter = 2) => {
93
+ const lines = code.split(/\r?\n/);
94
+ const startLine = Math.max(0, code.slice(0, start).split(/\r?\n/).length - 1 - linesBefore);
95
+ const endLine = Math.min(lines.length, code.slice(0, end).split(/\r?\n/).length - 1 + linesAfter);
96
+ return lines.slice(startLine, endLine).map((line, i) => {
97
+ return `${(startLine + i + 1).toString().padStart(3)} | ${line}`;
98
+ }).join("\n");
99
+ };
100
+ const program$1 = this.parse(code, {
101
+ lang: "ts",
102
+ astType: "js",
103
+ range: true
104
+ });
105
+ const extractObjectLiteral = (declaration) => {
106
+ if (declaration.init?.type !== "ObjectExpression") {
107
+ const snippet = getSnippet(...declaration.range ?? [0, 0]);
108
+ this.error(`${colors.red.bold("✗")} Invalid bf6Strings export: expected an object literal.\n\n${snippet}`);
109
+ }
110
+ for (const property of declaration.init.properties) {
111
+ if (property.type !== "Property") {
112
+ const snippet = getSnippet(...property.range ?? [0, 0]);
113
+ this.error(`${colors.red.bold("✗")} Invalid bf6Strings property: expected a key/value pair.\nFound type: ${property.type}\n\n${snippet}`);
114
+ }
115
+ let key;
116
+ if (property.key.type === "Literal") key = property.key.value;
117
+ else if (property.key.type === "Identifier") key = property.key.name;
118
+ else {
119
+ const snippet = getSnippet(...property.range ?? [0, 0]);
120
+ this.error(`${colors.red.bold("✗")} Invalid bf6Strings key: expected a string literal or identifier.\nFound type: ${property.key.type}\n\n${snippet}`);
121
+ continue;
122
+ }
123
+ if (property.value.type !== "Literal") {
124
+ const snippet = getSnippet(...property.range ?? [0, 0]);
125
+ this.error(`${colors.red.bold("✗")} Invalid bf6Strings value: expected a string literal.\nFound type: ${property.value.type}\n\n${snippet}`);
126
+ }
127
+ const value = property.value.value;
128
+ if (typeof key !== "string" && typeof key !== "number" && typeof key !== "bigint" && typeof key !== "boolean") {
129
+ const snippet = getSnippet(...property.range ?? [0, 0]);
130
+ this.error(`${colors.red.bold("✗")} bf6Strings key must be a string, found: ${typeof key}\n\n${snippet}`);
131
+ }
132
+ if (typeof value !== "string" && typeof value !== "number" && typeof value !== "bigint" && typeof value !== "boolean") {
133
+ const snippet = getSnippet(...property.range ?? [0, 0]);
134
+ this.error(`${colors.red.bold("✗")} bf6Strings value must be a string, found: ${typeof value}\n\n${snippet}`);
135
+ }
136
+ bf6Strings[`${key}`] = `${value}`;
137
+ }
138
+ };
139
+ for (const item of program$1.body) {
140
+ if (item.type === "VariableDeclaration") {
141
+ for (const declaration of item.declarations) if (declaration.type === "VariableDeclarator" && declaration.id.type === "Identifier" && declaration.id.name === "bf6Strings") extractObjectLiteral(declaration);
142
+ }
143
+ if (item.type === "ExportNamedDeclaration" && item.declaration?.type === "VariableDeclaration") {
144
+ for (const declaration of item.declaration.declarations) if (declaration.type === "VariableDeclarator" && declaration.id.type === "Identifier" && declaration.id.name === "bf6Strings") extractObjectLiteral(declaration);
145
+ }
146
+ }
147
+ }
148
+ }
149
+ };
228
150
  }
229
151
 
230
- // src/cli/build/index.ts
152
+ //#endregion
153
+ //#region src/cli/build/index.ts
154
+ /**
155
+ * Dynamically imports and validates bf6.config.ts
156
+ */
231
157
  async function getBf6Config(rootDir) {
232
- globalThis.defineBf6Config = (c) => c;
233
- globalThis.MapId = MapId;
234
- const jiti = createJiti(rootDir, { interopDefault: true });
235
- const result = await jiti.import("./bf6.config", { default: true });
236
- delete globalThis.defineBf6Config;
237
- delete globalThis.MapId;
238
- return result;
158
+ globalThis.defineBf6Config = (c) => c;
159
+ globalThis.MapId = MapId;
160
+ const result = await createJiti(rootDir, { interopDefault: true }).import("./bf6.config", { default: true });
161
+ delete globalThis.defineBf6Config;
162
+ delete globalThis.MapId;
163
+ return result;
239
164
  }
165
+ /**
166
+ * Builds the entire mod project once (for production or single run).
167
+ */
240
168
  async function build() {
241
- const workingDir = path.resolve(".");
242
- const config = await getBf6Config(workingDir);
243
- const outDir = path.resolve(workingDir, config.outDir);
244
- if (fs.existsSync(outDir))
245
- fs.rmSync(outDir, { recursive: true, force: true });
246
- fs.mkdirSync(outDir, { recursive: true });
247
- const minifyJson = typeof config.minify === "boolean" ? config.minify : config.minify?.json ?? false;
248
- const generatedStrings = {};
249
- let tsAttachment;
250
- if (config.entrypoint) {
251
- const entryAbs = path.resolve(workingDir, config.entrypoint);
252
- const compiled = await buildEntrypoint(
253
- entryAbs,
254
- generatedStrings,
255
- config.generateStrings ?? true
256
- );
257
- tsAttachment = createTsAttachment(entryAbs, compiled);
258
- }
259
- const { attachments, mapRotation } = await collectAttachments(
260
- config,
261
- workingDir,
262
- tsAttachment,
263
- generatedStrings
264
- );
265
- await writeModJson(config, outDir, attachments, mapRotation, minifyJson);
266
- console.log(`\u2714 Built mod: ${config.name}`);
169
+ const workingDir = path.resolve(".");
170
+ const config = await getBf6Config(workingDir);
171
+ const outDir = path.resolve(workingDir, config.outDir);
172
+ if (fs.existsSync(outDir)) fs.rmSync(outDir, {
173
+ recursive: true,
174
+ force: true
175
+ });
176
+ fs.mkdirSync(outDir, { recursive: true });
177
+ const minifyJson = typeof config.minify === "boolean" ? config.minify : config.minify?.json ?? false;
178
+ const generatedStrings = {};
179
+ let tsAttachment;
180
+ if (config.entrypoint) {
181
+ const entryAbs = path.resolve(workingDir, config.entrypoint);
182
+ tsAttachment = createTsAttachment(entryAbs, await buildEntrypoint(entryAbs, generatedStrings, config.generateStrings ?? true));
183
+ }
184
+ const { attachments, mapRotation } = await collectAttachments(config, workingDir, tsAttachment, generatedStrings);
185
+ await writeModJson(config, outDir, attachments, mapRotation, minifyJson);
186
+ printToConsole(`${colors.green.bold("✓")} Built mod: ${config.name}`);
267
187
  }
188
+ /**
189
+ * Compiles the TypeScript entrypoint using rolldown and returns the compiled code.
190
+ */
268
191
  async function buildEntrypoint(entry, bf6Strings, generateStringsFromLiterals) {
269
- const bundle = await rolldown({
270
- input: entry,
271
- plugins: [extractBf6Strings(bf6Strings, generateStringsFromLiterals)],
272
- logLevel: "debug"
273
- });
274
- const result = await bundle.generate({
275
- format: "esm",
276
- inlineDynamicImports: true
277
- });
278
- const code = result.output[0].code;
279
- return code;
192
+ return (await (await rolldown({
193
+ input: entry,
194
+ plugins: [extractBf6Strings(bf6Strings, generateStringsFromLiterals)],
195
+ logLevel: "debug"
196
+ })).generate({
197
+ format: "esm",
198
+ inlineDynamicImports: true
199
+ })).output[0].code;
280
200
  }
281
- var __filename = fileURLToPath(import.meta.url);
282
- var __dirname = path.dirname(__filename);
283
- var spatialsDir = path.resolve(__dirname, "../resources/maps/spatial");
201
+ const __filename$1 = fileURLToPath(import.meta.url);
202
+ const __dirname$1 = path.dirname(__filename$1);
203
+ const spatialsDir = path.resolve(__dirname$1, "../resources/maps/spatial");
204
+ /**
205
+ * Collects all attachments (TS, scenes, strings) and returns them + mapRotation.
206
+ */
284
207
  async function collectAttachments(config, workingDir, tsAttachment, generatedStrings) {
285
- const attachments = [];
286
- const mapRotation = [];
287
- if (tsAttachment) attachments.push(tsAttachment);
288
- if (config.strings) {
289
- const strPath = path.resolve(workingDir, config.strings);
290
- if (!fs.existsSync(strPath)) throw new Error("Cannot find strings file");
291
- const raw = await fs.promises.readFile(strPath, "utf8");
292
- const attachment = createStringsAttachment(strPath, raw, generatedStrings);
293
- attachments.push(attachment);
294
- }
295
- if (config.scenes) {
296
- let mapIdx = 0;
297
- for (const map of config.scenes) {
298
- let mapId;
299
- let scene;
300
- if (Array.isArray(map)) {
301
- [mapId, scene] = map;
302
- } else {
303
- mapId = map;
304
- scene = path.resolve(spatialsDir, `${mapId}.spatial.json`);
305
- }
306
- const scenePath = path.resolve(workingDir, scene);
307
- if (!fs.existsSync(scenePath))
308
- throw new Error(`Cannot find spatial data file: ${scene}`);
309
- const raw = await fs.promises.readFile(scenePath, "utf8");
310
- const spatial = createSpatialAttachment(scenePath, raw, mapIdx++);
311
- attachments.push(spatial);
312
- mapRotation.push({ id: mapId, spatialAttachment: spatial });
313
- }
314
- }
315
- return { attachments, mapRotation };
208
+ const attachments = [];
209
+ const mapRotation = [];
210
+ if (tsAttachment) attachments.push(tsAttachment);
211
+ if (config.strings || Object.keys(generatedStrings ?? {}).length > 0) {
212
+ const strPath = path.resolve(workingDir, config.strings ?? "strings.json");
213
+ let raw = "{}";
214
+ if (fs.existsSync(strPath)) raw = await fs.promises.readFile(strPath, "utf8");
215
+ else printToConsole(colors.yellow(`⚠️ No strings.json found, generating from literals only.`));
216
+ const attachment = createStringsAttachment(strPath, raw, generatedStrings);
217
+ attachments.push(attachment);
218
+ }
219
+ if (config.scenes) {
220
+ let mapIdx = 0;
221
+ for (const map of config.scenes) {
222
+ let mapId;
223
+ let scene;
224
+ if (Array.isArray(map)) [mapId, scene] = map;
225
+ else {
226
+ mapId = map;
227
+ scene = path.resolve(spatialsDir, `${mapId}.spatial.json`);
228
+ }
229
+ const scenePath = path.resolve(workingDir, scene);
230
+ if (!fs.existsSync(scenePath)) throw new Error(`Cannot find spatial data file: ${scene}`);
231
+ const spatial = createSpatialAttachment(scenePath, await fs.promises.readFile(scenePath, "utf8"), mapIdx++);
232
+ attachments.push(spatial);
233
+ mapRotation.push({
234
+ id: mapId,
235
+ spatialAttachment: spatial
236
+ });
237
+ }
238
+ }
239
+ return {
240
+ attachments,
241
+ mapRotation
242
+ };
316
243
  }
244
+ /**
245
+ * Writes the mod.json output to disk.
246
+ */
317
247
  async function writeModJson(config, outDir, attachments, mapRotation, minify) {
318
- const baseGame = config.game;
319
- const finalJson = {
320
- name: config.name,
321
- description: config.description,
322
- gameMode: "ModBuilderCustom",
323
- mutators: baseGame.mutators ?? {},
324
- assetRestrictions: baseGame.assetRestrictions ?? {},
325
- teamComposition: baseGame.teamComposition ?? [],
326
- mapRotation,
327
- attachments
328
- };
329
- const jsonOutput = minify ? JSON.stringify(finalJson) : JSON.stringify(finalJson, null, 2);
330
- await fs.promises.writeFile(path.resolve(outDir, "mod.json"), jsonOutput);
331
- if (config.outputArtifacts && finalJson?.attachments) {
332
- for (const attachment of finalJson.attachments) {
333
- const attachmentsDir = path.resolve(outDir, "attachments");
334
- if (!fs.existsSync(attachmentsDir))
335
- fs.mkdirSync(attachmentsDir, {
336
- recursive: true
337
- });
338
- await fs.promises.writeFile(
339
- path.resolve(outDir, "attachments", attachment.filename),
340
- atob(attachment.attachmentData.original)
341
- );
342
- }
343
- }
248
+ const baseGame = config.game;
249
+ const finalJson = {
250
+ name: config.name,
251
+ description: config.description,
252
+ gameMode: "ModBuilderCustom",
253
+ mutators: baseGame.mutators ?? {},
254
+ assetRestrictions: baseGame.assetRestrictions ?? {},
255
+ teamComposition: baseGame.teamComposition ?? [],
256
+ mapRotation,
257
+ attachments
258
+ };
259
+ const jsonOutput = minify ? JSON.stringify(finalJson) : JSON.stringify(finalJson, null, 2);
260
+ await fs.promises.writeFile(path.resolve(outDir, "mod.json"), jsonOutput);
261
+ if (config.outputArtifacts && finalJson?.attachments) for (const attachment of finalJson.attachments) {
262
+ const attachmentsDir = path.resolve(outDir, "attachments");
263
+ if (!fs.existsSync(attachmentsDir)) fs.mkdirSync(attachmentsDir, { recursive: true });
264
+ await fs.promises.writeFile(path.resolve(outDir, "attachments", attachment.filename), atob(attachment.attachmentData.original));
265
+ }
344
266
  }
345
267
  function createTsAttachment(filePath, compiled) {
346
- return {
347
- id: crypto.randomUUID(),
348
- version: "1.0",
349
- filename: `${path.parse(filePath).name}.js`,
350
- isProcessable: true,
351
- processingStatus: 2,
352
- attachmentType: AttachmentType.TypeScript,
353
- attachmentData: { original: toBase64(compiled), compiled: "" },
354
- errors: []
355
- };
268
+ return {
269
+ id: crypto.randomUUID(),
270
+ version: "1.0",
271
+ filename: `${path.parse(filePath).name}.js`,
272
+ isProcessable: true,
273
+ processingStatus: 2,
274
+ attachmentType: AttachmentType.TypeScript,
275
+ attachmentData: {
276
+ original: toBase64(compiled),
277
+ compiled: ""
278
+ },
279
+ errors: []
280
+ };
356
281
  }
357
282
  function createStringsAttachment(filePath, raw, generatedStrings) {
358
- let result = raw;
359
- if (generatedStrings) {
360
- result = JSON.stringify(
361
- {
362
- ...generatedStrings,
363
- ...JSON.parse(raw)
364
- },
365
- null,
366
- 4
367
- );
368
- }
369
- return {
370
- id: crypto.randomUUID(),
371
- version: "1.0",
372
- filename: path.basename(filePath),
373
- isProcessable: true,
374
- processingStatus: 2,
375
- attachmentType: AttachmentType.Strings,
376
- attachmentData: { original: toBase64(result), compiled: "" },
377
- errors: []
378
- };
283
+ let result = raw;
284
+ if (generatedStrings) result = JSON.stringify({
285
+ ...generatedStrings,
286
+ ...JSON.parse(raw)
287
+ }, null, 4);
288
+ return {
289
+ id: crypto.randomUUID(),
290
+ version: "1.0",
291
+ filename: path.basename(filePath),
292
+ isProcessable: true,
293
+ processingStatus: 2,
294
+ attachmentType: AttachmentType.Strings,
295
+ attachmentData: {
296
+ original: toBase64(result),
297
+ compiled: ""
298
+ },
299
+ errors: []
300
+ };
379
301
  }
380
302
  function createSpatialAttachment(filePath, raw, mapIdx) {
381
- return {
382
- id: crypto.randomUUID(),
383
- version: "1.0",
384
- filename: path.basename(filePath),
385
- isProcessable: true,
386
- processingStatus: 2,
387
- attachmentType: AttachmentType.SpatialData,
388
- attachmentData: { original: toBase64(raw), compiled: "" },
389
- metadata: `mapIdx=${mapIdx}`,
390
- errors: []
391
- };
303
+ return {
304
+ id: crypto.randomUUID(),
305
+ version: "1.0",
306
+ filename: path.basename(filePath),
307
+ isProcessable: true,
308
+ processingStatus: 2,
309
+ attachmentType: AttachmentType.SpatialData,
310
+ attachmentData: {
311
+ original: toBase64(raw),
312
+ compiled: ""
313
+ },
314
+ metadata: `mapIdx=${mapIdx}`,
315
+ errors: []
316
+ };
392
317
  }
393
318
  function toBase64(input) {
394
- return Buffer.isBuffer(input) ? input.toString("base64") : Buffer.from(input, "utf8").toString("base64");
319
+ return Buffer.isBuffer(input) ? input.toString("base64") : Buffer.from(input, "utf8").toString("base64");
395
320
  }
396
321
 
397
- // src/cli/dev.ts
398
- import fs2 from "fs";
399
- import { glob } from "fs/promises";
400
- import path2 from "path";
401
- import chokidar from "chokidar";
402
- import colors from "colors";
322
+ //#endregion
323
+ //#region src/cli/log.ts
324
+ var Bf6Logger = class Bf6Logger {
325
+ constructor(input) {
326
+ if (Bf6Logger.instance) {
327
+ printToConsole(colors.yellow("⚠ Existing logger found — closing it first..."));
328
+ Bf6Logger.instance.stop();
329
+ }
330
+ if (input) this.watchTarget = path.resolve(input);
331
+ else if (process.env.LOCALAPPDATA) this.watchTarget = path.resolve(process.env.LOCALAPPDATA, "temp", "Battlefieldâ„¢ 6", "PortalLog.txt");
332
+ else throw new Error(`${colors.red.bold("✗")} Env variable LOCALAPPDATA not defined, and input file to watch was not provided!`);
333
+ if (!fs.existsSync(this.watchTarget)) throw new Error(`${colors.red.bold("✗")} Cannot find file '${this.watchTarget}' to watch for changes`);
334
+ this.lastSize = fs.statSync(this.watchTarget).size;
335
+ Bf6Logger.instance = this;
336
+ }
337
+ async start() {
338
+ printToConsole(colors.cyan(`👀 Watching Battlefield logs at: ${colors.yellow(this.watchTarget)}`));
339
+ this.watcher = chokidar.watch(this.watchTarget, {
340
+ persistent: true,
341
+ ignoreInitial: true
342
+ });
343
+ this.watcher.on("change", this.handleChange.bind(this));
344
+ }
345
+ async stop() {
346
+ if (this.watcher) {
347
+ this.watcher.close();
348
+ this.watcher = void 0;
349
+ printToConsole(colors.yellow(`${colors.red.bold("✗")} Stopped watching: ${colors.gray(this.watchTarget)}`));
350
+ }
351
+ Bf6Logger.instance = void 0;
352
+ }
353
+ handleChange(file, stats) {
354
+ if (!stats) return;
355
+ if (stats.size > this.lastSize) {
356
+ const newBytes = stats.size - this.lastSize;
357
+ const fd = fs.openSync(file, "r");
358
+ const buffer = Buffer.alloc(newBytes);
359
+ fs.readSync(fd, buffer, 0, newBytes, this.lastSize);
360
+ fs.closeSync(fd);
361
+ const newContent = buffer.toString("utf8").split("QuickJS: ").filter((debug) => debug.trim() !== "").map((debug) => debug.trim()).map((debug) => {
362
+ if (debug.startsWith("console.log: ")) return {
363
+ type: "console.log",
364
+ text: debug.replace("console.log: ", "")
365
+ };
366
+ else if (debug.startsWith("Exception:")) return {
367
+ type: "exception",
368
+ text: debug.replace("Exception:", "")
369
+ };
370
+ return {
371
+ type: "unknown",
372
+ text: debug
373
+ };
374
+ });
375
+ for (const content of newContent) if (content.type === "console.log") printToConsole(colors.gray(content.text));
376
+ else if (content.type === "exception") printToConsole(colors.red(content.text), true);
377
+ else printToConsole(colors.bold(content.text));
378
+ }
379
+ this.lastSize = stats.size;
380
+ }
381
+ };
382
+
383
+ //#endregion
384
+ //#region src/cli/dev.ts
403
385
  async function dev() {
404
- const workingDir = path2.resolve(".");
405
- let config = await getBf6Config(workingDir);
406
- const outDir = path2.resolve(workingDir, config.outDir);
407
- if (!fs2.existsSync(outDir)) fs2.mkdirSync(outDir, { recursive: true });
408
- console.log(colors.cyan(`\u25B6 Starting dev for ${config.name}`));
409
- let watcher;
410
- async function rebuild(trigger) {
411
- console.log(
412
- colors.yellow(
413
- `\u21BB Change detected in ${path2.basename(trigger)}, rebuilding...`
414
- )
415
- );
416
- const start = performance.now();
417
- try {
418
- await build();
419
- const end = performance.now();
420
- const duration = ((end - start) / 1e3).toFixed(2);
421
- console.log(colors.green(`\u2714 Updated mod.json (${duration}s)`));
422
- } catch (err) {
423
- console.error(colors.red(`\u2716 Rebuild failed: ${err.message}`));
424
- }
425
- }
426
- async function collectWatchTargets() {
427
- const targets = [];
428
- if (config.entrypoint)
429
- targets.push(path2.resolve(workingDir, config.entrypoint));
430
- if (config.scenes) {
431
- for (const [, scene] of config.scenes) {
432
- targets.push(path2.resolve(workingDir, scene));
433
- }
434
- }
435
- if (config.strings) targets.push(path2.resolve(workingDir, config.strings));
436
- const srcDir = path2.resolve(workingDir, "src");
437
- for await (const entry of glob(`${srcDir}/**/*`)) targets.push(entry);
438
- for await (const entry of glob("bf6.config.*")) targets.push(entry);
439
- return targets;
440
- }
441
- async function setupWatcher() {
442
- if (watcher) {
443
- await watcher.close();
444
- console.log(colors.grey("\u267B Reloading watcher due to config change..."));
445
- }
446
- const watchTargets = await collectWatchTargets();
447
- watcher = chokidar.watch(watchTargets, {
448
- persistent: true,
449
- ignoreInitial: true,
450
- awaitWriteFinish: true,
451
- ignored: [path2.resolve(outDir), `${path2.resolve(outDir)}/**`]
452
- });
453
- watcher.on("change", async (file) => {
454
- if (file.includes("bf6.config.")) {
455
- console.log(
456
- colors.magenta(
457
- "\u2699 Config changed \u2014 reloading and rebuilding watcher..."
458
- )
459
- );
460
- try {
461
- config = await getBf6Config(workingDir);
462
- await setupWatcher();
463
- } catch (err) {
464
- console.error(
465
- colors.red(`\u2716 Failed to reload config: ${err.message}`)
466
- );
467
- }
468
- return;
469
- }
470
- await rebuild(file);
471
- });
472
- watcher.on("add", async (file) => await rebuild(file));
473
- await rebuild("initial");
474
- }
475
- await setupWatcher();
386
+ const workingDir = path.resolve(".");
387
+ let config = await getBf6Config(workingDir);
388
+ const outDir = path.resolve(workingDir, config.outDir);
389
+ fs.mkdirSync(outDir, { recursive: true });
390
+ printToConsole(colors.cyan(`▶ Starting dev for ${config.name}`));
391
+ let watcher;
392
+ function debounce(fn, delay) {
393
+ let timer;
394
+ return (...args) => {
395
+ if (timer) clearTimeout(timer);
396
+ timer = setTimeout(() => fn(...args), delay);
397
+ };
398
+ }
399
+ const rebuild = async (trigger) => {
400
+ printToConsole(colors.yellow(`↻ Change detected in ${path.basename(trigger)}, rebuilding...`));
401
+ const start = performance.now();
402
+ try {
403
+ await build();
404
+ const duration = ((performance.now() - start) / 1e3).toFixed(2);
405
+ printToConsole(`${colors.green.bold("✓")} Updated mod.json (${duration}s)`);
406
+ } catch (err) {
407
+ printToConsole(colors.red(`${colors.red.bold("✗")} Rebuild failed: ${err.message}`), true);
408
+ }
409
+ };
410
+ const debouncedRebuild = debounce(rebuild, 200);
411
+ async function collectWatchTargets() {
412
+ const targets = [];
413
+ if (config.entrypoint) targets.push(path.resolve(workingDir, config.entrypoint));
414
+ if (config.scenes) for (const [, scene] of config.scenes) targets.push(path.resolve(workingDir, scene));
415
+ if (config.strings) targets.push(path.resolve(workingDir, config.strings));
416
+ for await (const entry of glob("bf6.config.*")) targets.push(entry);
417
+ for await (const entry of glob("src/**/*")) targets.push(entry);
418
+ return targets;
419
+ }
420
+ async function setupWatcher() {
421
+ if (watcher) {
422
+ await watcher.close();
423
+ printToConsole(colors.grey("♻ Reloading watcher due to config change..."));
424
+ await new Promise((r) => setTimeout(r, 10));
425
+ }
426
+ const watchTargets = await collectWatchTargets();
427
+ printToConsole(colors.cyan(`👀 Watching ${watchTargets.length} files...`));
428
+ watcher = chokidar.watch(watchTargets, {
429
+ persistent: true,
430
+ ignoreInitial: true,
431
+ awaitWriteFinish: {
432
+ stabilityThreshold: 250,
433
+ pollInterval: 100
434
+ },
435
+ ignored: [
436
+ outDir,
437
+ `${outDir}/**`,
438
+ "node_modules/**",
439
+ ".git/**"
440
+ ]
441
+ });
442
+ watcher.on("error", (err) => {
443
+ printToConsole(colors.red(`⚠ Watcher error: ${err.message}`), true);
444
+ setTimeout(setupWatcher, 1e3);
445
+ });
446
+ watcher.on("change", async (file) => {
447
+ if (file.includes("bf6.config.")) {
448
+ printToConsole(colors.magenta("⚙ Config changed — reloading watcher..."));
449
+ config = await getBf6Config(workingDir);
450
+ await setupWatcher();
451
+ return;
452
+ }
453
+ debouncedRebuild(file);
454
+ });
455
+ watcher.on("add", debouncedRebuild);
456
+ await rebuild("initial");
457
+ }
458
+ process.on("SIGINT", async () => {
459
+ printToConsole(colors.red(`\n${colors.red.bold("✗")} Exiting dev mode...`));
460
+ await watcher?.close();
461
+ process.exit(0);
462
+ });
463
+ await setupWatcher();
464
+ let logger;
465
+ try {
466
+ logger = new Bf6Logger();
467
+ logger.start();
468
+ } catch (error) {
469
+ printToConsole(colors.grey("Failed to start logging for the following reason (building will still work)"), true);
470
+ printToConsole(error.message, true);
471
+ }
476
472
  }
477
473
 
478
- // src/cli/import.ts
479
- import fs4 from "fs";
480
- import path4 from "path";
481
- import { AttachmentType as AttachmentType2 } from "@bf6mods/sdk";
482
- import stringifyObject from "stringify-object";
483
-
484
- // src/cli/init.ts
485
- import child_process from "child_process";
486
- import fs3 from "fs";
487
- import path3 from "path";
488
- import { fileURLToPath as fileURLToPath2 } from "url";
489
- import * as prompts from "@clack/prompts";
490
- var __filename2 = fileURLToPath2(import.meta.url);
491
- var __dirname2 = path3.dirname(__filename2);
492
- var templatesDir = path3.resolve(__dirname2, "../resources/templates");
474
+ //#endregion
475
+ //#region src/cli/init.ts
476
+ const __filename = fileURLToPath(import.meta.url);
477
+ const __dirname = path.dirname(__filename);
478
+ const templatesDir = path.resolve(__dirname, "../resources/templates");
493
479
  function renameFilesRecursively(dir, modName) {
494
- const entries = fs3.readdirSync(dir, { withFileTypes: true });
495
- for (const entry of entries) {
496
- const oldPath = path3.join(dir, entry.name);
497
- let newPath = oldPath;
498
- if (entry.name.includes("{name}")) {
499
- const newName = entry.name.replace("{name}", modName);
500
- newPath = path3.join(dir, newName);
501
- fs3.renameSync(oldPath, newPath);
502
- }
503
- if (entry.name === "bf6.config.ts") {
504
- let content = fs3.readFileSync(newPath, "utf-8");
505
- content = content.replace(/{bf6ConfigName}/g, modName);
506
- fs3.writeFileSync(newPath, content, "utf-8");
507
- }
508
- if (entry.name === "package.json") {
509
- const pkg = JSON.parse(fs3.readFileSync(newPath, "utf-8"));
510
- pkg.name = modName;
511
- fs3.writeFileSync(newPath, JSON.stringify(pkg, null, 2));
512
- }
513
- if (fs3.statSync(newPath).isDirectory()) {
514
- renameFilesRecursively(newPath, modName);
515
- }
516
- }
480
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
481
+ for (const entry of entries) {
482
+ const oldPath = path.join(dir, entry.name);
483
+ let newPath = oldPath;
484
+ if (entry.name.includes("{name}")) {
485
+ const newName = entry.name.replace("{name}", modName);
486
+ newPath = path.join(dir, newName);
487
+ fs.renameSync(oldPath, newPath);
488
+ }
489
+ if (entry.name === "bf6.config.ts") {
490
+ let content = fs.readFileSync(newPath, "utf-8");
491
+ content = content.replace(/{bf6ConfigName}/g, modName);
492
+ fs.writeFileSync(newPath, content, "utf-8");
493
+ }
494
+ if (entry.name === "package.json") {
495
+ const pkg = JSON.parse(fs.readFileSync(newPath, "utf-8"));
496
+ pkg.name = modName;
497
+ fs.writeFileSync(newPath, JSON.stringify(pkg, null, 2));
498
+ }
499
+ if (fs.statSync(newPath).isDirectory()) renameFilesRecursively(newPath, modName);
500
+ }
517
501
  }
518
- var templates = [
519
- "Basic",
520
- "AcePursuit",
521
- "BombSquad",
522
- "Exfil",
523
- "Vertigo"
502
+ const templates = [
503
+ "Basic",
504
+ "AcePursuit",
505
+ "BombSquad",
506
+ "Exfil",
507
+ "Vertigo"
524
508
  ];
525
509
  async function startProject(destination, template, name) {
526
- if (["AcePursuit", "BombSquad", "Exfil", "Vertigo"].includes(template)) {
527
- const importPath = path3.resolve(templatesDir, `${template}.json`);
528
- await importFile(importPath, destination, name);
529
- } else if (template === "None") {
530
- } else {
531
- const templateDir = path3.resolve(templatesDir, template);
532
- fs3.cpSync(templateDir, destination, { recursive: true });
533
- }
534
- fs3.cpSync(path3.resolve(templatesDir, "All"), destination, {
535
- recursive: true
536
- });
537
- if (name) renameFilesRecursively(destination, name);
510
+ if ([
511
+ "AcePursuit",
512
+ "BombSquad",
513
+ "Exfil",
514
+ "Vertigo"
515
+ ].includes(template)) await importFile(path.resolve(templatesDir, `${template}.json`), destination, name);
516
+ else if (template === "None") {} else {
517
+ const templateDir = path.resolve(templatesDir, template);
518
+ fs.cpSync(templateDir, destination, { recursive: true });
519
+ }
520
+ fs.cpSync(path.resolve(templatesDir, "All"), destination, { recursive: true });
521
+ if (name) renameFilesRecursively(destination, name);
538
522
  }
539
523
  function installDependencies(projectDir) {
540
- try {
541
- child_process.execSync(`npm install`, {
542
- stdio: "inherit",
543
- cwd: projectDir
544
- });
545
- return true;
546
- } catch (_err) {
547
- return false;
548
- }
524
+ try {
525
+ child_process.execSync(`npm install`, {
526
+ stdio: "inherit",
527
+ cwd: projectDir
528
+ });
529
+ return true;
530
+ } catch (_err) {
531
+ return false;
532
+ }
549
533
  }
550
- var cancel2 = () => prompts.cancel("Operation cancelled");
551
- function isEmpty(path6) {
552
- const files = fs3.readdirSync(path6);
553
- return files.length === 0 || files.length === 1 && files[0] === ".git";
534
+ const cancel = () => prompts.cancel("Operation cancelled");
535
+ function isEmpty(path$1) {
536
+ const files = fs.readdirSync(path$1);
537
+ return files.length === 0 || files.length === 1 && files[0] === ".git";
554
538
  }
555
539
  function emptyDir(dir) {
556
- if (!fs3.existsSync(dir)) {
557
- return;
558
- }
559
- for (const file of fs3.readdirSync(dir)) {
560
- if (file === ".git") {
561
- continue;
562
- }
563
- fs3.rmSync(path3.resolve(dir, file), { recursive: true, force: true });
564
- }
540
+ if (!fs.existsSync(dir)) return;
541
+ for (const file of fs.readdirSync(dir)) {
542
+ if (file === ".git") continue;
543
+ fs.rmSync(path.resolve(dir, file), {
544
+ recursive: true,
545
+ force: true
546
+ });
547
+ }
565
548
  }
566
549
  async function init(argTargetDir) {
567
- prompts.intro("Initialize Bf6 Mod");
568
- const path6 = argTargetDir ? argTargetDir : await prompts.text({
569
- message: "Where should we create your project?",
570
- placeholder: "./ace-pursuit",
571
- validate: (value) => {
572
- if (!value) return "Please enter a path.";
573
- if (value[0] !== ".") return "Please enter a relative path.";
574
- }
575
- });
576
- if (prompts.isCancel(path6)) return cancel2();
577
- let name = await prompts.text({
578
- message: "What is the name of your mod?",
579
- placeholder: "Ace Pursuit",
580
- validate: (value) => {
581
- if (!value.trim()) return "Please enter a name.";
582
- }
583
- });
584
- if (prompts.isCancel(name)) return cancel2();
585
- name = name.trim();
586
- if (fs3.existsSync(path6) && !isEmpty(path6)) {
587
- let overwrite;
588
- const res = await prompts.select({
589
- message: (path6 === "." ? "Current directory" : `Target directory "${path6}"`) + ` is not empty. Please choose how to proceed:`,
590
- options: [
591
- {
592
- label: "Cancel operation",
593
- value: "no"
594
- },
595
- {
596
- label: "Remove existing files and continue",
597
- value: "yes"
598
- },
599
- {
600
- label: "Ignore files and continue",
601
- value: "ignore"
602
- }
603
- ]
604
- });
605
- if (prompts.isCancel(res)) return cancel2();
606
- overwrite = res;
607
- switch (overwrite) {
608
- case "yes":
609
- emptyDir(path6);
610
- break;
611
- case "no":
612
- cancel2();
613
- return;
614
- }
615
- }
616
- const template = await prompts.select({
617
- message: "Select a template:",
618
- options: templates.map((template2) => {
619
- return {
620
- label: template2,
621
- value: template2
622
- };
623
- })
624
- });
625
- if (prompts.isCancel(template)) return cancel2();
626
- await startProject(path6, template, name);
627
- const s = prompts.spinner();
628
- s.start("Installing via npm");
629
- const installed = installDependencies(path6);
630
- if (installed) s.stop("Installed via npm");
631
- else s.stop("Failed to install via npm", 1);
632
- const nextSteps = `cd ${path6}
633
- npm run build`;
634
- prompts.note(nextSteps, "Next steps.");
550
+ prompts.intro("Initialize Bf6 Mod");
551
+ const path$1 = argTargetDir ? argTargetDir : await prompts.text({
552
+ message: "Where should we create your project?",
553
+ placeholder: "./ace-pursuit",
554
+ validate: (value) => {
555
+ if (!value) return "Please enter a path.";
556
+ if (value[0] !== ".") return "Please enter a relative path.";
557
+ }
558
+ });
559
+ if (prompts.isCancel(path$1)) return cancel();
560
+ let name = await prompts.text({
561
+ message: "What is the name of your mod?",
562
+ placeholder: "Ace Pursuit",
563
+ validate: (value) => {
564
+ if (!value.trim()) return "Please enter a name.";
565
+ }
566
+ });
567
+ if (prompts.isCancel(name)) return cancel();
568
+ name = name.trim();
569
+ if (fs.existsSync(path$1) && !isEmpty(path$1)) {
570
+ let overwrite;
571
+ const res = await prompts.select({
572
+ message: (path$1 === "." ? "Current directory" : `Target directory "${path$1}"`) + ` is not empty. Please choose how to proceed:`,
573
+ options: [
574
+ {
575
+ label: "Cancel operation",
576
+ value: "no"
577
+ },
578
+ {
579
+ label: "Remove existing files and continue",
580
+ value: "yes"
581
+ },
582
+ {
583
+ label: "Ignore files and continue",
584
+ value: "ignore"
585
+ }
586
+ ]
587
+ });
588
+ if (prompts.isCancel(res)) return cancel();
589
+ overwrite = res;
590
+ switch (overwrite) {
591
+ case "yes":
592
+ emptyDir(path$1);
593
+ break;
594
+ case "no":
595
+ cancel();
596
+ return;
597
+ }
598
+ }
599
+ const template = await prompts.select({
600
+ message: "Select a template:",
601
+ options: templates.map((template$1) => {
602
+ return {
603
+ label: template$1,
604
+ value: template$1
605
+ };
606
+ })
607
+ });
608
+ if (prompts.isCancel(template)) return cancel();
609
+ await startProject(path$1, template, name);
610
+ const s = prompts.spinner();
611
+ s.start("Installing via npm");
612
+ if (installDependencies(path$1)) s.stop("Installed via npm");
613
+ else s.stop("Failed to install via npm", 1);
614
+ const nextSteps = `cd ${path$1}\nnpm run build`;
615
+ prompts.note(nextSteps, "Next steps.");
635
616
  }
636
617
 
637
- // src/cli/import.ts
618
+ //#endregion
619
+ //#region src/cli/import.ts
638
620
  async function writeFileSafe(filePath, data) {
639
- const dir = path4.dirname(filePath);
640
- await fs4.promises.mkdir(dir, { recursive: true });
641
- await fs4.promises.writeFile(filePath, data);
621
+ const dir = path.dirname(filePath);
622
+ await fs.promises.mkdir(dir, { recursive: true });
623
+ await fs.promises.writeFile(filePath, data);
642
624
  }
643
625
  function getMapKeyByValue(value) {
644
- return Object.keys(MapId).find(
645
- (k) => MapId[k] === value
646
- );
626
+ return Object.keys(MapId).find((k) => MapId[k] === value);
647
627
  }
648
- async function importFile(input, output, name) {
649
- const workingDir = path4.resolve(".");
650
- const entrypoint = path4.resolve(workingDir, input);
651
- const outDir = path4.resolve(workingDir, output);
652
- if (!fs4.existsSync(entrypoint)) throw new Error("Cannot find strings file");
653
- const config = JSON.parse(
654
- await fs4.promises.readFile(entrypoint, { encoding: "utf8" })
655
- );
656
- if (!fs4.existsSync(outDir))
657
- await fs4.promises.mkdir(outDir, { recursive: true });
658
- await startProject(outDir, "None", name ?? config.name);
659
- let typescriptFile;
660
- let stringsFile;
661
- const scenes = [];
662
- const promises = [];
663
- if (config.attachments) {
664
- for (const attachment of config.attachments) {
665
- if (attachment.attachmentType === AttachmentType2.TypeScript)
666
- typescriptFile = attachment.filename;
667
- if (attachment.attachmentType === AttachmentType2.Strings)
668
- stringsFile = attachment.filename;
669
- if (attachment.attachmentType === AttachmentType2.SpatialData) {
670
- continue;
671
- }
672
- promises.push(
673
- writeFileSafe(
674
- path4.resolve(outDir, "src", attachment.filename),
675
- atob(attachment.attachmentData.original)
676
- )
677
- );
678
- }
679
- }
680
- if (config.mapRotation?.length) {
681
- for (const map of config.mapRotation) {
682
- promises.push(
683
- writeFileSafe(
684
- path4.resolve(outDir, "src", "scenes", map.spatialAttachment.filename),
685
- atob(map.spatialAttachment.attachmentData.original)
686
- )
687
- );
688
- scenes.push([map.id, `src/scenes/${map.spatialAttachment.filename}`]);
689
- }
690
- }
691
- const bf6Config = {
692
- name: name ?? config.name,
693
- description: config.description,
694
- outDir: "dist",
695
- entrypoint: typescriptFile ? `src/${typescriptFile}` : void 0,
696
- scenes: scenes ? scenes.map(([id, path6]) => [
697
- `MapId.${getMapKeyByValue(id) ?? id}`,
698
- path6
699
- ]) : void 0,
700
- strings: stringsFile ? `src/${stringsFile}` : void 0,
701
- game: {
702
- mutators: config.mutators,
703
- assetRestrictions: config.assetRestrictions,
704
- gameMode: config.gameMode,
705
- teamComposition: config.teamComposition
706
- }
707
- };
708
- const bf6ConfigContent = `export default defineBf6Config(${stringifyWithRaw(
709
- bf6Config
710
- )});
711
- `;
712
- promises.push(
713
- writeFileSafe(path4.resolve(outDir, "bf6.config.ts"), bf6ConfigContent)
714
- );
715
- await Promise.all(promises);
628
+ async function importFile(input, output, name, logging = false) {
629
+ const workingDir = path.resolve(".");
630
+ const entrypoint = path.resolve(workingDir, input);
631
+ const outDir = path.resolve(workingDir, output);
632
+ if (!fs.existsSync(entrypoint)) throw new Error("Cannot find strings file");
633
+ const config = JSON.parse(await fs.promises.readFile(entrypoint, { encoding: "utf8" }));
634
+ if (!fs.existsSync(outDir)) await fs.promises.mkdir(outDir, { recursive: true });
635
+ await startProject(outDir, "None", name ?? config.name);
636
+ let typescriptFile;
637
+ let stringsFile;
638
+ const scenes = [];
639
+ const promises = [];
640
+ if (config.attachments) {
641
+ let unamedAttachmentIndex = 0;
642
+ for (const attachment of config.attachments) {
643
+ let filename = attachment.filename?.trim();
644
+ if (!filename) {
645
+ unamedAttachmentIndex++;
646
+ let ext;
647
+ if (attachment.attachmentType === AttachmentType.TypeScript) ext = ".ts";
648
+ else if (attachment.attachmentType === AttachmentType.Strings) ext = ".strings.json";
649
+ else if (attachment.attachmentType === AttachmentType.SpatialData) ext = ".spatial.json";
650
+ if (unamedAttachmentIndex > 1) filename = `attachment_${unamedAttachmentIndex}${ext}`;
651
+ else filename = `attachment${ext}`;
652
+ }
653
+ if (attachment.attachmentType === AttachmentType.TypeScript) typescriptFile = filename;
654
+ if (attachment.attachmentType === AttachmentType.Strings) stringsFile = filename;
655
+ if (attachment.attachmentType === AttachmentType.SpatialData) continue;
656
+ promises.push(writeFileSafe(path.resolve(outDir, "src", filename), atob(attachment.attachmentData.original)));
657
+ }
658
+ }
659
+ if (config.mapRotation?.length) for (const map of config.mapRotation) {
660
+ promises.push(writeFileSafe(path.resolve(outDir, "src", "scenes", map.spatialAttachment.filename), atob(map.spatialAttachment.attachmentData.original)));
661
+ scenes.push([map.id, `src/scenes/${map.spatialAttachment.filename}`]);
662
+ }
663
+ const bf6ConfigContent = `export default defineBf6Config(${stringifyWithRaw({
664
+ name: name ?? config.name,
665
+ description: config.description,
666
+ outDir: "dist",
667
+ entrypoint: typescriptFile ? `src/${typescriptFile}` : void 0,
668
+ scenes: scenes ? scenes.map(([id, path$1]) => [`MapId.${getMapKeyByValue(id) ?? id}`, path$1]) : void 0,
669
+ strings: stringsFile ? `src/${stringsFile}` : void 0,
670
+ game: {
671
+ mutators: config.mutators,
672
+ assetRestrictions: config.assetRestrictions,
673
+ gameMode: config.gameMode,
674
+ teamComposition: config.teamComposition
675
+ }
676
+ })});\n`;
677
+ promises.push(writeFileSafe(path.resolve(outDir, "bf6.config.ts"), bf6ConfigContent));
678
+ await Promise.all(promises);
716
679
  }
717
680
  function stringifyWithRaw(value, options = {}) {
718
- return stringifyObject(value, {
719
- indent: " ",
720
- // 4 spaces
721
- singleQuotes: true,
722
- transform: (_obj, _prop, originalResult) => {
723
- if (/^['"]MapId\.[A-Za-z0-9_]+['"]$/.test(originalResult)) {
724
- return originalResult.slice(1, -1);
725
- }
726
- return originalResult;
727
- },
728
- ...options
729
- });
681
+ return stringifyObject(value, {
682
+ indent: " ",
683
+ singleQuotes: true,
684
+ transform: (_obj, _prop, originalResult) => {
685
+ if (/^['"]MapId\.[A-Za-z0-9_]+['"]$/.test(originalResult)) return originalResult.slice(1, -1);
686
+ return originalResult;
687
+ },
688
+ ...options
689
+ });
730
690
  }
731
691
 
732
- // src/cli/prepare.ts
733
- import fs5 from "fs";
734
- import path5 from "path";
735
- import { fileURLToPath as fileURLToPath3 } from "url";
736
- import colors3 from "colors";
737
- import { genExport, genInlineTypeImport } from "knitwork";
738
-
739
- // src/cli/utils.ts
740
- import colors2 from "colors";
741
-
742
- // ../../node_modules/ansi-regex/index.js
743
- function ansiRegex({ onlyFirst = false } = {}) {
744
- const ST = "(?:\\u0007|\\u001B\\u005C|\\u009C)";
745
- const osc = `(?:\\u001B\\][\\s\\S]*?${ST})`;
746
- const csi = "[\\u001B\\u009B][[\\]()#;?]*(?:\\d{1,4}(?:[;:]\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]";
747
- const pattern = `${osc}|${csi}`;
748
- return new RegExp(pattern, onlyFirst ? void 0 : "g");
749
- }
750
-
751
- // ../../node_modules/strip-ansi/index.js
752
- var regex = ansiRegex();
753
- function stripAnsi(string) {
754
- if (typeof string !== "string") {
755
- throw new TypeError(`Expected a \`string\`, got \`${typeof string}\``);
756
- }
757
- return string.replace(regex, "");
758
- }
759
-
760
- // src/cli/utils.ts
761
- var printToConsole = (message) => {
762
- const now = /* @__PURE__ */ new Date();
763
- const formattedTime = colors2.grey(now.toLocaleTimeString());
764
- const terminalWidth = process.stdout.columns || 80;
765
- const timeLength = stripAnsi(formattedTime).length;
766
- const messageLength = stripAnsi(message).length;
767
- const available = terminalWidth - (messageLength + timeLength);
768
- const spacing = available > 1 ? " ".repeat(available) : " ";
769
- console.log(`${message}${spacing}${formattedTime}`);
770
- };
771
-
772
- // src/cli/prepare.ts
692
+ //#endregion
693
+ //#region src/cli/prepare.ts
773
694
  async function prepare() {
774
- try {
775
- const __filename3 = fileURLToPath3(import.meta.url);
776
- const __dirname3 = path5.dirname(__filename3);
777
- const _workingDir = path5.resolve(".");
778
- const buildDir = path5.resolve(".bf6");
779
- const resources = path5.resolve(__dirname3, "../resources/prepare");
780
- fs5.cpSync(
781
- path5.resolve(resources, "tsconfig.json"),
782
- path5.resolve(buildDir, "tsconfig.json")
783
- );
784
- fs5.cpSync(
785
- path5.resolve(resources, "bf6.d.ts"),
786
- path5.resolve(buildDir, "bf6.d.ts")
787
- );
788
- fs5.cpSync(
789
- path5.resolve(resources, "types", "config.ts"),
790
- path5.resolve(buildDir, "types", "config.ts")
791
- );
792
- const ConfigFileExports = genExport("./types/config.ts", [
793
- "defineBf6Config"
794
- ]);
795
- fs5.writeFileSync(
796
- path5.resolve(buildDir, "imports.d.ts"),
797
- `${ConfigFileExports}
798
- `
799
- );
800
- const augmentations = {
801
- defineBf6Config: genInlineTypeImport(
802
- "./types/config.ts",
803
- `defineBf6Config`
804
- ),
805
- MapId: genInlineTypeImport("./types/config.ts", `MapId`)
806
- };
807
- const args = genNamespaceAugmentation("global", augmentations);
808
- fs5.writeFileSync(
809
- path5.resolve(buildDir, "globals.d.ts"),
810
- `export {}
811
-
812
- ${args}
813
- `
814
- );
815
- printToConsole(`${colors3.green("\u2714")} Types generated in .bf6`);
816
- } catch (error) {
817
- console.error(error);
818
- printToConsole(`${colors3.red("\u2717")} Types failed to generate in .bf6`);
819
- }
695
+ try {
696
+ const __filename$2 = fileURLToPath(import.meta.url);
697
+ const __dirname$2 = path.dirname(__filename$2);
698
+ path.resolve(".");
699
+ const buildDir = path.resolve(".bf6");
700
+ const resources = path.resolve(__dirname$2, "../resources/prepare");
701
+ fs.cpSync(path.resolve(resources, "tsconfig.json"), path.resolve(buildDir, "tsconfig.json"));
702
+ fs.cpSync(path.resolve(resources, "bf6.d.ts"), path.resolve(buildDir, "bf6.d.ts"));
703
+ fs.cpSync(path.resolve(resources, "types", "config.ts"), path.resolve(buildDir, "types", "config.ts"));
704
+ const ConfigFileExports = genExport("./types/config.ts", ["defineBf6Config"]);
705
+ fs.writeFileSync(path.resolve(buildDir, "imports.d.ts"), `${ConfigFileExports}\n`);
706
+ const args = genNamespaceAugmentation("global", {
707
+ defineBf6Config: genInlineTypeImport("./types/config.ts", `defineBf6Config`),
708
+ MapId: genInlineTypeImport("./types/config.ts", `MapId`)
709
+ });
710
+ fs.writeFileSync(path.resolve(buildDir, "globals.d.ts"), `export {}\n\n${args}\n`);
711
+ printToConsole(`${colors.green.bold("")} Types generated in .bf6`);
712
+ } catch (error) {
713
+ console.error(error);
714
+ printToConsole(`${colors.red.bold("")} Types failed to generate in .bf6`, true);
715
+ }
820
716
  }
821
- var genNamespaceAugmentation = (name, contents) => {
822
- if (!contents || Object.keys(contents).length === 0)
823
- return `declare ${name} {}`;
824
- const decls = Object.entries(contents).map(([k, v]) => ` const ${k}: ${v};`).join("\n");
825
- return `declare ${name} {
826
- ${decls}
827
- }`;
717
+ const genNamespaceAugmentation = (name, contents) => {
718
+ if (!contents || Object.keys(contents).length === 0) return `declare ${name} {}`;
719
+ return `declare ${name} {\n${Object.entries(contents).map(([k, v]) => `\tconst ${k}: ${v};`).join("\n")}\n}`;
828
720
  };
829
721
 
830
- // src/cli/index.ts
831
- var program = new Command();
832
- program.name(Object.keys(package_default.bin)[0]).description(package_default.description).version(package_default.version);
722
+ //#endregion
723
+ //#region src/cli/index.ts
724
+ const program = new Command();
725
+ program.name(Object.keys(bin)[0]).description(description).version(version);
833
726
  program.command("init").argument("[directory]").description("Create a new bf6 mod").action(async (directory) => {
834
- await init(directory);
727
+ await init(directory);
835
728
  });
836
729
  program.command("build").description("build the bf6 mod").action(async () => {
837
- await build();
730
+ await build();
838
731
  });
839
732
  program.command("prepare").description("prepare the types for bf6 mod").action(async () => {
840
- await prepare();
733
+ await prepare();
841
734
  });
842
735
  program.command("dev").description("watch the changes in src, and recompile as needed").action(async () => {
843
- await dev();
736
+ await dev();
844
737
  });
845
738
  program.command("import").argument("<input>").argument("<output>").description("decompiles the json config of a mod into a new project").action(async (input, output) => {
846
- await importFile(input, output);
847
- installDependencies(output);
739
+ await importFile(input, output, void 0, true);
740
+ installDependencies(output);
741
+ });
742
+ program.command("log").argument("[input]").description("logs the output from a locally running server").action(async (input) => {
743
+ new Bf6Logger(input).start();
848
744
  });
849
745
  program.exitOverride((_err) => {
850
- if (process.env.EXIT_CODE === "none") process.exit(0);
746
+ if (process.env.EXIT_CODE === "none") process.exit(0);
851
747
  });
852
748
  program.parse();
749
+
750
+ //#endregion
751
+ export { };
853
752
  //# sourceMappingURL=index.js.map