@mafly/bdev-cli 1.0.4 → 1.0.6

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,11 +93,18 @@ 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;
99
- let rebuildPending = false;
103
+ let queuedFilePath = null;
100
104
  let rebuildTimeout = null;
105
+ let lastReloadTime = 0;
106
+ const RELOAD_COOLDOWN_MS = 1200;
107
+ const DEBOUNCE_MS = 350;
101
108
  const ignoredPatterns = [
102
109
  /(^|[/\\])\../, // dotfiles
103
110
  /node_modules/,
@@ -115,11 +122,41 @@ export async function devCommand(dirArg, options = {}) {
115
122
  persistent: true,
116
123
  ignoreInitial: true,
117
124
  });
125
+ const scheduleRebuild = (filePath) => {
126
+ queuedFilePath = filePath;
127
+ if (rebuildTimeout) {
128
+ clearTimeout(rebuildTimeout);
129
+ }
130
+ rebuildTimeout = setTimeout(() => {
131
+ rebuildTimeout = null;
132
+ if (!isRebuilding && queuedFilePath) {
133
+ const target = queuedFilePath;
134
+ queuedFilePath = null;
135
+ runRebuildCycle(target);
136
+ }
137
+ }, DEBOUNCE_MS);
138
+ };
118
139
  const runRebuildCycle = async (triggerPath) => {
119
140
  isRebuilding = true;
120
- rebuildPending = false;
121
141
  const rel = path.relative(rootDir, triggerPath);
142
+ const isManifestRelated = triggerPath.endsWith("manifest.json") || triggerPath.endsWith("package.json");
122
143
  try {
144
+ // If resource_pack was added while dev server is running
145
+ if (!hasRp && fs.existsSync(rpDir)) {
146
+ hasRp = true;
147
+ watcher.add(rpDir);
148
+ logger.info("Detected new Resource Pack. Watching resource_pack...");
149
+ if (shouldLink && mojang) {
150
+ const destRp = path.join(mojang.devResourcePacks, `${folderName}_RP`);
151
+ try {
152
+ linkPack(rpDir, destRp);
153
+ logger.success(`Linked Resource Pack -> ${destRp}`);
154
+ }
155
+ catch (err) {
156
+ logger.error(`Failed to link Resource Pack: ${err.message}`);
157
+ }
158
+ }
159
+ }
123
160
  if (triggerPath.endsWith("manifest.json")) {
124
161
  const res = validateManifest(triggerPath);
125
162
  if (res.valid) {
@@ -135,9 +172,15 @@ export async function devCommand(dirArg, options = {}) {
135
172
  const buildOk = await buildCommand(rootDir);
136
173
  if (buildOk && hotReloadServer) {
137
174
  if (hotReloadServer.clientCount > 0) {
138
- // Buffer 150ms before reload command so Proton/Wine file cache flushes
139
- await new Promise((resolve) => setTimeout(resolve, 150));
175
+ // Buffer 200ms before reload command so Proton/Wine file cache flushes
176
+ await new Promise((resolve) => setTimeout(resolve, 200));
177
+ // Enforce reload cooldown so rapid saves cannot crash Minecraft scripting engine
178
+ const elapsed = Date.now() - lastReloadTime;
179
+ if (elapsed < RELOAD_COOLDOWN_MS) {
180
+ await new Promise((resolve) => setTimeout(resolve, RELOAD_COOLDOWN_MS - elapsed));
181
+ }
140
182
  hotReloadServer.reload();
183
+ lastReloadTime = Date.now();
141
184
  logger.success("Sent /reload to Minecraft via WebSocket");
142
185
  }
143
186
  else {
@@ -145,30 +188,35 @@ export async function devCommand(dirArg, options = {}) {
145
188
  }
146
189
  }
147
190
  }
191
+ if (isManifestRelated) {
192
+ logger.warn("Note: Minecraft /reload only reloads scripts. Re-enter world to apply manifest/package.json changes.");
193
+ }
148
194
  }
149
195
  catch (err) {
150
196
  logger.error(`Error during rebuild: ${err.message}`);
151
197
  }
152
198
  finally {
153
199
  isRebuilding = false;
154
- if (rebuildPending) {
155
- runRebuildCycle(triggerPath);
200
+ if (queuedFilePath) {
201
+ const nextTarget = queuedFilePath;
202
+ queuedFilePath = null;
203
+ scheduleRebuild(nextTarget);
156
204
  }
157
205
  }
158
206
  };
159
207
  const triggerRebuild = (filePath) => {
208
+ // Ignore internal writes to manifest.json caused by syncManifestDependencies
209
+ if (filePath.endsWith("manifest.json") &&
210
+ Date.now() - getLastManifestSyncTime() < 600) {
211
+ return;
212
+ }
160
213
  const rel = path.relative(rootDir, filePath);
161
214
  logger.info(`Change detected in ${rel}`);
162
215
  if (isRebuilding) {
163
- rebuildPending = true;
216
+ queuedFilePath = filePath;
164
217
  return;
165
218
  }
166
- if (rebuildTimeout) {
167
- clearTimeout(rebuildTimeout);
168
- }
169
- rebuildTimeout = setTimeout(() => {
170
- runRebuildCycle(filePath);
171
- }, 300);
219
+ scheduleRebuild(filePath);
172
220
  };
173
221
  watcher.on("change", triggerRebuild);
174
222
  watcher.on("add", triggerRebuild);
@@ -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.6",
4
4
  "description": "Modern CLI tool for Minecraft Bedrock Edition Addon development",
5
5
  "author": "Mafly",
6
6
  "license": "MIT",