gdk-version-archiver 1.0.0-alpha
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/LICENSE +21 -0
- package/README.md +41 -0
- package/bin/cli +2 -0
- package/dist/cli.js +1102 -0
- package/package.json +38 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ConMaster
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# GDK Version Archiver
|
|
2
|
+
|
|
3
|
+
**NOTICE: THIS TOOL DOES NOT HELP YOU TO PIRATE THE GAME. YOU MUST OWN A LEGITIMATE COPY OF MINECRAFT TO USE THIS TOOL.**
|
|
4
|
+
|
|
5
|
+
A CLI tool for archiving and managing Minecraft GDK installations on Windows. It captures current installations as mirrors and manages them using tags for easy version switching.
|
|
6
|
+
|
|
7
|
+
- **Archive Versions**: Capture installed Appx packages to local mirrors.
|
|
8
|
+
- **Tagging**: Use tags like `current` or `beta` to manage different versions. Default is `current` tag and is also hardcoded to always be used.
|
|
9
|
+
- **Protected File Handling**: Automatically copies encrypted files via internal instructions.
|
|
10
|
+
- **Concurrent Copying**: Fast archiving using a task concurrency channel.
|
|
11
|
+
|
|
12
|
+
## Setup and Usage
|
|
13
|
+
|
|
14
|
+
**Prerequisites**: Windows OS, [Bun](https://bun.sh/) runtime, and Powershell.
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pnpm install
|
|
18
|
+
npm run build
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Binary name: `gdk-archive-manager`
|
|
22
|
+
|
|
23
|
+
- **Archive**: `bunx gdk-archive-manager archive [package-pattern]`
|
|
24
|
+
- `--tag <name>`: Target tag (default: `current`).
|
|
25
|
+
- `--force`: Overwrite existing mirrors.
|
|
26
|
+
- `--concurrency <n>`: Parallel file copy limit. Default is `10`, recommended is to follow default value.
|
|
27
|
+
- **List**: `bunx gdk-archive-manager list` - Show stored mirrors and tags.
|
|
28
|
+
- **Run**: `bunx gdk-archive-manager run --tag <name>` - Launch a tagged version. Default is `current` tag.
|
|
29
|
+
|
|
30
|
+
Global flags: `--verbose`, `--tag <name>`.
|
|
31
|
+
|
|
32
|
+
## Storage and Mechanics
|
|
33
|
+
|
|
34
|
+
Data is stored in `%APPDATA%\ConMaster.BedrockArchiver\clients\`.
|
|
35
|
+
|
|
36
|
+
- `mirrors/`: Full version file structures.
|
|
37
|
+
- `tags/`: Junctions pointing to specific mirrors.
|
|
38
|
+
|
|
39
|
+
The tool bypasses GDK file protections by invoking a copy instruction within the package context. It then uses symlink junctions to allow external tools to reference a static path while the underlying version is updated.
|
|
40
|
+
|
|
41
|
+
MIT License
|
package/bin/cli
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1102 @@
|
|
|
1
|
+
import { copyFile, mkdir, readFile, realpath, rm, rmdir, stat, symlink } from "node:fs/promises";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { exit } from "node:process";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
|
|
7
|
+
//#region node_modules/.pnpm/con-utils@0.0.2/node_modules/con-utils/dist/format-helper-CE23_M3S.js
|
|
8
|
+
var FormatCode = class FormatCode$1 {
|
|
9
|
+
prefixes;
|
|
10
|
+
suffixes;
|
|
11
|
+
constructor(prefix, suffix) {
|
|
12
|
+
this.prefixes = prefix;
|
|
13
|
+
this.suffixes = suffix;
|
|
14
|
+
}
|
|
15
|
+
static create(format, reset) {
|
|
16
|
+
return new FormatCode$1([format], reset === void 0 ? [] : [reset]);
|
|
17
|
+
}
|
|
18
|
+
static createForegroundRGB(red, green, blue) {
|
|
19
|
+
return new FormatCode$1([
|
|
20
|
+
38,
|
|
21
|
+
2,
|
|
22
|
+
red,
|
|
23
|
+
green,
|
|
24
|
+
blue
|
|
25
|
+
], [39]);
|
|
26
|
+
}
|
|
27
|
+
static createBackgroundRGB(red, green, blue) {
|
|
28
|
+
return new FormatCode$1([
|
|
29
|
+
48,
|
|
30
|
+
2,
|
|
31
|
+
red,
|
|
32
|
+
green,
|
|
33
|
+
blue
|
|
34
|
+
], [49]);
|
|
35
|
+
}
|
|
36
|
+
get prefix() {
|
|
37
|
+
return this.compile(this.prefixes);
|
|
38
|
+
}
|
|
39
|
+
get suffix() {
|
|
40
|
+
return this.compile(this.suffixes);
|
|
41
|
+
}
|
|
42
|
+
compile(format) {
|
|
43
|
+
return `\x1b[${format.join(";")}m`;
|
|
44
|
+
}
|
|
45
|
+
merge(format) {
|
|
46
|
+
pushUnique([...this.prefixes], ...format.prefixes);
|
|
47
|
+
pushUnique([...this.suffixes], ...this.suffixes);
|
|
48
|
+
return new FormatCode$1(this.prefixes, this.suffixes);
|
|
49
|
+
}
|
|
50
|
+
get wrap() {
|
|
51
|
+
return (raw) => `${this.prefix}${raw}${this.suffix}`;
|
|
52
|
+
}
|
|
53
|
+
get format() {
|
|
54
|
+
return (template, ...args) => this.wrap(template.reduce((acc, part, i) => acc + this.wrap(part) + (args[i] ?? ""), ""));
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const DIM$1 = FormatCode.create(2, 22);
|
|
58
|
+
const ITALIC = FormatCode.create(3, 23);
|
|
59
|
+
const UNDERLINE = FormatCode.create(4, 24);
|
|
60
|
+
const INVERSE = FormatCode.create(7, 27);
|
|
61
|
+
const CENSOR = FormatCode.create(8, 28);
|
|
62
|
+
const CANCELED = FormatCode.create(9, 29);
|
|
63
|
+
const SOFT_RESET = [
|
|
64
|
+
21,
|
|
65
|
+
22,
|
|
66
|
+
23,
|
|
67
|
+
24,
|
|
68
|
+
25,
|
|
69
|
+
27,
|
|
70
|
+
28,
|
|
71
|
+
29,
|
|
72
|
+
39,
|
|
73
|
+
49,
|
|
74
|
+
54,
|
|
75
|
+
55,
|
|
76
|
+
65
|
|
77
|
+
].join(";");
|
|
78
|
+
/**
|
|
79
|
+
* Pushes value into array if it's not already present.
|
|
80
|
+
* Returns true if inserted, false if already existed.
|
|
81
|
+
*/
|
|
82
|
+
function pushUnique(arr, ...values) {
|
|
83
|
+
for (const value of values) {
|
|
84
|
+
if (arr.includes(value)) continue;
|
|
85
|
+
arr.push(value);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
var Format = class {
|
|
89
|
+
/**
|
|
90
|
+
* Wraps words in a string to fit within a specified print length.
|
|
91
|
+
* @param text The input string to wrap.
|
|
92
|
+
* @param printLength The maximum length of each line.
|
|
93
|
+
* @returns A generator yielding wrapped lines.
|
|
94
|
+
*/
|
|
95
|
+
static *wrapWords(text, printLength) {
|
|
96
|
+
const words = text.split(/\s+/);
|
|
97
|
+
const currentLineWords = [];
|
|
98
|
+
let currentLength = 0;
|
|
99
|
+
for (const word of words) {
|
|
100
|
+
const wordLength = word.length;
|
|
101
|
+
if (currentLength + wordLength + currentLineWords.length > printLength && currentLineWords.length > 0) {
|
|
102
|
+
yield currentLineWords.join(" ");
|
|
103
|
+
currentLineWords.length = 0;
|
|
104
|
+
currentLength = 0;
|
|
105
|
+
}
|
|
106
|
+
currentLineWords.push(word);
|
|
107
|
+
currentLength += wordLength;
|
|
108
|
+
}
|
|
109
|
+
if (currentLineWords.length > 0) yield currentLineWords.join(" ");
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
//#endregion
|
|
114
|
+
//#region node_modules/.pnpm/con-utils@0.0.2/node_modules/con-utils/dist/con-cli.js
|
|
115
|
+
var ValueTypeValidator = class {
|
|
116
|
+
toString() {
|
|
117
|
+
return this.name;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
const VALID_BOOLEAN_TYPES = new Set([
|
|
121
|
+
"false",
|
|
122
|
+
"true",
|
|
123
|
+
"0",
|
|
124
|
+
"1"
|
|
125
|
+
]);
|
|
126
|
+
var BooleanTypeValidator = class extends ValueTypeValidator {
|
|
127
|
+
name = "bool";
|
|
128
|
+
description = "Boolean Type (true/false)";
|
|
129
|
+
isValid(input) {
|
|
130
|
+
return VALID_BOOLEAN_TYPES.has(input.toLowerCase());
|
|
131
|
+
}
|
|
132
|
+
coerce(input) {
|
|
133
|
+
input = input.toLowerCase();
|
|
134
|
+
return input === "false" || input === "0" ? false : Boolean(input);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
var NumberTypeValidator = class extends ValueTypeValidator {
|
|
138
|
+
name = "number";
|
|
139
|
+
description = "Floating point number type";
|
|
140
|
+
isValid(input) {
|
|
141
|
+
return !isNaN(Number(input));
|
|
142
|
+
}
|
|
143
|
+
coerce(input) {
|
|
144
|
+
return Number(input);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
var StringTypeValidator = class extends ValueTypeValidator {
|
|
148
|
+
name = "string";
|
|
149
|
+
description = "Raw string type";
|
|
150
|
+
isValid(input) {
|
|
151
|
+
return typeof input === "string";
|
|
152
|
+
}
|
|
153
|
+
coerce(input) {
|
|
154
|
+
return input;
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
/**
|
|
158
|
+
* Represents a command-line argument with validation and type coercion.
|
|
159
|
+
* @template T - The type of the argument value after validation
|
|
160
|
+
*/
|
|
161
|
+
var ArgumentLike = class {
|
|
162
|
+
name;
|
|
163
|
+
validator;
|
|
164
|
+
description;
|
|
165
|
+
defaultValue;
|
|
166
|
+
constructor(name, opts) {
|
|
167
|
+
this.name = name.toLowerCase();
|
|
168
|
+
this.validator = opts.validator;
|
|
169
|
+
this.description = opts?.description ?? void 0;
|
|
170
|
+
this.defaultValue = opts.defaultValue;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Determines if this argument is required (has no default value).
|
|
174
|
+
* @returns True if the argument must be provided
|
|
175
|
+
*/
|
|
176
|
+
isRequired() {
|
|
177
|
+
return this.defaultValue === null;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Validates and coerces a string value to the argument's type.
|
|
181
|
+
* @param value - The string value to enforce
|
|
182
|
+
* @returns The coerced value of type T
|
|
183
|
+
* @throws {Error} If the value is invalid or required argument is missing
|
|
184
|
+
*/
|
|
185
|
+
enforce(value) {
|
|
186
|
+
if (value === void 0 || value === null) if (this.defaultValue === null) throw new Error("Following argument is required and can't be omitted! " + this.name);
|
|
187
|
+
else return this.defaultValue;
|
|
188
|
+
if (!this.validator.isValid(value)) throw new Error("Following argument has not valid value! " + this.name + " -> " + value);
|
|
189
|
+
return this.validator.coerce(value);
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Returns a string representation of this argument for help text.
|
|
193
|
+
* @returns Formatted string like `<name:type>` or `[name:type]`
|
|
194
|
+
*/
|
|
195
|
+
toString() {
|
|
196
|
+
const $ = `${this.name}:${this.validator.toString()}`;
|
|
197
|
+
return this.isRequired() ? `<${$}>` : `[${$}]`;
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
/**
|
|
201
|
+
* Represents a named flag that accepts a value.
|
|
202
|
+
* @template T - The type of the flag value
|
|
203
|
+
*/
|
|
204
|
+
var ValueFlag = class extends ArgumentLike {
|
|
205
|
+
long;
|
|
206
|
+
short;
|
|
207
|
+
isValueFlag;
|
|
208
|
+
constructor(name, opts) {
|
|
209
|
+
super(name, opts);
|
|
210
|
+
this.long = opts.long?.toLowerCase();
|
|
211
|
+
this.short = opts.short?.toLowerCase();
|
|
212
|
+
this.isValueFlag = true;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Determines if this flag accepts a value.
|
|
216
|
+
* @returns True if this flag is value-based
|
|
217
|
+
*/
|
|
218
|
+
isValueBased() {
|
|
219
|
+
return this.isValueFlag;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Returns a string representation of this flag for help text.
|
|
223
|
+
* @returns Formatted string like `--long, -s`
|
|
224
|
+
*/
|
|
225
|
+
toString() {
|
|
226
|
+
return [this.long && `--${this.long}`, this.short && `-${this.short}`].filter(Boolean).join(", ");
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
/**
|
|
230
|
+
* Represents a boolean flag (presence indicator).
|
|
231
|
+
*/
|
|
232
|
+
var Flag = class extends ValueFlag {
|
|
233
|
+
constructor(name, opts) {
|
|
234
|
+
const { description: description$1, long, short } = opts ?? {};
|
|
235
|
+
super(name, {
|
|
236
|
+
validator: new BooleanTypeValidator(),
|
|
237
|
+
defaultValue: false,
|
|
238
|
+
description: description$1,
|
|
239
|
+
long,
|
|
240
|
+
short
|
|
241
|
+
});
|
|
242
|
+
this.isValueFlag = false;
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
/**
|
|
246
|
+
* A collection of flags with lookup capabilities by name, long form, or short form.
|
|
247
|
+
*/
|
|
248
|
+
var FlagsGroup = class {
|
|
249
|
+
flags = /* @__PURE__ */ new Set();
|
|
250
|
+
map = /* @__PURE__ */ new Map();
|
|
251
|
+
base = null;
|
|
252
|
+
constructor(base) {
|
|
253
|
+
this.base = base ?? null;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Checks if a flag exists in this group or inherited from base groups.
|
|
257
|
+
* @param valueFlag - The flag to check for
|
|
258
|
+
* @returns True if the flag is registered
|
|
259
|
+
*/
|
|
260
|
+
hasFlag(valueFlag) {
|
|
261
|
+
return this.hasOwnFlag(valueFlag) || (this.base?.hasFlag(valueFlag) ?? false);
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Checks if a flag exists directly in this group (not inherited).
|
|
265
|
+
* @param valueFlag - The flag to check for
|
|
266
|
+
* @returns True if the flag is registered in this group
|
|
267
|
+
*/
|
|
268
|
+
hasOwnFlag(valueFlag) {
|
|
269
|
+
return this.flags.has(valueFlag);
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Adds flags and registers their searchable keys.
|
|
273
|
+
* @param flags - The flags to add
|
|
274
|
+
* @returns This group for chaining
|
|
275
|
+
*/
|
|
276
|
+
add(...flags) {
|
|
277
|
+
for (const flag of flags) {
|
|
278
|
+
this.flags.add(flag);
|
|
279
|
+
this.setGeneralInternal(flag.name, flag);
|
|
280
|
+
if (flag.long) this.setLongInternal(flag.long, flag);
|
|
281
|
+
if (flag.short) this.setShortInternal(flag.short, flag);
|
|
282
|
+
}
|
|
283
|
+
return this;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Adds a short alias for an existing flag.
|
|
287
|
+
* @param flag - The flag to add an alias for
|
|
288
|
+
* @param short - The short form alias
|
|
289
|
+
* @returns This group for chaining
|
|
290
|
+
*/
|
|
291
|
+
addShortAlias(flag, short) {
|
|
292
|
+
this.flags.add(flag);
|
|
293
|
+
this.setGeneralInternal(flag.name, flag);
|
|
294
|
+
this.setShortInternal(short, flag);
|
|
295
|
+
return this;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Adds a long alias for an existing flag.
|
|
299
|
+
* @param flag - The flag to add an alias for
|
|
300
|
+
* @param long - The long form alias
|
|
301
|
+
* @returns This group for chaining
|
|
302
|
+
*/
|
|
303
|
+
addLongAlias(flag, long) {
|
|
304
|
+
this.flags.add(flag);
|
|
305
|
+
this.setGeneralInternal(flag.name, flag);
|
|
306
|
+
this.setLongInternal(long, flag);
|
|
307
|
+
return this;
|
|
308
|
+
}
|
|
309
|
+
setLongInternal(key, flag) {
|
|
310
|
+
this.map.set(`long:${key}`, flag);
|
|
311
|
+
}
|
|
312
|
+
setShortInternal(key, flag) {
|
|
313
|
+
this.map.set(`short:${key}`, flag);
|
|
314
|
+
}
|
|
315
|
+
setGeneralInternal(key, flag) {
|
|
316
|
+
this.map.set(`flag:${key}`, flag);
|
|
317
|
+
}
|
|
318
|
+
getLong(key) {
|
|
319
|
+
return this.map.get(`long:${key}`) ?? this.base?.getLong(key) ?? null;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Retrieves a flag by its short form name.
|
|
323
|
+
* @param key - The short form name
|
|
324
|
+
* @returns The flag or null if not found
|
|
325
|
+
*/
|
|
326
|
+
getShort(key) {
|
|
327
|
+
return this.map.get(`short:${key}`) ?? this.base?.getShort(key) ?? null;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Retrieves a flag by its primary name.
|
|
331
|
+
* @param name - The flag name
|
|
332
|
+
* @returns The flag or null if not found
|
|
333
|
+
*/
|
|
334
|
+
getByName(name) {
|
|
335
|
+
return this.map.get(`flag:${name}`) ?? this.base?.getByName(name) ?? null;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Gets all flags directly registered in this group.
|
|
339
|
+
* @returns A read-only set of all flags
|
|
340
|
+
*/
|
|
341
|
+
getAll() {
|
|
342
|
+
return this.flags;
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
const dim$1 = DIM$1.wrap;
|
|
346
|
+
var CommandHelpStringBuilder = class {
|
|
347
|
+
static *buildHelp(command, preferredWidth$1) {
|
|
348
|
+
yield dim$1(` Usage of ${command.syntax()}`);
|
|
349
|
+
yield "";
|
|
350
|
+
yield dim$1(" " + "-".repeat(preferredWidth$1));
|
|
351
|
+
yield* Format.wrapWords(command.description ?? "", preferredWidth$1).map((e) => dim$1(" ") + e);
|
|
352
|
+
yield dim$1(" " + "-".repeat(preferredWidth$1));
|
|
353
|
+
yield "";
|
|
354
|
+
yield* this.buildFlags(command);
|
|
355
|
+
yield "";
|
|
356
|
+
}
|
|
357
|
+
static *buildFlags(command) {
|
|
358
|
+
yield dim$1("Flags:");
|
|
359
|
+
yield* command.flags.getAll().values().map((e) => " " + e.toString().padEnd(20) + " " + dim$1(e.description ?? ""));
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
const dim = DIM$1.wrap;
|
|
363
|
+
var Command = class {
|
|
364
|
+
parent;
|
|
365
|
+
uniqueHelpFlag;
|
|
366
|
+
name;
|
|
367
|
+
flags;
|
|
368
|
+
description;
|
|
369
|
+
onFlags;
|
|
370
|
+
constructor(name, description$1, parent = null) {
|
|
371
|
+
this.name = name.toLowerCase();
|
|
372
|
+
this.description = description$1 ?? "";
|
|
373
|
+
this.flags = new FlagsGroup(parent?.flags ?? null);
|
|
374
|
+
this.parent = parent;
|
|
375
|
+
this.uniqueHelpFlag = new Flag("help", {
|
|
376
|
+
description: "Show's help message",
|
|
377
|
+
short: "?",
|
|
378
|
+
long: "help"
|
|
379
|
+
});
|
|
380
|
+
this.flags.add(this.uniqueHelpFlag);
|
|
381
|
+
this.flags.addShortAlias(this.uniqueHelpFlag, "h");
|
|
382
|
+
}
|
|
383
|
+
getParentCommand() {
|
|
384
|
+
return this.parent ?? null;
|
|
385
|
+
}
|
|
386
|
+
*getHelp() {
|
|
387
|
+
yield* CommandHelpStringBuilder.buildHelp(this, preferredWidth);
|
|
388
|
+
}
|
|
389
|
+
*getFlags() {
|
|
390
|
+
yield dim("Flags:");
|
|
391
|
+
yield* this.flags.getAll().values().map((e) => " " + e.toString().padEnd(20) + " " + dim(e.description ?? ""));
|
|
392
|
+
}
|
|
393
|
+
getFullPathName() {
|
|
394
|
+
return `${this.parent ? this.parent.getFullPathName() + " " : ""}${this.name}`;
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
var CommandAction = class CommandAction$1 extends Command {
|
|
398
|
+
argumentsTypes;
|
|
399
|
+
action = null;
|
|
400
|
+
constructor(name, opts) {
|
|
401
|
+
super(name, opts.description, opts.parent);
|
|
402
|
+
this.argumentsTypes = opts.argTypes;
|
|
403
|
+
}
|
|
404
|
+
*getHelp() {
|
|
405
|
+
yield* super.getHelp();
|
|
406
|
+
if (!this.argumentsTypes.length) return;
|
|
407
|
+
yield dim(" Arguments:");
|
|
408
|
+
yield* this.argumentsTypes.map((e) => ` ${e.toString().padEnd(20)} ${dim(e.description ?? "")}`);
|
|
409
|
+
yield "";
|
|
410
|
+
}
|
|
411
|
+
static create(name, description$1, parent, args) {
|
|
412
|
+
return new CommandAction$1(name, {
|
|
413
|
+
argTypes: args,
|
|
414
|
+
description: description$1,
|
|
415
|
+
parent
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
syntax() {
|
|
419
|
+
return `${this.getFullPathName()} ${this.argumentsTypes.map((e) => e.isRequired() ? `<${e.name}:${e.validator.name}>` : `[${e.name}:${e.validator.name}]`).join(" ")}`;
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
var GroupCommand = class GroupCommand$1 extends Command {
|
|
423
|
+
action = null;
|
|
424
|
+
subcommands = /* @__PURE__ */ new Map();
|
|
425
|
+
static create(name, description$1, parent) {
|
|
426
|
+
return new GroupCommand$1(name, description$1, parent);
|
|
427
|
+
}
|
|
428
|
+
createGroup(name, description$1) {
|
|
429
|
+
const cmd = GroupCommand$1.create(name, description$1, this);
|
|
430
|
+
this.subcommands.set(name, cmd);
|
|
431
|
+
return cmd;
|
|
432
|
+
}
|
|
433
|
+
createAction(name, description$1, args) {
|
|
434
|
+
const cmd = CommandAction.create(name, description$1, this, args);
|
|
435
|
+
this.subcommands.set(name, cmd);
|
|
436
|
+
return cmd;
|
|
437
|
+
}
|
|
438
|
+
syntax() {
|
|
439
|
+
return `${this.getFullPathName()} <${this.subcommands.keys().toArray().join("|")}>`;
|
|
440
|
+
}
|
|
441
|
+
*getHelp() {
|
|
442
|
+
yield* super.getHelp();
|
|
443
|
+
yield dim(" SubCommands:");
|
|
444
|
+
let p = preferredWidth - 25;
|
|
445
|
+
yield* this.subcommands.values().map((e) => ` ${e.name.padEnd(20)} ${dim((e.description?.length ?? 0) > p ? (e.description?.substring(0, p) ?? "") + "..." : e.description ?? "")}`);
|
|
446
|
+
yield "";
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
let preferredWidth = 75;
|
|
450
|
+
/**
|
|
451
|
+
* Represents the result of parsing a command-line input.
|
|
452
|
+
* Contains the matched command, parsed arguments, and resolved flags.
|
|
453
|
+
*/
|
|
454
|
+
var ParseResult = class {
|
|
455
|
+
constructor(command, args, flags) {
|
|
456
|
+
this.command = command;
|
|
457
|
+
this.args = args;
|
|
458
|
+
this.flags = flags;
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Gets the value of a flag, falling back to its default if not provided.
|
|
462
|
+
* @template T - The type of the flag value
|
|
463
|
+
* @param flag - The flag to retrieve
|
|
464
|
+
* @returns The flag value or its default value
|
|
465
|
+
*/
|
|
466
|
+
getValue(flag) {
|
|
467
|
+
return this.flags.get(flag) ?? flag.defaultValue;
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Gets the executable function for this command if it's an action command.
|
|
471
|
+
* @returns The bound action function or null if this is a group command
|
|
472
|
+
*/
|
|
473
|
+
getExecutable() {
|
|
474
|
+
if ("action" in this.command && typeof this.command["action"] === "function") return this.command.action.bind(null, this, ...this.args);
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
/**
|
|
479
|
+
* Error thrown during command-line parsing.
|
|
480
|
+
* Contains context about where and why parsing failed.
|
|
481
|
+
*/
|
|
482
|
+
var ParserError = class extends Error {
|
|
483
|
+
command;
|
|
484
|
+
argv;
|
|
485
|
+
index;
|
|
486
|
+
flags;
|
|
487
|
+
constructor(command, message, argv, index, flags) {
|
|
488
|
+
super(message);
|
|
489
|
+
this.command = command;
|
|
490
|
+
this.name = new.target.name;
|
|
491
|
+
this.argv = argv;
|
|
492
|
+
this.index = index;
|
|
493
|
+
this.flags = flags ?? null;
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
/**
|
|
497
|
+
* Parses command-line arguments into commands, flags, and positional arguments.
|
|
498
|
+
*/
|
|
499
|
+
var CommandsParser = class CommandsParser$1 {
|
|
500
|
+
argv;
|
|
501
|
+
index;
|
|
502
|
+
lastError = null;
|
|
503
|
+
constructor(argv, index) {
|
|
504
|
+
this.argv = argv;
|
|
505
|
+
this.index = index;
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Parses the provided command against the command-line arguments.
|
|
509
|
+
* @param command - The command to parse
|
|
510
|
+
* @returns The parse result containing the matched command and parsed values
|
|
511
|
+
* @throws {ParserError} If parsing fails
|
|
512
|
+
*/
|
|
513
|
+
parse(command) {
|
|
514
|
+
if (command instanceof GroupCommand) return this.parseGroupCommand(command);
|
|
515
|
+
else if (command instanceof CommandAction) return this.parseActionCommand(command);
|
|
516
|
+
else throw new ParserError(command, "Unsupported command type", this.argv, this.index);
|
|
517
|
+
}
|
|
518
|
+
parseGroupCommand(command) {
|
|
519
|
+
const RESULT = new ParseResult(command, [], /* @__PURE__ */ new Map());
|
|
520
|
+
let argument = this.getArgument();
|
|
521
|
+
if (!argument) {
|
|
522
|
+
if (!command.action) throw new ParserError(command, `Expected subcommand name, received none: ${command.getFullPathName()}`, this.argv, this.index);
|
|
523
|
+
return new ParseResult(command, [], /* @__PURE__ */ new Map());
|
|
524
|
+
}
|
|
525
|
+
if (argument.startsWith("-")) {
|
|
526
|
+
while (argument = this.getArgument()) {
|
|
527
|
+
const flag = CommandsParser$1.parseFlag(argument);
|
|
528
|
+
if (!flag) break;
|
|
529
|
+
const result = this.resolveFlag(command.flags, flag, this.getArgument(1));
|
|
530
|
+
if (!result) if (this.getLastError()) throw new ParserError(command, this.getLastError(), this.argv, this.index, new Set(RESULT.flags.keys()));
|
|
531
|
+
else throw new ParserError(command, "Failed to resolve tag with name: " + flag.name, this.argv, this.index, new Set(RESULT.flags.keys()));
|
|
532
|
+
const { flag: FLAG_TYPE, nextValueUsed, value } = result;
|
|
533
|
+
this.index++;
|
|
534
|
+
if (nextValueUsed) this.index++;
|
|
535
|
+
RESULT.flags.set(FLAG_TYPE, value);
|
|
536
|
+
}
|
|
537
|
+
if (!argument) return RESULT;
|
|
538
|
+
}
|
|
539
|
+
const sub = argument.toLowerCase();
|
|
540
|
+
const cmd = command.subcommands.get(sub);
|
|
541
|
+
if (!cmd) throw new ParserError(command, `Unknown subcommand: ${command.getFullPathName()} >>${sub}<<`, this.argv, this.index, new Set(RESULT.flags.keys()));
|
|
542
|
+
this.index++;
|
|
543
|
+
try {
|
|
544
|
+
const HRESULT = this.parse(cmd);
|
|
545
|
+
for (const flag of RESULT.flags.keys()) if (!HRESULT.flags.has(flag)) HRESULT.flags.set(flag, RESULT.flags.get(flag));
|
|
546
|
+
return HRESULT;
|
|
547
|
+
} catch (error) {
|
|
548
|
+
if (error instanceof ParserError) {
|
|
549
|
+
const flags = error.flags;
|
|
550
|
+
if (flags) RESULT.flags.keys().forEach((_) => flags.add(_));
|
|
551
|
+
else error.flags = new Set(RESULT.flags.keys());
|
|
552
|
+
}
|
|
553
|
+
throw error;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
parseActionCommand(command) {
|
|
557
|
+
const positionals = [];
|
|
558
|
+
let argument;
|
|
559
|
+
const FLAGS_MAP = /* @__PURE__ */ new Map();
|
|
560
|
+
while (argument = this.getArgument()) {
|
|
561
|
+
const flag = CommandsParser$1.parseFlag(argument);
|
|
562
|
+
this.index++;
|
|
563
|
+
if (!flag) {
|
|
564
|
+
positionals.push(argument);
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
const result = this.resolveFlag(command.flags, flag, this.getArgument());
|
|
568
|
+
if (!result) if (this.getLastError()) throw new ParserError(command, this.getLastError(), this.argv, this.index, new Set(FLAGS_MAP.keys()));
|
|
569
|
+
else {
|
|
570
|
+
positionals.push(argument);
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
const { flag: FLAG_TYPE, nextValueUsed, value } = result;
|
|
574
|
+
if (nextValueUsed) this.index++;
|
|
575
|
+
FLAGS_MAP.set(FLAG_TYPE, value);
|
|
576
|
+
}
|
|
577
|
+
const finalArgs = [];
|
|
578
|
+
for (let idx = 0; idx < command.argumentsTypes.length; idx++) {
|
|
579
|
+
const def = command.argumentsTypes[idx];
|
|
580
|
+
const posVal = positionals[idx];
|
|
581
|
+
if (typeof posVal === "undefined") {
|
|
582
|
+
if (def.isRequired()) throw new ParserError(command, `Missing required argument: ${command.getFullPathName()} ... ${def.toString()}`, this.argv, this.index, new Set(FLAGS_MAP.keys()));
|
|
583
|
+
finalArgs.push(def.defaultValue);
|
|
584
|
+
} else {
|
|
585
|
+
if (!def.validator.isValid(posVal)) throw new ParserError(command, `Invalid value: ${posVal}`, this.argv, this.index, new Set(FLAGS_MAP.keys()));
|
|
586
|
+
finalArgs.push(def.enforce(posVal));
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
for (let j = command.argumentsTypes.length; j < positionals.length; j++) finalArgs.push(positionals[j]);
|
|
590
|
+
return new ParseResult(command, finalArgs, FLAGS_MAP);
|
|
591
|
+
}
|
|
592
|
+
resolveFlag(group, flag, next) {
|
|
593
|
+
const FLAG_TYPE = flag.isLong ? group.getLong(flag.name) : group.getShort(flag.name);
|
|
594
|
+
if (!FLAG_TYPE) return this.setLastError(null);
|
|
595
|
+
let nextValueUsed = false;
|
|
596
|
+
if (FLAG_TYPE.isValueBased()) {
|
|
597
|
+
let { value } = flag;
|
|
598
|
+
if (value === null) {
|
|
599
|
+
nextValueUsed = true;
|
|
600
|
+
value = next;
|
|
601
|
+
}
|
|
602
|
+
if (value === null) {
|
|
603
|
+
if (FLAG_TYPE.defaultValue) return {
|
|
604
|
+
flag: FLAG_TYPE,
|
|
605
|
+
value: null,
|
|
606
|
+
nextValueUsed
|
|
607
|
+
};
|
|
608
|
+
return this.setLastError("Flag with no default value, requires value to be set explicitly, flag: " + flag.name);
|
|
609
|
+
}
|
|
610
|
+
if (!FLAG_TYPE.validator.isValid(value)) return this.setLastError("Incorrect type passed as value for flag: " + flag.name + " value: " + value);
|
|
611
|
+
return {
|
|
612
|
+
flag: FLAG_TYPE,
|
|
613
|
+
value: FLAG_TYPE.validator.coerce(value),
|
|
614
|
+
nextValueUsed
|
|
615
|
+
};
|
|
616
|
+
} else if (flag.value) return this.setLastError("Syntax error, this flag doesn't supports values: " + flag.name + " value: " + flag.value);
|
|
617
|
+
return {
|
|
618
|
+
flag: FLAG_TYPE,
|
|
619
|
+
value: true,
|
|
620
|
+
nextValueUsed
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
getLastError() {
|
|
624
|
+
return this.lastError;
|
|
625
|
+
}
|
|
626
|
+
setLastError(error) {
|
|
627
|
+
this.lastError = error;
|
|
628
|
+
return null;
|
|
629
|
+
}
|
|
630
|
+
getArgument(relative = 0) {
|
|
631
|
+
return this.argv[this.index + (relative ?? 0)] ?? null;
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Parses a flag string into its components (name, type, and optional value).
|
|
635
|
+
* @param argument - The argument string to parse (e.g., "-f", "--flag=value")
|
|
636
|
+
* @returns The parsed flag result or null if the argument is not a flag
|
|
637
|
+
*/
|
|
638
|
+
static parseFlag(argument) {
|
|
639
|
+
if (argument.length < 2 || argument[0] !== "-") return null;
|
|
640
|
+
const isLongBit = argument[1] === "-", hasValueIndex = argument.indexOf("="), hasValueBit = hasValueIndex !== -1;
|
|
641
|
+
let name, value = hasValueBit ? argument.substring(hasValueIndex + 1) : null;
|
|
642
|
+
if (isLongBit) name = argument.substring(2, hasValueBit ? hasValueIndex : void 0);
|
|
643
|
+
else {
|
|
644
|
+
name = argument[1];
|
|
645
|
+
if (!value && argument.length > 2) value = argument.substring(2);
|
|
646
|
+
}
|
|
647
|
+
return {
|
|
648
|
+
name: name.toLowerCase(),
|
|
649
|
+
isLong: isLongBit,
|
|
650
|
+
value
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
};
|
|
654
|
+
/**
|
|
655
|
+
* Main entry point for parsing and executing command-line arguments.
|
|
656
|
+
* Handles parsing, flag resolution, and command execution with error handling.
|
|
657
|
+
*/
|
|
658
|
+
var CommandLine = class {
|
|
659
|
+
constructor() {}
|
|
660
|
+
/**
|
|
661
|
+
* Parses command-line arguments and executes the appropriate command.
|
|
662
|
+
* @param argv - The command-line arguments to parse (typically process.argv)
|
|
663
|
+
* @param command - The root command to parse against
|
|
664
|
+
* @throws {ParserError} If parsing fails or validation errors occur
|
|
665
|
+
*/
|
|
666
|
+
static async run(argv, command) {
|
|
667
|
+
const parser = new CommandsParser(argv, 2);
|
|
668
|
+
try {
|
|
669
|
+
const result = parser.parse(command);
|
|
670
|
+
if (result.flags.has(result.command.uniqueHelpFlag)) return void console.info(Array.from(result.command.getHelp()).join("\r\n"));
|
|
671
|
+
for (const flag of result.flags.keys()) for (let command$1 = result.command; command$1; command$1 = command$1.getParentCommand()) if (command$1.flags.hasOwnFlag(flag)) command$1.onFlags?.(result);
|
|
672
|
+
result.getExecutable()?.();
|
|
673
|
+
return;
|
|
674
|
+
} catch (err) {
|
|
675
|
+
if (err instanceof ParserError) {
|
|
676
|
+
if (!err.flags?.has(err.command.uniqueHelpFlag)) console.error(FormatCode.create(31, 39).wrap("->" + err.message));
|
|
677
|
+
console.error("\n", Array.from(err.command.getHelp()).join("\r\n"));
|
|
678
|
+
} else throw err;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Creates a new group command for organizing subcommands.
|
|
683
|
+
* @param name - The name of the command group
|
|
684
|
+
* @param description - A description of what this command group does
|
|
685
|
+
* @returns A new GroupCommand instance
|
|
686
|
+
*/
|
|
687
|
+
static createGroup(name, description$1) {
|
|
688
|
+
return GroupCommand.create(name, description$1, null);
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Creates a new action command that executes a handler function.
|
|
692
|
+
* @template T - The tuple type of arguments the action accepts
|
|
693
|
+
* @param name - The name of the command
|
|
694
|
+
* @param description - A description of what this command does
|
|
695
|
+
* @param args - The argument definitions for this command
|
|
696
|
+
* @returns A new CommandAction instance
|
|
697
|
+
*/
|
|
698
|
+
static createAction(name, description$1, args) {
|
|
699
|
+
return CommandAction.create(name, description$1, null, args);
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
|
|
703
|
+
//#endregion
|
|
704
|
+
//#region package.json
|
|
705
|
+
var description = "";
|
|
706
|
+
|
|
707
|
+
//#endregion
|
|
708
|
+
//#region node_modules/.pnpm/con-utils@0.0.2/node_modules/con-utils/dist/general.js
|
|
709
|
+
/**
|
|
710
|
+
* Manages concurrent execution of tasks with a maximum concurrency limit.
|
|
711
|
+
*/
|
|
712
|
+
var TaskConcurrencyChannel = class {
|
|
713
|
+
/**
|
|
714
|
+
* Set of promises currently being tracked.
|
|
715
|
+
*/
|
|
716
|
+
stack = /* @__PURE__ */ new Set();
|
|
717
|
+
/**
|
|
718
|
+
* Maximum number of concurrent tasks allowed.
|
|
719
|
+
*/
|
|
720
|
+
concurrency;
|
|
721
|
+
/**
|
|
722
|
+
* Create a new TaskConcurrencyChannel.
|
|
723
|
+
* @param maxConcurrency The maximum number of tasks to run concurrently.
|
|
724
|
+
*/
|
|
725
|
+
constructor(maxConcurrency) {
|
|
726
|
+
this.concurrency = maxConcurrency;
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Add a task to the queue. Waits if concurrency limit is reached.
|
|
730
|
+
* @param task The task to execute.
|
|
731
|
+
*/
|
|
732
|
+
async push(task) {
|
|
733
|
+
const promise = Promise.resolve(task).then((_) => void this.stack.delete(promise), (_) => void 0);
|
|
734
|
+
this.stack.add(promise);
|
|
735
|
+
if (this.stack.size >= this.concurrency) await Promise.any(this.stack);
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Get a promise that resolves when all tasks complete.
|
|
739
|
+
* @returns A promise that resolves when the stack is empty.
|
|
740
|
+
*/
|
|
741
|
+
getAwaiter() {
|
|
742
|
+
return Promise.all(this.stack).then((_) => void 0);
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
|
|
746
|
+
//#endregion
|
|
747
|
+
//#region src/logger.ts
|
|
748
|
+
let LOG_ENABLED = false;
|
|
749
|
+
const SET_VERBOSE = (value) => LOG_ENABLED = value;
|
|
750
|
+
const rgb = FormatCode.createForegroundRGB;
|
|
751
|
+
const INFO_CODE = rgb(80, 175, 199);
|
|
752
|
+
const WARN_CODE = rgb(199, 133, 80);
|
|
753
|
+
const E = rgb(199, 80, 96).wrap, I = INFO_CODE.wrap, W = WARN_CODE.wrap;
|
|
754
|
+
const ACCENT = FormatCode.create(92, 39).wrap;
|
|
755
|
+
const DIM = DIM$1.wrap;
|
|
756
|
+
const OUTPUT = (raw) => process.stderr.write(raw);
|
|
757
|
+
const LOG = (string) => void (LOG_ENABLED ? OUTPUT(DIM(`| ` + string)) : null);
|
|
758
|
+
const INFO = (string) => void OUTPUT(I(`| ` + string));
|
|
759
|
+
const WARN = (string) => void OUTPUT(W(`| ` + string));
|
|
760
|
+
const ERROR = (string) => void OUTPUT(E(`| ` + string));
|
|
761
|
+
|
|
762
|
+
//#endregion
|
|
763
|
+
//#region src/constants.ts
|
|
764
|
+
const { APPDATA, SAVE_DATA_PATH } = import.meta.env;
|
|
765
|
+
if (!(SAVE_DATA_PATH || APPDATA)) {
|
|
766
|
+
console.error("Couldn't find path to store the mirrors");
|
|
767
|
+
exit(-1);
|
|
768
|
+
}
|
|
769
|
+
const DEFAULT_APPDATA_FOLDER_NAME = "ConMaster.BedrockArchiver";
|
|
770
|
+
const PREFERRED_PATH = resolve(SAVE_DATA_PATH ?? join(APPDATA ?? ".", DEFAULT_APPDATA_FOLDER_NAME, "clients"));
|
|
771
|
+
const MIRRORS_FOLDER_PATH = join(PREFERRED_PATH, "mirrors");
|
|
772
|
+
const TAGS_FOLDER_PATH = join(PREFERRED_PATH, "tags");
|
|
773
|
+
|
|
774
|
+
//#endregion
|
|
775
|
+
//#region src/cli/root.ts
|
|
776
|
+
const ROOT = CommandLine.createGroup(">.", description);
|
|
777
|
+
const VERBOSE = new Flag("verbose", {
|
|
778
|
+
description: "Shows LOG messages",
|
|
779
|
+
long: "verbose"
|
|
780
|
+
});
|
|
781
|
+
const TAG = new ValueFlag("tag", {
|
|
782
|
+
defaultValue: "current",
|
|
783
|
+
description: "Environment Tag under this instance operates, archiving performs tag redirection of this name",
|
|
784
|
+
validator: new StringTypeValidator(),
|
|
785
|
+
long: "tag"
|
|
786
|
+
});
|
|
787
|
+
ROOT.flags.add(VERBOSE, TAG);
|
|
788
|
+
ROOT.onFlags = (options) => {
|
|
789
|
+
if (options.getValue(VERBOSE)) SET_VERBOSE(true);
|
|
790
|
+
};
|
|
791
|
+
const ENVIRONMENT_INFO = (options) => {
|
|
792
|
+
LOG(`-----------------------\n`);
|
|
793
|
+
LOG(`DATA: ${ACCENT(PREFERRED_PATH)}\n`);
|
|
794
|
+
LOG(`TAG: ${ACCENT(options.getValue(TAG))} ${options.getValue(TAG) === TAG.defaultValue ? "(default)" : ""}\n`);
|
|
795
|
+
LOG(`-----------------------\n`);
|
|
796
|
+
};
|
|
797
|
+
|
|
798
|
+
//#endregion
|
|
799
|
+
//#region src/cli/instruction.ts
|
|
800
|
+
const INSTRUCTION = ROOT.createAction("instruction", "Internal use", [new ArgumentLike("data", {
|
|
801
|
+
defaultValue: null,
|
|
802
|
+
validator: new StringTypeValidator(),
|
|
803
|
+
description: "BASE64 Encoded data"
|
|
804
|
+
})]);
|
|
805
|
+
INSTRUCTION.action = async (_, base64) => {
|
|
806
|
+
let data;
|
|
807
|
+
try {
|
|
808
|
+
LOG(`Trying to transform BASE64 to string\n`);
|
|
809
|
+
const RAW = atob(base64);
|
|
810
|
+
LOG(`Parsing as JSON\n`);
|
|
811
|
+
data = JSON.parse(RAW);
|
|
812
|
+
LOG(`Checking data validity\n`);
|
|
813
|
+
if (!Array.isArray(data)) return void ERROR("INVALID INSTRUCTION FORMAT");
|
|
814
|
+
if (typeof data[0] !== "number" || !(data[0] in InstructionType)) return void ERROR("INVALID INSTRUCTION FORMAT");
|
|
815
|
+
} catch {
|
|
816
|
+
ERROR("INVALID INSTRUCTION FORMAT");
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
switch (data[0]) {
|
|
820
|
+
case InstructionType.Copy:
|
|
821
|
+
if (!await copyFile(data[1], data[2]).then((_$1) => true, (_$1) => false)) {
|
|
822
|
+
setTimeout(() => null, 5e3);
|
|
823
|
+
ERROR("FAILED TO COPY THIS ITEM: " + data[1]);
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
break;
|
|
827
|
+
default:
|
|
828
|
+
ERROR("Unknown instruction");
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
};
|
|
832
|
+
let InstructionType = /* @__PURE__ */ function(InstructionType$1) {
|
|
833
|
+
InstructionType$1[InstructionType$1["Copy"] = 1] = "Copy";
|
|
834
|
+
return InstructionType$1;
|
|
835
|
+
}({});
|
|
836
|
+
function createCommand(...args) {
|
|
837
|
+
const [EXECUTABLE, SELF_CLI] = Bun.argv;
|
|
838
|
+
return [
|
|
839
|
+
EXECUTABLE,
|
|
840
|
+
SELF_CLI,
|
|
841
|
+
INSTRUCTION.name,
|
|
842
|
+
btoa(JSON.stringify(args))
|
|
843
|
+
];
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
//#endregion
|
|
847
|
+
//#region src/pwsh-helper.ts
|
|
848
|
+
var PowershellHelper = class PowershellHelper {
|
|
849
|
+
static POWERSHELL_EXECUTABLE = "powershell";
|
|
850
|
+
static async GetAppxPackage(packageNameLike) {
|
|
851
|
+
LOG(`Requesting Packages\n`);
|
|
852
|
+
let { output, exitCode } = await PowershellHelper.runRawCommand(`((Get-AppxPackage -Name ${packageNameLike}) | ForEach-Object { "$($_.PackageFamilyName);$($_.Version);$($_.InstallLocation)" })`);
|
|
853
|
+
if (exitCode != 0) return null;
|
|
854
|
+
LOG(`Parsing and Resoling Installation Paths\n`);
|
|
855
|
+
return output?.trim().split("\n").map((e) => {
|
|
856
|
+
const [PackageFamilyName, Version, InstallationPath] = e.trim().split(";");
|
|
857
|
+
return {
|
|
858
|
+
PackageFamilyName,
|
|
859
|
+
Version,
|
|
860
|
+
InstallationPath
|
|
861
|
+
};
|
|
862
|
+
}).filter((e) => e?.PackageFamilyName) ?? null;
|
|
863
|
+
}
|
|
864
|
+
static async runRawCommand(command) {
|
|
865
|
+
LOG(`Spawning PWSH command\n`);
|
|
866
|
+
const process$1 = Bun.spawn({
|
|
867
|
+
cmd: [
|
|
868
|
+
this.POWERSHELL_EXECUTABLE,
|
|
869
|
+
"-NoProfile",
|
|
870
|
+
"-NonInteractive",
|
|
871
|
+
"-Command",
|
|
872
|
+
command
|
|
873
|
+
],
|
|
874
|
+
stdout: "pipe",
|
|
875
|
+
stderr: "pipe",
|
|
876
|
+
stdin: "ignore"
|
|
877
|
+
});
|
|
878
|
+
LOG(`Waiting for exit\n`);
|
|
879
|
+
const stdout = await new Response(process$1.stdout).text();
|
|
880
|
+
const stderr = await new Response(process$1.stderr).text();
|
|
881
|
+
const code = await process$1.exited;
|
|
882
|
+
LOG(`PWSH Process exited with code: ${code}\n`);
|
|
883
|
+
return {
|
|
884
|
+
exitCode: code,
|
|
885
|
+
output: code === 0 ? stdout : stderr.length > 0 ? stderr : null
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
static async InvokeCommandInDesktopPackage(packageFamilyName, appId, executable, args) {
|
|
889
|
+
let cmd = `Invoke-CommandInDesktopPackage -PackageFamilyName ${packageFamilyName} -AppId ${appId} -Command ${JSON.stringify(executable)}`;
|
|
890
|
+
if (args.length > 0) cmd += ` -Args '${args.map((_) => `"${_}"`).join(" ")}'`;
|
|
891
|
+
await this.runRawCommand(cmd);
|
|
892
|
+
}
|
|
893
|
+
};
|
|
894
|
+
|
|
895
|
+
//#endregion
|
|
896
|
+
//#region src/cli/archive.ts
|
|
897
|
+
const RECURSIVE_GLOB = new Bun.Glob("**/*");
|
|
898
|
+
const ARCHIVE = ROOT.createAction("archive", "Archive current version", [new ArgumentLike("package-name", {
|
|
899
|
+
defaultValue: "*minecraftuwp*",
|
|
900
|
+
validator: new StringTypeValidator(),
|
|
901
|
+
description: "Minecraft GDK Installation Package Name Pattern (*minecraftuwp*)"
|
|
902
|
+
})]);
|
|
903
|
+
const FORCE_TAG = new Flag("force", {
|
|
904
|
+
long: "force",
|
|
905
|
+
description: "Forces to overwrite archive folder if expected version is already archived."
|
|
906
|
+
});
|
|
907
|
+
const CONCURRENCY_TAG = new ValueFlag("concurrency", {
|
|
908
|
+
short: "c",
|
|
909
|
+
defaultValue: 10,
|
|
910
|
+
validator: new NumberTypeValidator(),
|
|
911
|
+
long: "concurrency",
|
|
912
|
+
description: "Forces to overwrite archive folder if expected version is already archived."
|
|
913
|
+
});
|
|
914
|
+
ARCHIVE.flags.add(FORCE_TAG, CONCURRENCY_TAG);
|
|
915
|
+
ARCHIVE.action = async (_, packageName) => {
|
|
916
|
+
const isForced = _.getValue(FORCE_TAG);
|
|
917
|
+
INFO(`TARGET: ${ACCENT(packageName)}\n`);
|
|
918
|
+
INFO(`----------------------------\n`);
|
|
919
|
+
const data = await PowershellHelper.GetAppxPackage(packageName);
|
|
920
|
+
LOG(`Found ${data?.length ?? 0} matches. . .\n`);
|
|
921
|
+
if (!data || !data.length) return void ERROR("Failed to obtain installation\n");
|
|
922
|
+
const packageInfo = data[0];
|
|
923
|
+
if (!packageInfo) {
|
|
924
|
+
ERROR("FAILED TO RESOLVE PACKAGE\n");
|
|
925
|
+
ERROR(`Minecraft Package Not Found: Looking for preview? Run with *minecraftwindowsbeta*\n`);
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
const { InstallationPath, PackageFamilyName, Version } = packageInfo;
|
|
929
|
+
if (!PackageFamilyName.toLowerCase().includes("minecraft")) return void ERROR(`Minecraft Package Not Found: Looking for preview? Run with *minecraftwindowsbeta*\n`);
|
|
930
|
+
LOG(`RESOLVING REAL PATH FOR: ${JSON.stringify(InstallationPath)}\n`);
|
|
931
|
+
LOG(`----------------------------\n`);
|
|
932
|
+
let path = await realpath(InstallationPath).catch((_$1) => null);
|
|
933
|
+
if (!path) {
|
|
934
|
+
ERROR("FAILED TO RESOLVE INSTALLATION PATH\n");
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
INFO(`NAME: ${ACCENT(PackageFamilyName)}\n`);
|
|
938
|
+
INFO(`VERSION: ${ACCENT(realVersion(Version))}\n`);
|
|
939
|
+
INFO(`PATH: ${ACCENT(toURL(path))}\n`);
|
|
940
|
+
const REAL_VERSION = realVersion(Version);
|
|
941
|
+
const appId = await getPackageApplicationId(path);
|
|
942
|
+
if (!appId) return void ERROR("FAILED TO GET APPLICATION ID");
|
|
943
|
+
INFO(`APPID: ${ACCENT(appId)}\n`);
|
|
944
|
+
const isPreview = PackageFamilyName.toLowerCase().match(/beta|preview|candidate/)?.[0];
|
|
945
|
+
const name = `minecraft${isPreview ? "-" + isPreview : ""}_${REAL_VERSION}`;
|
|
946
|
+
ENVIRONMENT_INFO(_);
|
|
947
|
+
const ARCHIVE_FOLDER = join(MIRRORS_FOLDER_PATH, name);
|
|
948
|
+
let OLD_FILES;
|
|
949
|
+
const KNOWN_FOLDERS = /* @__PURE__ */ new Set();
|
|
950
|
+
const KNOWN_FILES = /* @__PURE__ */ new Set();
|
|
951
|
+
if (existsSync(ARCHIVE_FOLDER)) if (!isForced) return void ERROR("This version is already archived, if you are sure you can use --force tag to rewrite this version.\n");
|
|
952
|
+
else OLD_FILES = new Set(Array.from(RECURSIVE_GLOB.scanSync({
|
|
953
|
+
cwd: ARCHIVE_FOLDER,
|
|
954
|
+
dot: true,
|
|
955
|
+
absolute: false,
|
|
956
|
+
onlyFiles: true,
|
|
957
|
+
followSymlinks: false
|
|
958
|
+
})).map((e) => e.toLowerCase()));
|
|
959
|
+
else {
|
|
960
|
+
LOG(`Creating archive folder for this version, ${name}\n`);
|
|
961
|
+
if (!await mkdir(ARCHIVE_FOLDER, { recursive: true }).then((_$1) => true, (_$1) => false)) return void ERROR("Something went wrong when creating folder\n");
|
|
962
|
+
OLD_FILES = /* @__PURE__ */ new Set();
|
|
963
|
+
}
|
|
964
|
+
INFO(`Invoking external process to decrypt the main executable and move it to archive\n`);
|
|
965
|
+
const EXE_PATH = "Minecraft.Windows.exe";
|
|
966
|
+
const SRC_EXE_PATH = join(path, EXE_PATH);
|
|
967
|
+
const FINAL_EXE_PATH = join(ARCHIVE_FOLDER, EXE_PATH);
|
|
968
|
+
const [runtime, ...args] = createCommand(InstructionType.Copy, SRC_EXE_PATH, FINAL_EXE_PATH);
|
|
969
|
+
await PowershellHelper.InvokeCommandInDesktopPackage(PackageFamilyName, appId, runtime, args);
|
|
970
|
+
const startTime = performance.now();
|
|
971
|
+
await new Promise((res) => setTimeout(res, 1e3));
|
|
972
|
+
LOG(`ORIGINAL STATS\n`);
|
|
973
|
+
let { size } = await stat(SRC_EXE_PATH);
|
|
974
|
+
while (performance.now() - startTime > 15 * 1e3) {
|
|
975
|
+
LOG(`WAITING FOR NEW STATS TO MATCH ORIGINAL\n`);
|
|
976
|
+
await new Promise((res) => setTimeout(res, 3e3));
|
|
977
|
+
if ((await stat(FINAL_EXE_PATH).catch((_$1) => null))?.size === size) break;
|
|
978
|
+
}
|
|
979
|
+
INFO(`Successfully copied encrypted file: ${EXE_PATH}\n`);
|
|
980
|
+
KNOWN_FILES.add(EXE_PATH.toLowerCase());
|
|
981
|
+
const concurrency = _.getValue(CONCURRENCY_TAG);
|
|
982
|
+
if (concurrency < 1) WARN(`Concurrency can not be less than 1!!!\n`);
|
|
983
|
+
const channel = new TaskConcurrencyChannel(concurrency);
|
|
984
|
+
let failedFiles = [];
|
|
985
|
+
const FILTER = /(gamelaunchhelper.exe|layout.xml|layout_[^.\\\/]+.xml|minecraft.windows.exe)$/;
|
|
986
|
+
let files = 0;
|
|
987
|
+
for await (const file of RECURSIVE_GLOB.scan({
|
|
988
|
+
cwd: path,
|
|
989
|
+
absolute: false,
|
|
990
|
+
dot: true,
|
|
991
|
+
followSymlinks: false,
|
|
992
|
+
onlyFiles: true
|
|
993
|
+
})) {
|
|
994
|
+
files++;
|
|
995
|
+
if (FILTER.test(file.toLowerCase())) continue;
|
|
996
|
+
let lio = file.lastIndexOf("\\");
|
|
997
|
+
const folder = file.substring(0, lio);
|
|
998
|
+
if (lio > 0 && !KNOWN_FOLDERS.has(folder)) {
|
|
999
|
+
const ABSOLUTE_FOLDER = join(ARCHIVE_FOLDER, folder);
|
|
1000
|
+
INFO(`\x1b[36m${folder}\n`);
|
|
1001
|
+
if (!existsSync(ABSOLUTE_FOLDER) && !await mkdir(join(ARCHIVE_FOLDER, folder), { recursive: true }).then((_$1) => true, (_$1) => false)) return void ERROR(`FAILED TO CREATE FOLDER IN ARCHIVE PATHS, MAYBE ITS TOO LONG? ${ARCHIVE_FOLDER.length + folder.length + 1}, ${folder}\n`);
|
|
1002
|
+
KNOWN_FOLDERS.add(folder);
|
|
1003
|
+
}
|
|
1004
|
+
await channel.push((async () => {
|
|
1005
|
+
if (!await copyFile(join(path, file), join(ARCHIVE_FOLDER, file)).then((_$1) => true, (_$1) => false)) failedFiles.push(file);
|
|
1006
|
+
else KNOWN_FILES.add(file.toLowerCase());
|
|
1007
|
+
})());
|
|
1008
|
+
}
|
|
1009
|
+
await channel.getAwaiter();
|
|
1010
|
+
if (failedFiles.length) {
|
|
1011
|
+
ERROR(`Something went wrong when coping these specific files: \n${failedFiles.join("\n")}\n`);
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
if (isForced) WARN(`FILES TO REMOVE: ${OLD_FILES.difference(KNOWN_FILES).size}\n`);
|
|
1015
|
+
for (const file of OLD_FILES.difference(KNOWN_FILES)) {
|
|
1016
|
+
WARN(`Removing ${ACCENT(file)}\n`);
|
|
1017
|
+
await channel.push(rm(join(ARCHIVE_FOLDER, file)));
|
|
1018
|
+
}
|
|
1019
|
+
await channel.getAwaiter();
|
|
1020
|
+
INFO(`TOTAL FILES ARCHIVED: ${ACCENT(String(files))}\n`);
|
|
1021
|
+
const tags = [_.getValue(TAG).toLowerCase()];
|
|
1022
|
+
if (tags[0] !== TAG.defaultValue) tags.push(TAG.defaultValue?.toLowerCase() ?? "current");
|
|
1023
|
+
for (let tag of tags) {
|
|
1024
|
+
tag = join(TAGS_FOLDER_PATH, tag);
|
|
1025
|
+
LOG(`REMOVING OLD TAG: ${tag}\n`);
|
|
1026
|
+
await rmdir(tag).catch((_$1) => null);
|
|
1027
|
+
LOG(`CREATING NEW TAG: ${_.getValue(TAG).toLowerCase()}\n`);
|
|
1028
|
+
await symlink(ARCHIVE_FOLDER, tag, "junction").catch((_$1) => null);
|
|
1029
|
+
}
|
|
1030
|
+
INFO(`OUTPUT: ${ACCENT(pathToFileURL(join(TAGS_FOLDER_PATH, tags[0])).href)}\n`);
|
|
1031
|
+
};
|
|
1032
|
+
function realVersion(version) {
|
|
1033
|
+
const [MA = 1, MI = 0, B = 0, P = 0] = version.split(".").map(Number);
|
|
1034
|
+
return [
|
|
1035
|
+
MA,
|
|
1036
|
+
MI,
|
|
1037
|
+
Math.floor(B / 100),
|
|
1038
|
+
P + B % 100
|
|
1039
|
+
].join(".");
|
|
1040
|
+
}
|
|
1041
|
+
function toURL(path) {
|
|
1042
|
+
return `file:///${encodeURI(path.replaceAll("\\", "/"))}`;
|
|
1043
|
+
}
|
|
1044
|
+
async function getPackageApplicationId(installationPath) {
|
|
1045
|
+
LOG(`Trying to read file: /AppxManifest.xml\n`);
|
|
1046
|
+
return await readFile(join(installationPath, "AppxManifest.xml")).then((_) => {
|
|
1047
|
+
LOG(`Trying to search for application id\n`);
|
|
1048
|
+
const id = _.toString().match(/<Application(?:\s+[^>]*)??\s+Id\s*=\s*["']([^"']+)["']/is)?.[1];
|
|
1049
|
+
if (!id) {
|
|
1050
|
+
LOG(`FAILED TO SEARCH APPLICATION ID`);
|
|
1051
|
+
return null;
|
|
1052
|
+
}
|
|
1053
|
+
return id;
|
|
1054
|
+
}).catch((_) => (LOG(`FAILED TO READ FILE OR FAILED TO SEARCH FOR APPLICATION ID`), null));
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
//#endregion
|
|
1058
|
+
//#region src/cli/list.ts
|
|
1059
|
+
const LIST = ROOT.createAction("list", "List your installations", []);
|
|
1060
|
+
LIST.action = async (_) => {
|
|
1061
|
+
ENVIRONMENT_INFO(_);
|
|
1062
|
+
INFO(`----------------------------\n`);
|
|
1063
|
+
INFO(`MIRRORS:\n`);
|
|
1064
|
+
for await (const folder of new Bun.Glob("*").scan({
|
|
1065
|
+
cwd: PREFERRED_PATH + "\\mirrors",
|
|
1066
|
+
onlyFiles: false
|
|
1067
|
+
})) INFO(` ${ACCENT(folder)}\n`);
|
|
1068
|
+
INFO(`\n`);
|
|
1069
|
+
INFO(`TAGS:\n`);
|
|
1070
|
+
for await (const folder of new Bun.Glob("*").scan({
|
|
1071
|
+
cwd: PREFERRED_PATH + "\\tags",
|
|
1072
|
+
onlyFiles: false
|
|
1073
|
+
})) INFO(` ${ACCENT(folder)}\n`);
|
|
1074
|
+
INFO(`\n`);
|
|
1075
|
+
INFO(`----------------------------\n`);
|
|
1076
|
+
};
|
|
1077
|
+
|
|
1078
|
+
//#endregion
|
|
1079
|
+
//#region src/cli/run.ts
|
|
1080
|
+
const RUN = ROOT.createAction("run", "Start new minecraft instance from tag or mirror version id", [new ArgumentLike("package-name", {
|
|
1081
|
+
defaultValue: "",
|
|
1082
|
+
validator: new StringTypeValidator(),
|
|
1083
|
+
description: "Minecraft GDK Installation Mirror Id if empty then tag is used"
|
|
1084
|
+
})]);
|
|
1085
|
+
RUN.action = async (_, packageName) => {
|
|
1086
|
+
ENVIRONMENT_INFO(_);
|
|
1087
|
+
Bun.spawn({
|
|
1088
|
+
cmd: [TAGS_FOLDER_PATH + "\\" + _.getValue(TAG) + "\\Minecraft.Windows.exe"],
|
|
1089
|
+
detached: true,
|
|
1090
|
+
stdio: [
|
|
1091
|
+
"ignore",
|
|
1092
|
+
"ignore",
|
|
1093
|
+
"ignore"
|
|
1094
|
+
]
|
|
1095
|
+
}).unref();
|
|
1096
|
+
};
|
|
1097
|
+
|
|
1098
|
+
//#endregion
|
|
1099
|
+
//#region src/cli/main.ts
|
|
1100
|
+
CommandLine.run(Bun.argv, ROOT);
|
|
1101
|
+
|
|
1102
|
+
//#endregion
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gdk-version-archiver",
|
|
3
|
+
"version": "1.0.0-alpha",
|
|
4
|
+
"description": "",
|
|
5
|
+
"keywords": [],
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "conmaster2112",
|
|
8
|
+
"bin": {
|
|
9
|
+
"gdkva": "./bin/cli"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"bin/cli",
|
|
13
|
+
"dist/*",
|
|
14
|
+
"package.json",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"type": "module",
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "rolldown -c",
|
|
21
|
+
"gdk-archive-manager": "rolldown -c && bun ./dist/cli.js"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/bun": "^1.3.5",
|
|
25
|
+
"@typescript/native-preview": "7.0.0-dev.20260108.1",
|
|
26
|
+
"con-utils": "^0.0.2",
|
|
27
|
+
"oxfmt": "^0.23.0",
|
|
28
|
+
"oxlint": "^1.38.0",
|
|
29
|
+
"rolldown": "1.0.0-beta.59",
|
|
30
|
+
"rolldown-plugin-dts": "^0.20.0",
|
|
31
|
+
"typescript": "6.0.0-dev.20260108"
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"bun": ">=1.0.0",
|
|
35
|
+
"node": "please-use-bun"
|
|
36
|
+
},
|
|
37
|
+
"packageManager": "pnpm@10.25.0"
|
|
38
|
+
}
|