@bf6mods/cli 1.1.1 → 1.2.0

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,737 @@
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.0";
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) {
212
+ const strPath = path.resolve(workingDir, config.strings);
213
+ if (!fs.existsSync(strPath)) throw new Error("Cannot find strings file");
214
+ const attachment = createStringsAttachment(strPath, await fs.promises.readFile(strPath, "utf8"), generatedStrings);
215
+ attachments.push(attachment);
216
+ }
217
+ if (config.scenes) {
218
+ let mapIdx = 0;
219
+ for (const map of config.scenes) {
220
+ let mapId;
221
+ let scene;
222
+ if (Array.isArray(map)) [mapId, scene] = map;
223
+ else {
224
+ mapId = map;
225
+ scene = path.resolve(spatialsDir, `${mapId}.spatial.json`);
226
+ }
227
+ const scenePath = path.resolve(workingDir, scene);
228
+ if (!fs.existsSync(scenePath)) throw new Error(`Cannot find spatial data file: ${scene}`);
229
+ const spatial = createSpatialAttachment(scenePath, await fs.promises.readFile(scenePath, "utf8"), mapIdx++);
230
+ attachments.push(spatial);
231
+ mapRotation.push({
232
+ id: mapId,
233
+ spatialAttachment: spatial
234
+ });
235
+ }
236
+ }
237
+ return {
238
+ attachments,
239
+ mapRotation
240
+ };
316
241
  }
242
+ /**
243
+ * Writes the mod.json output to disk.
244
+ */
317
245
  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
- }
246
+ const baseGame = config.game;
247
+ const finalJson = {
248
+ name: config.name,
249
+ description: config.description,
250
+ gameMode: "ModBuilderCustom",
251
+ mutators: baseGame.mutators ?? {},
252
+ assetRestrictions: baseGame.assetRestrictions ?? {},
253
+ teamComposition: baseGame.teamComposition ?? [],
254
+ mapRotation,
255
+ attachments
256
+ };
257
+ const jsonOutput = minify ? JSON.stringify(finalJson) : JSON.stringify(finalJson, null, 2);
258
+ await fs.promises.writeFile(path.resolve(outDir, "mod.json"), jsonOutput);
259
+ if (config.outputArtifacts && finalJson?.attachments) for (const attachment of finalJson.attachments) {
260
+ const attachmentsDir = path.resolve(outDir, "attachments");
261
+ if (!fs.existsSync(attachmentsDir)) fs.mkdirSync(attachmentsDir, { recursive: true });
262
+ await fs.promises.writeFile(path.resolve(outDir, "attachments", attachment.filename), atob(attachment.attachmentData.original));
263
+ }
344
264
  }
345
265
  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
- };
266
+ return {
267
+ id: crypto.randomUUID(),
268
+ version: "1.0",
269
+ filename: `${path.parse(filePath).name}.js`,
270
+ isProcessable: true,
271
+ processingStatus: 2,
272
+ attachmentType: AttachmentType.TypeScript,
273
+ attachmentData: {
274
+ original: toBase64(compiled),
275
+ compiled: ""
276
+ },
277
+ errors: []
278
+ };
356
279
  }
357
280
  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
- };
281
+ let result = raw;
282
+ if (generatedStrings) result = JSON.stringify({
283
+ ...generatedStrings,
284
+ ...JSON.parse(raw)
285
+ }, null, 4);
286
+ return {
287
+ id: crypto.randomUUID(),
288
+ version: "1.0",
289
+ filename: path.basename(filePath),
290
+ isProcessable: true,
291
+ processingStatus: 2,
292
+ attachmentType: AttachmentType.Strings,
293
+ attachmentData: {
294
+ original: toBase64(result),
295
+ compiled: ""
296
+ },
297
+ errors: []
298
+ };
379
299
  }
380
300
  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
- };
301
+ return {
302
+ id: crypto.randomUUID(),
303
+ version: "1.0",
304
+ filename: path.basename(filePath),
305
+ isProcessable: true,
306
+ processingStatus: 2,
307
+ attachmentType: AttachmentType.SpatialData,
308
+ attachmentData: {
309
+ original: toBase64(raw),
310
+ compiled: ""
311
+ },
312
+ metadata: `mapIdx=${mapIdx}`,
313
+ errors: []
314
+ };
392
315
  }
