@mafly/bdev-cli 1.0.3 → 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,62 +93,107 @@ 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;
103
+ let rebuildPending = false;
99
104
  let rebuildTimeout = null;
105
+ const ignoredPatterns = [
106
+ /(^|[/\\])\../, // dotfiles
107
+ /node_modules/,
108
+ /dist/,
109
+ ];
110
+ if (fs.existsSync(srcDir)) {
111
+ // Ignore entire scripts directory in behavior pack when compiling from src
112
+ ignoredPatterns.push(/(^|[/\\])behavior_pack[/\\]scripts([/\\].*)?$/);
113
+ }
114
+ else {
115
+ ignoredPatterns.push(/\.map$/);
116
+ }
100
117
  const watcher = chokidar.watch(watchPaths, {
101
- ignored: [
102
- /(^|[/\\])\../, // dotfiles
103
- /node_modules/,
104
- /dist/,
105
- /behavior_pack[/\\]scripts[/\\]main\.js(\.map)?$/, // ignore build output
106
- ],
118
+ ignored: ignoredPatterns,
107
119
  persistent: true,
108
120
  ignoreInitial: true,
109
121
  });
110
- const triggerRebuild = (filePath) => {
111
- const rel = path.relative(rootDir, filePath);
112
- logger.info(`Change detected in ${rel}`);
113
- if (rebuildTimeout) {
114
- clearTimeout(rebuildTimeout);
115
- }
116
- rebuildTimeout = setTimeout(async () => {
117
- if (isRebuilding)
118
- return;
119
- isRebuilding = true;
120
- try {
121
- if (filePath.endsWith("manifest.json")) {
122
- const res = validateManifest(filePath);
123
- if (res.valid) {
124
- logger.success(`Validated ${rel}`);
122
+ const runRebuildCycle = async (triggerPath) => {
123
+ isRebuilding = true;
124
+ rebuildPending = false;
125
+ const rel = path.relative(rootDir, triggerPath);
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}`);
125
137
  }
126
- else {
127
- logger.error(`Invalid manifest in ${rel}:`);
128
- res.errors.forEach((e) => console.error(` - ${e}`));
138
+ catch (err) {
139
+ logger.error(`Failed to link Resource Pack: ${err.message}`);
129
140
  }
130
141
  }
131
- // Run build if src changed or if script output needed
132
- if (fs.existsSync(srcDir)) {
133
- const buildOk = await buildCommand(rootDir);
134
- if (buildOk && hotReloadServer) {
135
- if (hotReloadServer.clientCount > 0) {
136
- hotReloadServer.reload();
137
- logger.success("Sent /reload to Minecraft via WebSocket");
138
- }
139
- else {
140
- logger.info(`Hot reload skipped (no Minecraft client connected; run /connect localhost:${wsPort} in game)`);
141
- }
142
- }
142
+ }
143
+ if (triggerPath.endsWith("manifest.json")) {
144
+ const res = validateManifest(triggerPath);
145
+ if (res.valid) {
146
+ logger.success(`Validated ${rel}`);
147
+ }
148
+ else {
149
+ logger.error(`Invalid manifest in ${rel}:`);
150
+ res.errors.forEach((e) => console.error(` - ${e}`));
143
151
  }
144
152
  }
145
- catch (err) {
146
- logger.error(`Error during rebuild: ${err.message}`);
153
+ // Run build if src changed or if script output needed
154
+ if (fs.existsSync(srcDir)) {
155
+ const buildOk = await buildCommand(rootDir);
156
+ if (buildOk && hotReloadServer) {
157
+ if (hotReloadServer.clientCount > 0) {
158
+ // Buffer 150ms before reload command so Proton/Wine file cache flushes
159
+ await new Promise((resolve) => setTimeout(resolve, 150));
160
+ hotReloadServer.reload();
161
+ logger.success("Sent /reload to Minecraft via WebSocket");
162
+ }
163
+ else {
164
+ logger.info(`Hot reload skipped (no Minecraft client connected; run /connect localhost:${wsPort} in game)`);
165
+ }
166
+ }
147
167
  }
148
- finally {
149
- isRebuilding = false;
168
+ }
169
+ catch (err) {
170
+ logger.error(`Error during rebuild: ${err.message}`);
171
+ }
172
+ finally {
173
+ isRebuilding = false;
174
+ if (rebuildPending) {
175
+ runRebuildCycle(triggerPath);
150
176
  }
151
- }, 100);
177
+ }
178
+ };
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
+ }
185
+ const rel = path.relative(rootDir, filePath);
186
+ logger.info(`Change detected in ${rel}`);
187
+ if (isRebuilding) {
188
+ rebuildPending = true;
189
+ return;
190
+ }
191
+ if (rebuildTimeout) {
192
+ clearTimeout(rebuildTimeout);
193
+ }
194
+ rebuildTimeout = setTimeout(() => {
195
+ runRebuildCycle(filePath);
196
+ }, 300);
152
197
  };
153
198
  watcher.on("change", triggerRebuild);
154
199
  watcher.on("add", triggerRebuild);
@@ -1,28 +1,55 @@
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
+ import { resolveMinecraftDependencies } from "../utils/npm.js";
5
6
  import { logger } from "../utils/logger.js";
6
- const DEFAULT_ICON_BASE64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==";
7
7
  export async function initCommand(targetDirArg, options) {
8
8
  const targetDir = path.resolve(targetDirArg || ".");
9
9
  const projectName = options.name || path.basename(targetDir) || "my-bedrock-addon";
10
10
  const projectDesc = options.desc || `${projectName} - Bedrock Addon created with bdev`;
11
11
  const isTs = options.lang !== "js";
12
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
+ }
13
32
  logger.info(`Scaffolding Bedrock Addon project: ${projectName}`);
14
33
  logger.info(`Directory: ${targetDir}`);
15
34
  logger.info(`Language: ${isTs ? "TypeScript" : "JavaScript"} | Resource Pack: ${hasRp ? "Yes" : "No"}`);
16
35
  if (!fs.existsSync(targetDir)) {
17
36
  fs.mkdirSync(targetDir, { recursive: true });
18
37
  }
38
+ const spinner = logger.spinner("Fetching latest @minecraft/* versions from npm...");
39
+ const mcVersions = await resolveMinecraftDependencies([
40
+ "@minecraft/server",
41
+ "@minecraft/server-ui",
42
+ ]);
43
+ const serverVersion = mcVersions["@minecraft/server"] || "2.11.0";
44
+ const serverUiVersion = mcVersions["@minecraft/server-ui"] || "2.3.0";
45
+ spinner.succeed(`Resolved dependencies: @minecraft/server@${serverVersion}, @minecraft/server-ui@${serverUiVersion}`);
19
46
  const { bpManifest, rpManifest } = generateManifests({
20
47
  name: projectName,
21
48
  description: projectDesc,
22
49
  hasScript: true,
23
50
  hasRp,
24
51
  minEngineVersion: [1, 21, 0],
25
- serverVersion: "1.16.0",
52
+ serverVersion,
26
53
  });
27
54
  // 1. Behavior Pack
28
55
  const bpDir = path.join(targetDir, "behavior_pack");
@@ -40,24 +67,24 @@ export async function initCommand(targetDirArg, options) {
40
67
  fs.writeFileSync(path.join(textsDir, "en_US.lang"), `pack.name=${projectName}\npack.description=${projectDesc}\n`);
41
68
  }
42
69
  // 3. Scripts / Source code
43
- const starterCode = `import { world, system } from "@minecraft/server";
70
+ const starterCodeTs = `import { world, type Player } from "@minecraft/server";
44
71
 
45
- world.afterEvents.worldInitialize.subscribe(() => {
46
- world.sendMessage("Hello from ${projectName}!");
72
+ world.afterEvents.playerSpawn.subscribe((e) => {
73
+ const player: Player = e.player;
74
+ player.sendMessage("Welcome!");
47
75
  });
76
+ `;
77
+ const starterCodeJs = `import { world } from "@minecraft/server";
48
78
 
49
- let ticks = 0;
50
- system.runInterval(() => {
51
- ticks++;
52
- if (ticks % 100 === 0) {
53
- // Periodic tick logic
54
- }
55
- }, 20);
79
+ world.afterEvents.playerSpawn.subscribe((e) => {
80
+ const player = e.player;
81
+ player.sendMessage("Welcome!");
82
+ });
56
83
  `;
57
84
  if (isTs) {
58
85
  const srcDir = path.join(targetDir, "src");
59
86
  fs.mkdirSync(srcDir, { recursive: true });
60
- fs.writeFileSync(path.join(srcDir, "main.ts"), starterCode);
87
+ fs.writeFileSync(path.join(srcDir, "main.ts"), starterCodeTs);
61
88
  // tsconfig.json
62
89
  const tsconfig = {
63
90
  compilerOptions: {
@@ -75,7 +102,7 @@ system.runInterval(() => {
75
102
  else {
76
103
  const scriptsDir = path.join(bpDir, "scripts");
77
104
  fs.mkdirSync(scriptsDir, { recursive: true });
78
- fs.writeFileSync(path.join(scriptsDir, "main.js"), starterCode);
105
+ fs.writeFileSync(path.join(scriptsDir, "main.js"), starterCodeJs);
79
106
  }
80
107
  // 4. package.json
81
108
  const pkgJson = {
@@ -89,8 +116,8 @@ system.runInterval(() => {
89
116
  package: "bdev package",
90
117
  },
91
118
  dependencies: {
92
- "@minecraft/server": "1.16.0",
93
- "@minecraft/server-ui": "1.7.0",
119
+ "@minecraft/server": serverVersion,
120
+ "@minecraft/server-ui": serverUiVersion,
94
121
  },
95
122
  };
96
123
  if (isTs) {
@@ -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)) {
@@ -100,7 +103,7 @@ export function generateManifests(opts) {
100
103
  if (opts.hasScript) {
101
104
  bpDependencies.push({
102
105
  module_name: "@minecraft/server",
103
- version: opts.serverVersion || "1.16.0",
106
+ version: opts.serverVersion || "2.11.0",
104
107
  });
105
108
  }
106
109
  if (opts.hasRp) {
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ export declare function fetchLatestNpmVersion(pkgName: string, fallback?: string): Promise<string>;
2
+ export declare function resolveMinecraftDependencies(packages: string[]): Promise<Record<string, string>>;
@@ -0,0 +1,35 @@
1
+ const DEFAULT_FALLBACKS = {
2
+ "@minecraft/server": "2.11.0",
3
+ "@minecraft/server-ui": "2.3.0",
4
+ "@minecraft/server-admin": "1.0.0-beta",
5
+ "@minecraft/server-gametest": "1.0.0-beta",
6
+ "@minecraft/server-net": "1.0.0-beta",
7
+ };
8
+ export async function fetchLatestNpmVersion(pkgName, fallback) {
9
+ const defaultFallback = fallback || DEFAULT_FALLBACKS[pkgName] || "latest";
10
+ try {
11
+ const res = await fetch(`https://registry.npmjs.org/${pkgName}/latest`, {
12
+ headers: {
13
+ Accept: "application/json",
14
+ },
15
+ signal: AbortSignal.timeout(4000),
16
+ });
17
+ if (res.ok) {
18
+ const data = (await res.json());
19
+ if (data?.version) {
20
+ return data.version;
21
+ }
22
+ }
23
+ }
24
+ catch {
25
+ // Offline or network timeout, return fallback
26
+ }
27
+ return defaultFallback;
28
+ }
29
+ export async function resolveMinecraftDependencies(packages) {
30
+ const entries = await Promise.all(packages.map(async (pkg) => {
31
+ const ver = await fetchLatestNpmVersion(pkg);
32
+ return [pkg, ver];
33
+ }));
34
+ return Object.fromEntries(entries);
35
+ }
@@ -51,8 +51,13 @@ export class BedrockHotReloadServer {
51
51
  socket.on("message", (data) => {
52
52
  try {
53
53
  const res = JSON.parse(data.toString());
54
- if (res?.body?.statusCode && res.body.statusCode !== 0) {
55
- logger.warn(`Minecraft command response: ${res.body.statusMessage || "Error"}`);
54
+ if (res?.header?.messagePurpose === "commandResponse") {
55
+ if (res.body?.statusCode && res.body.statusCode !== 0) {
56
+ logger.warn(`Minecraft command error (${res.body.statusCode}): ${res.body.statusMessage || "Error"}`);
57
+ }
58
+ else if (res.body?.statusMessage) {
59
+ logger.info(`Minecraft: ${res.body.statusMessage}`);
60
+ }
56
61
  }
57
62
  }
58
63
  catch {
@@ -63,7 +68,8 @@ export class BedrockHotReloadServer {
63
68
  this.clients.delete(socket);
64
69
  logger.info("Minecraft disconnected from WebSocket.");
65
70
  });
66
- socket.on("error", () => {
71
+ socket.on("error", (err) => {
72
+ logger.warn(`WebSocket client error: ${err.message}`);
67
73
  this.clients.delete(socket);
68
74
  });
69
75
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mafly/bdev-cli",
3
- "version": "1.0.3",
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",