@mafly/bdev-cli 1.0.4 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,14 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { runBuild } from "../utils/bundler.js";
4
- import { validateManifest } from "../utils/manifest.js";
4
+ import { validateManifest, syncManifestDependencies, addResourcePack, } from "../utils/manifest.js";
5
5
  import { logger } from "../utils/logger.js";
6
6
  export async function buildCommand(dirArg, options = {}) {
7
7
  const rootDir = path.resolve(dirArg || ".");
8
8
  const bpDir = path.join(rootDir, "behavior_pack");
9
9
  const bpManifestPath = path.join(bpDir, "manifest.json");
10
+ // Sync @minecraft/* dependencies from package.json into manifest.json
11
+ syncManifestDependencies(rootDir);
10
12
  // 1. Validate Behavior Pack Manifest
11
13
  if (!fs.existsSync(bpManifestPath)) {
12
14
  logger.error(`Behavior pack manifest not found: ${bpManifestPath}`);
@@ -25,6 +27,10 @@ export async function buildCommand(dirArg, options = {}) {
25
27
  // Validate Resource Pack Manifest if exists
26
28
  const rpDir = path.join(rootDir, "resource_pack");
27
29
  const rpManifestPath = path.join(rpDir, "manifest.json");
30
+ if (fs.existsSync(rpDir) && !fs.existsSync(rpManifestPath)) {
31
+ logger.info("Found resource_pack folder without manifest. Initializing Resource Pack...");
32
+ addResourcePack(rootDir);
33
+ }
28
34
  if (fs.existsSync(rpManifestPath)) {
29
35
  const rpValidation = validateManifest(rpManifestPath);
30
36
  if (!rpValidation.valid) {
@@ -7,13 +7,13 @@ import { findMojangPath, linkPack } from "../utils/mojang.js";
7
7
  import { saveConfig } from "../utils/config.js";
8
8
  import { BedrockHotReloadServer } from "../utils/wsserver.js";
9
9
  import { logger } from "../utils/logger.js";
10
- import { validateManifest } from "../utils/manifest.js";
10
+ import { validateManifest, getLastManifestSyncTime } from "../utils/manifest.js";
11
11
  export async function devCommand(dirArg, options = {}) {
12
12
  const rootDir = path.resolve(dirArg || ".");
13
13
  const bpDir = path.join(rootDir, "behavior_pack");
14
14
  const rpDir = path.join(rootDir, "resource_pack");
15
15
  const hasBp = fs.existsSync(bpDir);
16
- const hasRp = fs.existsSync(rpDir);
16
+ let hasRp = fs.existsSync(rpDir);
17
17
  if (!hasBp && !hasRp) {
18
18
  logger.error(`No behavior_pack or resource_pack found in ${rootDir}. Run 'bdev init' first.`);
19
19
  process.exit(1);
@@ -33,8 +33,8 @@ export async function devCommand(dirArg, options = {}) {
33
33
  }
34
34
  // 2. Setup symlinks to com.mojang
35
35
  const shouldLink = options.link !== false;
36
+ const mojang = shouldLink ? findMojangPath(options.path, rootDir) : null;
36
37
  if (shouldLink) {
37
- const mojang = findMojangPath(options.path, rootDir);
38
38
  if (!mojang) {
39
39
  logger.warn("Minecraft com.mojang directory not found.");
40
40
  logger.warn("Tip: Set with 'bdev set-path <path>' or pass '-p <path>' or 'BEDROCK_MOJANG_PATH'.");
@@ -93,6 +93,10 @@ export async function devCommand(dirArg, options = {}) {
93
93
  if (hasRp) {
94
94
  watchPaths.push(rpDir);
95
95
  }
96
+ const pkgJsonPath = path.join(rootDir, "package.json");
97
+ if (fs.existsSync(pkgJsonPath)) {
98
+ watchPaths.push(pkgJsonPath);
99
+ }
96
100
  logger.info(`Starting file watcher on: ${watchPaths.map((p) => path.relative(rootDir, p)).join(", ")}`);
97
101
  logger.info("Press Ctrl+C to stop.");
98
102
  let isRebuilding = false;
@@ -120,6 +124,22 @@ export async function devCommand(dirArg, options = {}) {
120
124
  rebuildPending = false;
121
125
  const rel = path.relative(rootDir, triggerPath);
122
126
  try {
127
+ // If resource_pack was added while dev server is running
128
+ if (!hasRp && fs.existsSync(rpDir)) {
129
+ hasRp = true;
130
+ watcher.add(rpDir);
131
+ logger.info("Detected new Resource Pack. Watching resource_pack...");
132
+ if (shouldLink && mojang) {
133
+ const destRp = path.join(mojang.devResourcePacks, `${folderName}_RP`);
134
+ try {
135
+ linkPack(rpDir, destRp);
136
+ logger.success(`Linked Resource Pack -> ${destRp}`);
137
+ }
138
+ catch (err) {
139
+ logger.error(`Failed to link Resource Pack: ${err.message}`);
140
+ }
141
+ }
142
+ }
123
143
  if (triggerPath.endsWith("manifest.json")) {
124
144
  const res = validateManifest(triggerPath);
125
145
  if (res.valid) {
@@ -157,6 +177,11 @@ export async function devCommand(dirArg, options = {}) {
157
177
  }
158
178
  };
159
179
  const triggerRebuild = (filePath) => {
180
+ // Ignore internal writes to manifest.json caused by syncManifestDependencies
181
+ if (filePath.endsWith("manifest.json") &&
182
+ Date.now() - getLastManifestSyncTime() < 600) {
183
+ return;
184
+ }
160
185
  const rel = path.relative(rootDir, filePath);
161
186
  logger.info(`Change detected in ${rel}`);
162
187
  if (isRebuilding) {
@@ -1,16 +1,34 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { generateManifests } from "../utils/manifest.js";
3
+ import { generateManifests, addResourcePack, DEFAULT_ICON_BASE64, } from "../utils/manifest.js";
4
4
  import { saveConfig } from "../utils/config.js";
5
5
  import { resolveMinecraftDependencies } from "../utils/npm.js";
6
6
  import { logger } from "../utils/logger.js";
7
- const DEFAULT_ICON_BASE64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==";
8
7
  export async function initCommand(targetDirArg, options) {
9
8
  const targetDir = path.resolve(targetDirArg || ".");
10
9
  const projectName = options.name || path.basename(targetDir) || "my-bedrock-addon";
11
10
  const projectDesc = options.desc || `${projectName} - Bedrock Addon created with bdev`;
12
11
  const isTs = options.lang !== "js";
13
12
  const hasRp = options.rp !== false;
13
+ const existingBpManifest = path.join(targetDir, "behavior_pack", "manifest.json");
14
+ if (fs.existsSync(existingBpManifest)) {
15
+ if (hasRp) {
16
+ const existingRpManifest = path.join(targetDir, "resource_pack", "manifest.json");
17
+ if (!fs.existsSync(existingRpManifest)) {
18
+ logger.info("Existing Behavior Pack project detected without Resource Pack.");
19
+ addResourcePack(targetDir, { name: projectName, description: projectDesc });
20
+ return;
21
+ }
22
+ else {
23
+ logger.warn("Project already initialized with Behavior Pack and Resource Pack.");
24
+ return;
25
+ }
26
+ }
27
+ else {
28
+ logger.warn("Project is already initialized.");
29
+ return;
30
+ }
31
+ }
14
32
  logger.info(`Scaffolding Bedrock Addon project: ${projectName}`);
15
33
  logger.info(`Directory: ${targetDir}`);
16
34
  logger.info(`Language: ${isTs ? "TypeScript" : "JavaScript"} | Resource Pack: ${hasRp ? "Yes" : "No"}`);
@@ -1,3 +1,4 @@
1
+ export declare const DEFAULT_ICON_BASE64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==";
1
2
  export interface ManifestHeader {
2
3
  name: string;
3
4
  description: string;
@@ -40,3 +41,10 @@ export declare function generateManifests(opts: {
40
41
  bpManifest: BedrockManifest;
41
42
  rpManifest?: BedrockManifest;
42
43
  };
44
+ export interface AddResourcePackOptions {
45
+ name?: string;
46
+ description?: string;
47
+ }
48
+ export declare function addResourcePack(rootDir: string, options?: AddResourcePackOptions): boolean;
49
+ export declare function getLastManifestSyncTime(): number;
50
+ export declare function syncManifestDependencies(rootDir: string): boolean;
@@ -1,5 +1,8 @@
1
1
  import fs from "node:fs";
2
+ import path from "node:path";
2
3
  import { v4 as uuidv4, validate as validateUuid } from "uuid";
4
+ import { logger } from "./logger.js";
5
+ export const DEFAULT_ICON_BASE64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==";
3
6
  export function validateManifest(manifestPath) {
4
7
  const errors = [];
5
8
  if (!fs.existsSync(manifestPath)) {
@@ -144,3 +147,234 @@ export function generateManifests(opts) {
144
147
  }
145
148
  return { bpManifest, rpManifest };
146
149
  }
150
+ export function addResourcePack(rootDir, options = {}) {
151
+ const bpDir = path.join(rootDir, "behavior_pack");
152
+ const bpManifestPath = path.join(bpDir, "manifest.json");
153
+ if (!fs.existsSync(bpManifestPath)) {
154
+ logger.error(`Behavior pack manifest not found in ${rootDir}. Run 'bdev init' first.`);
155
+ return false;
156
+ }
157
+ const rpDir = path.join(rootDir, "resource_pack");
158
+ const rpManifestPath = path.join(rpDir, "manifest.json");
159
+ let bpManifest;
160
+ try {
161
+ bpManifest = JSON.parse(fs.readFileSync(bpManifestPath, "utf-8"));
162
+ }
163
+ catch (err) {
164
+ logger.error(`Failed to parse ${bpManifestPath}: ${err.message}`);
165
+ return false;
166
+ }
167
+ if (fs.existsSync(rpManifestPath)) {
168
+ try {
169
+ const rpManifest = JSON.parse(fs.readFileSync(rpManifestPath, "utf-8"));
170
+ const rpUuid = rpManifest?.header?.uuid;
171
+ if (rpUuid) {
172
+ if (!Array.isArray(bpManifest.dependencies)) {
173
+ bpManifest.dependencies = [];
174
+ }
175
+ const hasDep = bpManifest.dependencies.some((d) => d.uuid?.toLowerCase() === rpUuid.toLowerCase());
176
+ if (!hasDep) {
177
+ bpManifest.dependencies.push({
178
+ uuid: rpUuid,
179
+ version: rpManifest.header?.version || [1, 0, 0],
180
+ });
181
+ fs.writeFileSync(bpManifestPath, JSON.stringify(bpManifest, null, 2) + "\n");
182
+ logger.success("Linked existing Resource Pack to Behavior Pack manifest.");
183
+ return true;
184
+ }
185
+ }
186
+ }
187
+ catch {
188
+ // ignore
189
+ }
190
+ logger.warn("Resource pack already exists in this project.");
191
+ return false;
192
+ }
193
+ let projectName = options.name;
194
+ let projectDesc = options.description;
195
+ const pkgJsonPath = path.join(rootDir, "package.json");
196
+ if ((!projectName || !projectDesc) && fs.existsSync(pkgJsonPath)) {
197
+ try {
198
+ const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
199
+ if (!projectName && pkg.name)
200
+ projectName = pkg.name;
201
+ if (!projectDesc && pkg.description)
202
+ projectDesc = pkg.description;
203
+ }
204
+ catch {
205
+ // ignore
206
+ }
207
+ }
208
+ if (!projectName) {
209
+ projectName = (bpManifest.header?.name || path.basename(rootDir)).replace(/\s*\(BP\)$/i, "");
210
+ }
211
+ if (!projectDesc) {
212
+ projectDesc = bpManifest.header?.description || `${projectName} Resource Pack`;
213
+ }
214
+ const minEngineVersion = bpManifest.header?.min_engine_version || [1, 21, 0];
215
+ const rpHeaderUuid = uuidv4();
216
+ const rpModuleUuid = uuidv4();
217
+ const rpManifest = {
218
+ format_version: 2,
219
+ header: {
220
+ name: `${projectName} (RP)`,
221
+ description: projectDesc,
222
+ uuid: rpHeaderUuid,
223
+ version: [1, 0, 0],
224
+ min_engine_version: minEngineVersion,
225
+ },
226
+ modules: [
227
+ {
228
+ description: "Resource Pack Modules",
229
+ type: "resources",
230
+ uuid: rpModuleUuid,
231
+ version: [1, 0, 0],
232
+ },
233
+ ],
234
+ };
235
+ fs.mkdirSync(rpDir, { recursive: true });
236
+ fs.writeFileSync(rpManifestPath, JSON.stringify(rpManifest, null, 2) + "\n");
237
+ const bpIconPath = path.join(bpDir, "pack_icon.png");
238
+ const rpIconPath = path.join(rpDir, "pack_icon.png");
239
+ if (fs.existsSync(bpIconPath)) {
240
+ fs.copyFileSync(bpIconPath, rpIconPath);
241
+ }
242
+ else {
243
+ fs.writeFileSync(rpIconPath, Buffer.from(DEFAULT_ICON_BASE64, "base64"));
244
+ }
245
+ const textsDir = path.join(rpDir, "texts");
246
+ fs.mkdirSync(textsDir, { recursive: true });
247
+ fs.writeFileSync(path.join(textsDir, "en_US.lang"), `pack.name=${projectName}\npack.description=${projectDesc}\n`);
248
+ if (!Array.isArray(bpManifest.dependencies)) {
249
+ bpManifest.dependencies = [];
250
+ }
251
+ const existingRpIndex = bpManifest.dependencies.findIndex((d) => d.uuid && !d.module_name);
252
+ if (existingRpIndex !== -1) {
253
+ bpManifest.dependencies[existingRpIndex] = {
254
+ uuid: rpHeaderUuid,
255
+ version: [1, 0, 0],
256
+ };
257
+ }
258
+ else {
259
+ bpManifest.dependencies.push({
260
+ uuid: rpHeaderUuid,
261
+ version: [1, 0, 0],
262
+ });
263
+ }
264
+ fs.writeFileSync(bpManifestPath, JSON.stringify(bpManifest, null, 2) + "\n");
265
+ logger.success(`Resource Pack created and linked to Behavior Pack at ${rpDir}`);
266
+ return true;
267
+ }
268
+ let lastManifestSyncTime = 0;
269
+ export function getLastManifestSyncTime() {
270
+ return lastManifestSyncTime;
271
+ }
272
+ export function syncManifestDependencies(rootDir) {
273
+ const pkgPath = path.join(rootDir, "package.json");
274
+ const bpManifestPath = path.join(rootDir, "behavior_pack", "manifest.json");
275
+ if (!fs.existsSync(pkgPath) || !fs.existsSync(bpManifestPath)) {
276
+ return false;
277
+ }
278
+ let pkg;
279
+ let bpManifest;
280
+ try {
281
+ pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
282
+ bpManifest = JSON.parse(fs.readFileSync(bpManifestPath, "utf-8"));
283
+ }
284
+ catch {
285
+ return false;
286
+ }
287
+ const targetDeps = new Map();
288
+ if (pkg.devDependencies && typeof pkg.devDependencies === "object") {
289
+ for (const [name, ver] of Object.entries(pkg.devDependencies)) {
290
+ if (name.startsWith("@minecraft/") && typeof ver === "string") {
291
+ targetDeps.set(name, ver.replace(/^[~^>=v\s]+/, ""));
292
+ }
293
+ }
294
+ }
295
+ if (pkg.dependencies && typeof pkg.dependencies === "object") {
296
+ for (const [name, ver] of Object.entries(pkg.dependencies)) {
297
+ if (name.startsWith("@minecraft/") && typeof ver === "string") {
298
+ targetDeps.set(name, ver.replace(/^[~^>=v\s]+/, ""));
299
+ }
300
+ }
301
+ }
302
+ if (!Array.isArray(bpManifest.dependencies)) {
303
+ bpManifest.dependencies = [];
304
+ }
305
+ let modified = false;
306
+ const updatedModules = [];
307
+ // 1. Update existing @minecraft/* entries or prune removed ones
308
+ const toRemoveIndices = [];
309
+ bpManifest.dependencies.forEach((dep, idx) => {
310
+ if (dep.module_name && dep.module_name.startsWith("@minecraft/")) {
311
+ const targetVer = targetDeps.get(dep.module_name);
312
+ if (targetVer) {
313
+ if (dep.version !== targetVer) {
314
+ dep.version = targetVer;
315
+ modified = true;
316
+ updatedModules.push(`${dep.module_name}@${targetVer}`);
317
+ }
318
+ }
319
+ else if (dep.module_name !== "@minecraft/server") {
320
+ // Removed from package.json
321
+ toRemoveIndices.push(idx);
322
+ modified = true;
323
+ updatedModules.push(`-${dep.module_name}`);
324
+ }
325
+ }
326
+ });
327
+ if (toRemoveIndices.length > 0) {
328
+ bpManifest.dependencies = bpManifest.dependencies.filter((_, idx) => !toRemoveIndices.includes(idx));
329
+ }
330
+ // 2. Add new @minecraft/* dependencies from package.json that are missing in manifest
331
+ for (const [name, ver] of targetDeps.entries()) {
332
+ const exists = bpManifest.dependencies.some((d) => d.module_name === name);
333
+ if (!exists) {
334
+ bpManifest.dependencies.push({
335
+ module_name: name,
336
+ version: ver,
337
+ });
338
+ modified = true;
339
+ updatedModules.push(`+${name}@${ver}`);
340
+ }
341
+ }
342
+ // 3. Link resource pack if exists and not linked
343
+ const rpManifestPath = path.join(rootDir, "resource_pack", "manifest.json");
344
+ if (fs.existsSync(rpManifestPath)) {
345
+ try {
346
+ const rpManifest = JSON.parse(fs.readFileSync(rpManifestPath, "utf-8"));
347
+ const rpUuid = rpManifest?.header?.uuid;
348
+ if (rpUuid) {
349
+ const hasRpDep = bpManifest.dependencies.some((d) => d.uuid && d.uuid.toLowerCase() === rpUuid.toLowerCase());
350
+ if (!hasRpDep) {
351
+ const staleRpIndex = bpManifest.dependencies.findIndex((d) => d.uuid && !d.module_name);
352
+ if (staleRpIndex !== -1) {
353
+ bpManifest.dependencies[staleRpIndex] = {
354
+ uuid: rpUuid,
355
+ version: rpManifest.header?.version || [1, 0, 0],
356
+ };
357
+ }
358
+ else {
359
+ bpManifest.dependencies.push({
360
+ uuid: rpUuid,
361
+ version: rpManifest.header?.version || [1, 0, 0],
362
+ });
363
+ }
364
+ modified = true;
365
+ updatedModules.push(`linked RP (${rpUuid.slice(0, 8)}...)`);
366
+ }
367
+ }
368
+ }
369
+ catch {
370
+ // ignore
371
+ }
372
+ }
373
+ if (modified) {
374
+ lastManifestSyncTime = Date.now();
375
+ fs.writeFileSync(bpManifestPath, JSON.stringify(bpManifest, null, 2) + "\n");
376
+ logger.success(`Synchronized manifest.json dependencies: ${updatedModules.join(", ")}`);
377
+ return true;
378
+ }
379
+ return false;
380
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mafly/bdev-cli",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "Modern CLI tool for Minecraft Bedrock Edition Addon development",
5
5
  "author": "Mafly",
6
6
  "license": "MIT",