393
316
  function toBase64(input) {
394
- return Buffer.isBuffer(input) ? input.toString("base64") : Buffer.from(input, "utf8").toString("base64");
317
+ return Buffer.isBuffer(input) ? input.toString("base64") : Buffer.from(input, "utf8").toString("base64");
395
318
  }
396
319
 
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";
320
+ //#endregion
321
+ //#region src/cli/log.ts
322
+ var Bf6Logger = class Bf6Logger {
323
+ constructor(input) {
324
+ if (Bf6Logger.instance) {
325
+ printToConsole(colors.yellow("⚠ Existing logger found — closing it first..."));
326
+ Bf6Logger.instance.stop();
327
+ }
328
+ if (input) this.watchTarget = path.resolve(input);
329
+ else if (process.env.LOCALAPPDATA) this.watchTarget = path.resolve(process.env.LOCALAPPDATA, "temp", "Battlefieldâ„¢ 6", "PortalLog.txt");
330
+ else throw new Error(`${colors.red.bold("✗")} Env variable LOCALAPPDATA not defined, and input file to watch was not provided!`);
331
+ if (!fs.existsSync(this.watchTarget)) throw new Error(`${colors.red.bold("✗")} Cannot find file '${this.watchTarget}' to watch for changes`);
332
+ this.lastSize = fs.statSync(this.watchTarget).size;
333
+ Bf6Logger.instance = this;
334
+ }
335
+ async start() {
336
+ printToConsole(colors.cyan(`👀 Watching Battlefield logs at: ${colors.yellow(this.watchTarget)}`));
337
+ this.watcher = chokidar.watch(this.watchTarget, {
338
+ persistent: true,
339
+ ignoreInitial: true
340
+ });
341
+ this.watcher.on("change", this.handleChange.bind(this));
342
+ }
343
+ async stop() {
344
+ if (this.watcher) {
345
+ this.watcher.close();
346
+ this.watcher = void 0;
347
+ printToConsole(colors.yellow(`${colors.red.bold("✗")} Stopped watching: ${colors.gray(this.watchTarget)}`));
348
+ }
349
+ Bf6Logger.instance = void 0;
350
+ }
351
+ handleChange(file, stats) {
352
+ if (!stats) return;
353
+ if (stats.size > this.lastSize) {
354
+ const newBytes = stats.size - this.lastSize;
355
+ const fd = fs.openSync(file, "r");
356
+ const buffer = Buffer.alloc(newBytes);
357
+ fs.readSync(fd, buffer, 0, newBytes, this.lastSize);
358
+ fs.closeSync(fd);
359
+ const newContent = buffer.toString("utf8").split("QuickJS: ").filter((debug) => debug.trim() !== "").map((debug) => debug.trim()).map((debug) => {
360
+ if (debug.startsWith("console.log: ")) return {
361
+ type: "console.log",
362
+ text: debug.replace("console.log: ", "")
363
+ };
364
+ else if (debug.startsWith("Exception:")) return {
365
+ type: "exception",
366
+ text: debug.replace("Exception:", "")
367
+ };
368
+ return {
369
+ type: "unknown",
370
+ text: debug
371
+ };
372
+ });
373
+ for (const content of newContent) if (content.type === "console.log") printToConsole(colors.gray(content.text));
374
+ else if (content.type === "exception") printToConsole(colors.red(content.text), true);
375
+ else printToConsole(colors.bold(content.text));
376
+ }
377
+ this.lastSize = stats.size;
378
+ }
379
+ };
380
+
381
+ //#endregion
382
+ //#region src/cli/dev.ts
403
383
  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();
384
+ const workingDir = path.resolve(".");
385
+ let config = await getBf6Config(workingDir);
386
+ const outDir = path.resolve(workingDir, config.outDir);
387
+ fs.mkdirSync(outDir, { recursive: true });
388
+ printToConsole(colors.cyan(`▶ Starting dev for ${config.name}`));
389
+ let watcher;
390
+ function debounce(fn, delay) {
391
+ let timer;
392
+ return (...args) => {
393
+ if (timer) clearTimeout(timer);
394
+ timer = setTimeout(() => fn(...args), delay);
395
+ };
396
+ }
397
+ const rebuild = async (trigger) => {
398
+ printToConsole(colors.yellow(`↻ Change detected in ${path.basename(trigger)}, rebuilding...`));
399
+ const start = performance.now();
400
+ try {
401
+ await build();
402
+ const duration = ((performance.now() - start) / 1e3).toFixed(2);
403
+ printToConsole(`${colors.green.bold("✓")} Updated mod.json (${duration}s)`);
404
+ } catch (err) {
405
+ printToConsole(colors.red(`${colors.red.bold("✗")} Rebuild failed: ${err.message}`), true);
406
+ }
407
+ };
408
+ const debouncedRebuild = debounce(rebuild, 200);
409
+ async function collectWatchTargets() {
410
+ const targets = [];
411
+ if (config.entrypoint) targets.push(path.resolve(workingDir, config.entrypoint));
412
+ if (config.scenes) for (const [, scene] of config.scenes) targets.push(path.resolve(workingDir, scene));
413
+ if (config.strings) targets.push(path.resolve(workingDir, config.strings));
414
+ for await (const entry of glob("bf6.config.*")) targets.push(entry);
415
+ for await (const entry of glob("src/**/*")) targets.push(entry);
416
+ return targets;
417
+ }
418
+ async function setupWatcher() {
419
+ if (watcher) {
420
+ await watcher.close();
421
+ printToConsole(colors.grey("♻ Reloading watcher due to config change..."));
422
+ await new Promise((r) => setTimeout(r, 10));
423
+ }
424
+ const watchTargets = await collectWatchTargets();
425
+ printToConsole(colors.cyan(`👀 Watching ${watchTargets.length} files...`));
426
+ watcher = chokidar.watch(watchTargets, {
427
+ persistent: true,
428
+ ignoreInitial: true,
429
+ awaitWriteFinish: {
430
+ stabilityThreshold: 250,
431
+ pollInterval: 100
432
+ },
433
+ ignored: [
434
+ outDir,
435
+ `${outDir}/**`,
436
+ "node_modules/**",
437
+ ".git/**"
438
+ ]
439
+ });
440
+ watcher.on("error", (err) => {
441
+ printToConsole(colors.red(`⚠ Watcher error: ${err.message}`), true);
442
+ setTimeout(setupWatcher, 1e3);
443
+ });
444
+ watcher.on("change", async (file) => {
445
+ if (file.includes("bf6.config.")) {
446
+ printToConsole(colors.magenta("⚙ Config changed — reloading watcher..."));
447
+ config = await getBf6Config(workingDir);
448
+ await setupWatcher();
449
+ return;
450
+ }
451
+ debouncedRebuild(file);
452
+ });
453
+ watcher.on("add", debouncedRebuild);
454
+ await rebuild("initial");
455
+ }
456
+ process.on("SIGINT", async () => {
457
+ printToConsole(colors.red(`\n${colors.red.bold("✗")} Exiting dev mode...`));
458
+ await watcher?.close();
459
+ process.exit(0);
460
+ });
461
+ await setupWatcher();
462
+ let logger;
463
+ try {
464
+ logger = new Bf6Logger();
465
+ logger.start();
466
+ } catch (error) {
467
+ printToConsole(colors.grey("Failed to start logging for the following reason (building will still work)"), true);
468
+ printToConsole(error.message, true);
469
+ }
476
470
  }
477
471
 
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");
472
+ //#endregion
473
+ //#region src/cli/init.ts
474
+ const __filename = fileURLToPath(import.meta.url);
475
+ const __dirname = path.dirname(__filename);
476
+ const templatesDir = path.resolve(__dirname, "../resources/templates");
493
477
  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
- }
478
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
479
+ for (const entry of entries) {
480
+ const oldPath = path.join(dir, entry.name);
481
+ let newPath = oldPath;
482
+ if (entry.name.includes("{name}")) {
483
+ const newName = entry.name.replace("{name}", modName);
484
+ newPath = path.join(dir, newName);
485
+ fs.renameSync(oldPath, newPath);
486
+ }
487
+ if (entry.name === "bf6.config.ts") {
488
+ let content = fs.readFileSync(newPath, "utf-8");
489
+ content = content.replace(/{bf6ConfigName}/g, modName);
490
+ fs.writeFileSync(newPath, content, "utf-8");
491
+ }
492
+ if (entry.name === "package.json") {
493
+ const pkg = JSON.parse(fs.readFileSync(newPath, "utf-8"));
494
+ pkg.name = modName;
495
+ fs.writeFileSync(newPath, JSON.stringify(pkg, null, 2));
496
+ }
497
+ if (fs.statSync(newPath).isDirectory()) renameFilesRecursively(newPath, modName);
498
+ }
517
499
  }
518
- var templates = [
519
- "Basic",
520
- "AcePursuit",
521
- "BombSquad",
522
- "Exfil",
523
- "Vertigo"
500
+ const templates = [
501
+ "Basic",
502
+ "AcePursuit",
503
+ "BombSquad",
504
+ "Exfil",
505
+ "Vertigo"
524
506
  ];
525
507
  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);
508
+ if ([
509
+ "AcePursuit",
510
+ "BombSquad",
511
+ "Exfil",
512
+ "Vertigo"
513
+ ].includes(template)) await importFile(path.resolve(templatesDir, `${template}.json`), destination, name);
514
+ else if (template === "None") {} else {
515
+ const templateDir = path.resolve(templatesDir, template);
516
+ fs.cpSync(templateDir, destination, { recursive: true });
517
+ }
518
+ fs.cpSync(path.resolve(templatesDir, "All"), destination, { recursive: true });
519
+ if (name) renameFilesRecursively(destination, name);
538
520
  }
539
521
  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
- }
522
+ try {
523
+ child_process.execSync(`npm install`, {
524
+ stdio: "inherit",
525
+ cwd: projectDir
526
+ });
527
+ return true;
528
+ } catch (_err) {
529
+ return false;
530
+ }
549
531
  }
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";
532
+ const cancel = () => prompts.cancel("Operation cancelled");
533
+ function isEmpty(path$1) {
534
+ const files = fs.readdirSync(path$1);
535
+ return files.length === 0 || files.length === 1 && files[0] === ".git";
554
536
  }
555
537
  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
- }
538
+ if (!fs.existsSync(dir)) return;
539
+ for (const file of fs.readdirSync(dir)) {
540
+ if (file === ".git") continue;
541
+ fs.rmSync(path.resolve(dir, file), {
542
+ recursive: true,
543
+ force: true
544
+ });
545
+ }
565
546
  }
566
547
  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.");
548
+ prompts.intro("Initialize Bf6 Mod");
549
+ const path$1 = argTargetDir ? argTargetDir : await prompts.text({
550
+ message: "Where should we create your project?",
551
+ placeholder: "./ace-pursuit",
552
+ validate: (value) => {
553
+ if (!value) return "Please enter a path.";
554
+ if (value[0] !== ".") return "Please enter a relative path.";
555
+ }
556
+ });
557
+ if (prompts.isCancel(path$1)) return cancel();
558
+ let name = await prompts.text({
559
+ message: "What is the name of your mod?",
560
+ placeholder: "Ace Pursuit",
561
+ validate: (value) => {
562
+ if (!value.trim()) return "Please enter a name.";
563
+ }
564
+ });
565
+ if (prompts.isCancel(name)) return cancel();
566
+ name = name.trim();
567
+ if (fs.existsSync(path$1) && !isEmpty(path$1)) {
568
+ let overwrite;
569
+ const res = await prompts.select({
570
+ message: (path$1 === "." ? "Current directory" : `Target directory "${path$1}"`) + ` is not empty. Please choose how to proceed:`,
571
+ options: [
572
+ {
573
+ label: "Cancel operation",
574
+ value: "no"
575
+ },
576
+ {
577
+ label: "Remove existing files and continue",
578
+ value: "yes"
579
+ },
580
+ {
581
+ label: "Ignore files and continue",
582
+ value: "ignore"
583
+ }
584
+ ]
585
+ });
586
+ if (prompts.isCancel(res)) return cancel();
587
+ overwrite = res;
588
+ switch (overwrite) {
589
+ case "yes":
590
+ emptyDir(path$1);
591
+ break;
592
+ case "no":
593
+ cancel();
594
+ return;
595
+ }
596
+ }
597
+ const template = await prompts.select({
598
+ message: "Select a template:",
599
+ options: templates.map((template$1) => {
600
+ return {
601
+ label: template$1,
602
+ value: template$1
603
+ };
604
+ })
605
+ });
606
+ if (prompts.isCancel(template)) return cancel();
607
+ await startProject(path$1, template, name);
608
+ const s = prompts.spinner();
609
+ s.start("Installing via npm");
610
+ if (installDependencies(path$1)) s.stop("Installed via npm");
611
+ else s.stop("Failed to install via npm", 1);
612
+ const nextSteps = `cd ${path$1}\nnpm run build`;
613
+ prompts.note(nextSteps, "Next steps.");
635
614
  }
636
615
 
637
- // src/cli/import.ts
616
+ //#endregion
617
+ //#region src/cli/import.ts
638
618
  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);
619
+ const dir = path.dirname(filePath);
620
+ await fs.promises.mkdir(dir, { recursive: true });
621
+ await fs.promises.writeFile(filePath, data);
642
622
  }
643
623
  function getMapKeyByValue(value) {
644
- return Object.keys(MapId).find(
645
- (k) => MapId[k] === value
646
- );
624
+ return Object.keys(MapId).find((k) => MapId[k] === value);
647
625
  }
648
626
  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);
627
+ const workingDir = path.resolve(".");
628
+ const entrypoint = path.resolve(workingDir, input);
629
+ const outDir = path.resolve(workingDir, output);
630
+ if (!fs.existsSync(entrypoint)) throw new Error("Cannot find strings file");
631
+ const config = JSON.parse(await fs.promises.readFile(entrypoint, { encoding: "utf8" }));
632
+ if (!fs.existsSync(outDir)) await fs.promises.mkdir(outDir, { recursive: true });
633
+ await startProject(outDir, "None", name ?? config.name);
634
+ let typescriptFile;
635
+ let stringsFile;
636
+ const scenes = [];
637
+ const promises = [];
638
+ if (config.attachments) for (const attachment of config.attachments) {
639
+ if (attachment.attachmentType === AttachmentType.TypeScript) typescriptFile = attachment.filename;
640
+ if (attachment.attachmentType === AttachmentType.Strings) stringsFile = attachment.filename;
641
+ if (attachment.attachmentType === AttachmentType.SpatialData) continue;
642
+ promises.push(writeFileSafe(path.resolve(outDir, "src", attachment.filename), atob(attachment.attachmentData.original)));
643
+ }
644
+ if (config.mapRotation?.length) for (const map of config.mapRotation) {
645
+ promises.push(writeFileSafe(path.resolve(outDir, "src", "scenes", map.spatialAttachment.filename), atob(map.spatialAttachment.attachmentData.original)));
646
+ scenes.push([map.id, `src/scenes/${map.spatialAttachment.filename}`]);
647
+ }
648
+ const bf6ConfigContent = `export default defineBf6Config(${stringifyWithRaw({
649
+ name: name ?? config.name,
650
+ description: config.description,
651
+ outDir: "dist",
652
+ entrypoint: typescriptFile ? `src/${typescriptFile}` : void 0,
653
+ scenes: scenes ? scenes.map(([id, path$1]) => [`MapId.${getMapKeyByValue(id) ?? id}`, path$1]) : void 0,
654
+ strings: stringsFile ? `src/${stringsFile}` : void 0,
655
+ game: {
656
+ mutators: config.mutators,
657
+ assetRestrictions: config.assetRestrictions,
658
+ gameMode: config.gameMode,
659
+ teamComposition: config.teamComposition
660
+ }
661
+ })});\n`;
662
+ promises.push(writeFileSafe(path.resolve(outDir, "bf6.config.ts"), bf6ConfigContent));
663
+ await Promise.all(promises);
716
664
  }
717
665
  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
- });
666
+ return stringifyObject(value, {
667
+ indent: " ",
668
+ singleQuotes: true,
669
+ transform: (_obj, _prop, originalResult) => {
670
+ if (/^['"]MapId\.[A-Za-z0-9_]+['"]$/.test(originalResult)) return originalResult.slice(1, -1);
671
+ return originalResult;
672
+ },
673
+ ...options
674
+ });
730
675
  }
731
676
 
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
677
+ //#endregion
678
+ //#region src/cli/prepare.ts
773
679
  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
- }
680
+ try {
681
+ const __filename$2 = fileURLToPath(import.meta.url);
682
+ const __dirname$2 = path.dirname(__filename$2);
683
+ path.resolve(".");
684
+ const buildDir = path.resolve(".bf6");
685
+ const resources = path.resolve(__dirname$2, "../resources/prepare");
686
+ fs.cpSync(path.resolve(resources, "tsconfig.json"), path.resolve(buildDir, "tsconfig.json"));
687
+ fs.cpSync(path.resolve(resources, "bf6.d.ts"), path.resolve(buildDir, "bf6.d.ts"));
688
+ fs.cpSync(path.resolve(resources, "types", "config.ts"), path.resolve(buildDir, "types", "config.ts"));
689
+ const ConfigFileExports = genExport("./types/config.ts", ["defineBf6Config"]);
690
+ fs.writeFileSync(path.resolve(buildDir, "imports.d.ts"), `${ConfigFileExports}\n`);
691
+ const args = genNamespaceAugmentation("global", {
692
+ defineBf6Config: genInlineTypeImport("./types/config.ts", `defineBf6Config`),
693
+ MapId: genInlineTypeImport("./types/config.ts", `MapId`)
694
+ });
695
+ fs.writeFileSync(path.resolve(buildDir, "globals.d.ts"), `export {}\n\n${args}\n`);
696
+ printToConsole(`${colors.green.bold("")} Types generated in .bf6`);
697
+ } catch (error) {
698
+ console.error(error);
699
+ printToConsole(`${colors.red.bold("")} Types failed to generate in .bf6`, true);
700
+ }
820
701
  }
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
- }`;
702
+ const genNamespaceAugmentation = (name, contents) => {
703
+ if (!contents || Object.keys(contents).length === 0) return `declare ${name} {}`;
704
+ return `declare ${name} {\n${Object.entries(contents).map(([k, v]) => `\tconst ${k}: ${v};`).join("\n")}\n}`;
828
705
  };
829
706
 
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);
707
+ //#endregion
708
+ //#region src/cli/index.ts
709
+ const program = new Command();
710
+ program.name(Object.keys(bin)[0]).description(description).version(version);
833
711
  program.command("init").argument("[directory]").description("Create a new bf6 mod").action(async (directory) => {
834
- await init(directory);
712
+ await init(directory);
835
713
  });
836
714
  program.command("build").description("build the bf6 mod").action(async () => {
837
- await build();
715
+ await build();
838
716
  });
839
717
  program.command("prepare").description("prepare the types for bf6 mod").action(async () => {
840
- await prepare();
718
+ await prepare();
841
719
  });
842
720
  program.command("dev").description("watch the changes in src, and recompile as needed").action(async () => {
843
- await dev();
721
+ await dev();
844
722
  });
845
723
  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);
724
+ await importFile(input, output);
725
+ installDependencies(output);
726
+ });
727
+ program.command("log").argument("[input]").description("logs the output from a locally running server").action(async (input) => {
728
+ new Bf6Logger(input).start();
848
729
  });
849
730
  program.exitOverride((_err) => {
850
- if (process.env.EXIT_CODE === "none") process.exit(0);
731
+ if (process.env.EXIT_CODE === "none") process.exit(0);
851
732
  });
852
733
  program.parse();
734
+
735
+ //#endregion
736
+ export { };
853
737
  //# sourceMappingURL=index.js.map