kustom-mc 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +809 -0
  2. package/dist/commands/build.d.ts +2 -0
  3. package/dist/commands/build.js +447 -0
  4. package/dist/commands/bundle.d.ts +2 -0
  5. package/dist/commands/bundle.js +134 -0
  6. package/dist/commands/init.d.ts +2 -0
  7. package/dist/commands/init.js +219 -0
  8. package/dist/commands/list.d.ts +10 -0
  9. package/dist/commands/list.js +167 -0
  10. package/dist/commands/login.d.ts +9 -0
  11. package/dist/commands/login.js +167 -0
  12. package/dist/commands/new.d.ts +2 -0
  13. package/dist/commands/new.js +132 -0
  14. package/dist/commands/prepare.d.ts +9 -0
  15. package/dist/commands/prepare.js +267 -0
  16. package/dist/commands/push.d.ts +9 -0
  17. package/dist/commands/push.js +205 -0
  18. package/dist/commands/validate.d.ts +2 -0
  19. package/dist/commands/validate.js +191 -0
  20. package/dist/compiler/async-transform.d.ts +21 -0
  21. package/dist/compiler/async-transform.js +158 -0
  22. package/dist/compiler/inline.d.ts +32 -0
  23. package/dist/compiler/inline.js +87 -0
  24. package/dist/compiler/postprocess.d.ts +19 -0
  25. package/dist/compiler/postprocess.js +134 -0
  26. package/dist/compiler/rhino-plugin.d.ts +17 -0
  27. package/dist/compiler/rhino-plugin.js +324 -0
  28. package/dist/compiler/transform.d.ts +18 -0
  29. package/dist/compiler/transform.js +59 -0
  30. package/dist/config.d.ts +86 -0
  31. package/dist/config.js +166 -0
  32. package/dist/credentials.d.ts +65 -0
  33. package/dist/credentials.js +136 -0
  34. package/dist/index.d.ts +2 -0
  35. package/dist/index.js +28 -0
  36. package/dist/runtime.d.ts +116 -0
  37. package/dist/runtime.js +96 -0
  38. package/dist/types/globals.d.ts +80 -0
  39. package/dist/types/globals.js +10 -0
  40. package/dist/types/index.d.ts +2094 -0
  41. package/dist/types/index.js +9 -0
  42. package/package.json +57 -0
  43. package/templates/project/kustom.config.json +26 -0
  44. package/templates/project/scripts/example.ts +17 -0
  45. package/templates/project/scripts/lib/utils.ts +19 -0
  46. package/templates/project/tsconfig.json +27 -0
  47. package/templates/scripts/block.ts.hbs +14 -0
  48. package/templates/scripts/gui.ts.hbs +28 -0
  49. package/templates/scripts/item.ts.hbs +13 -0
  50. package/templates/scripts/script.ts.hbs +18 -0
@@ -0,0 +1,132 @@
1
+ import { Command } from 'commander';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import Handlebars from 'handlebars';
5
+ const templates = {
6
+ script: `import { defineScript } from 'kustom-mc';
7
+
8
+ export default defineScript({
9
+ props: {
10
+ // Define your props here
11
+ },
12
+
13
+ run({ executor, props, process }) {
14
+ const player = executor.asPlayer();
15
+ if (!player) {
16
+ console.error("This script requires a player executor");
17
+ return;
18
+ }
19
+
20
+ // Your script logic here
21
+ player.sendMessage("Hello from {{name}}!");
22
+ }
23
+ });
24
+ `,
25
+ block: `import { defineScript } from 'kustom-mc';
26
+
27
+ export default defineScript({
28
+ run({ shaper }) {
29
+ shaper.create("{{name}}")
30
+ .withModel("kustom:block/{{name}}")
31
+ .fullBlockCollision()
32
+ .onClick((event) => {
33
+ const player = event.player;
34
+ player.sendMessage("You clicked {{name}}!");
35
+ })
36
+ .register();
37
+ }
38
+ });
39
+ `,
40
+ item: `import { defineScript } from 'kustom-mc';
41
+
42
+ export default defineScript({
43
+ run({ items, definition }) {
44
+ items.create("{{name}}")
45
+ .withModel(definition.model("kustom:item/{{name}}"))
46
+ .on("rightClick", (event) => {
47
+ const player = event.player;
48
+ player.sendMessage("You used {{name}}!");
49
+ })
50
+ .register();
51
+ }
52
+ });
53
+ `,
54
+ gui: `import { defineScript, Screen, Props } from 'kustom-mc';
55
+
56
+ export default defineScript({
57
+ props: {
58
+ title: Props.String("{{name}}"),
59
+ },
60
+
61
+ run({ executor, props, process }) {
62
+ const player = executor.asPlayer();
63
+ if (!player) {
64
+ console.error("This script requires a player executor");
65
+ return;
66
+ }
67
+
68
+ const screen = new Screen(54, {
69
+ autoCover: true,
70
+ });
71
+
72
+ screen.appendText(props.title, 9, 20, true);
73
+
74
+ screen.addButton(8, "close", "Close", () => {
75
+ screen.close();
76
+ process.exit(null);
77
+ });
78
+
79
+ screen.open(player);
80
+ }
81
+ });
82
+ `
83
+ };
84
+ export const newCommand = new Command('new')
85
+ .description('Generate a new script from template')
86
+ .argument('<type>', 'Type of script (script, block, item, gui)')
87
+ .argument('<name>', 'Name of the script')
88
+ .option('-f, --force', 'Overwrite existing file')
89
+ .action(async (type, name, options) => {
90
+ const validTypes = Object.keys(templates);
91
+ if (!validTypes.includes(type)) {
92
+ console.error(`Invalid type: ${type}`);
93
+ console.error(`Valid types: ${validTypes.join(', ')}`);
94
+ process.exit(1);
95
+ }
96
+ // Determine output directory and file
97
+ const dirMap = {
98
+ script: 'scripts',
99
+ block: 'blocks',
100
+ item: 'items',
101
+ gui: 'scripts'
102
+ };
103
+ const dir = dirMap[type];
104
+ const fileName = `${name}.ts`;
105
+ const filePath = path.join(process.cwd(), dir, fileName);
106
+ // Check if file exists
107
+ if (fs.existsSync(filePath) && !options.force) {
108
+ console.error(`File already exists: ${dir}/${fileName}`);
109
+ console.error('Use --force to overwrite.');
110
+ process.exit(1);
111
+ }
112
+ // Ensure directory exists
113
+ const fullDir = path.dirname(filePath);
114
+ if (!fs.existsSync(fullDir)) {
115
+ fs.mkdirSync(fullDir, { recursive: true });
116
+ }
117
+ // Generate from template
118
+ const template = Handlebars.compile(templates[type]);
119
+ const content = template({ name });
120
+ fs.writeFileSync(filePath, content);
121
+ console.log(`Created: ${dir}/${fileName}`);
122
+ // Suggest next steps
123
+ if (type === 'gui') {
124
+ console.log(`\nDon't forget to create the GUI texture: gui/${name}.png`);
125
+ }
126
+ else if (type === 'block') {
127
+ console.log(`\nDon't forget to create the block model: models/block/${name}.json`);
128
+ }
129
+ else if (type === 'item') {
130
+ console.log(`\nDon't forget to create the item model: models/item/${name}.json`);
131
+ }
132
+ });
@@ -0,0 +1,9 @@
1
+ import { Command } from 'commander';
2
+ /**
3
+ * Run the prepare command - scan project and generate types.
4
+ */
5
+ export declare function prepare(projectDir: string, options?: {
6
+ verbose?: boolean;
7
+ skipServer?: boolean;
8
+ }): Promise<void>;
9
+ export declare const prepareCommand: Command;
@@ -0,0 +1,267 @@
1
+ import { Command } from 'commander';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import { glob } from 'glob';
5
+ import chalk from 'chalk';
6
+ import { loadConfig } from '../config.js';
7
+ import { getServerToken, normalizeServerUrl } from '../credentials.js';
8
+ /**
9
+ * Scan the sounds/ folder and return an array of sound event names.
10
+ *
11
+ * sounds/ding.ogg → "ding"
12
+ * sounds/effects/boom.ogg → "effects.boom"
13
+ * sounds/music/theme.ogg → "music.theme"
14
+ */
15
+ async function scanSounds(projectDir) {
16
+ const soundsDir = path.join(projectDir, 'sounds');
17
+ if (!fs.existsSync(soundsDir)) {
18
+ return [];
19
+ }
20
+ const oggFiles = await glob('**/*.ogg', {
21
+ cwd: soundsDir,
22
+ nodir: true
23
+ });
24
+ return oggFiles.map(file => {
25
+ // Remove .ogg extension and convert slashes to dots
26
+ const withoutExt = file.replace(/\.ogg$/i, '');
27
+ return withoutExt.replace(/[\\/]/g, '.');
28
+ });
29
+ }
30
+ /**
31
+ * Generate TypeScript declaration content for sounds.
32
+ * This is the single source of truth for sound types.
33
+ */
34
+ function generateSoundTypes(sounds) {
35
+ const soundType = sounds.length > 0
36
+ ? sounds.map(s => `"${s}"`).join(' | ')
37
+ : 'string'; // fallback to string when no sounds
38
+ return `// Auto-generated by kustom-mc prepare
39
+ // Do not edit manually - changes will be overwritten
40
+
41
+ export type KustomSounds = ${soundType};
42
+ `;
43
+ }
44
+ /**
45
+ * Generate the main index.d.ts that declares globals using the generated types.
46
+ */
47
+ function generateIndexTypes(sounds) {
48
+ const soundType = sounds.length > 0
49
+ ? sounds.map(s => `"${s}"`).join(' | ')
50
+ : 'string';
51
+ return `// Auto-generated by kustom-mc prepare
52
+ // Do not edit manually - changes will be overwritten
53
+
54
+ import type { PlaySoundOptions, Sound } from 'kustom-mc';
55
+
56
+ /** Available custom sounds in this project */
57
+ export type KustomSounds = ${soundType};
58
+
59
+ declare global {
60
+ /**
61
+ * Play a sound to one or more targets.
62
+ *
63
+ * @param sound - Custom sound name or "minecraft:..." for vanilla sounds
64
+ * @param target - Player, location, entity, or "*" for all players
65
+ * @param options - Volume, pitch, category options
66
+ */
67
+ function playSound(
68
+ sound: KustomSounds | \`minecraft:\${string}\`,
69
+ target: unknown,
70
+ options?: PlaySoundOptions
71
+ ): Sound;
72
+ }
73
+ `;
74
+ }
75
+ /**
76
+ * Parse a dependency string into pack ID and version range.
77
+ * Format: "pack-id@version-range" (e.g., "core-pack@^1.0.0")
78
+ */
79
+ function parseDependency(dep) {
80
+ const match = dep.match(/^([a-z0-9-]+)@(.+)$/);
81
+ if (!match)
82
+ return null;
83
+ return { packId: match[1], versionRange: match[2] };
84
+ }
85
+ /**
86
+ * Fetch type definitions for a pack from the server.
87
+ */
88
+ async function fetchPackTypes(serverUrl, packId) {
89
+ const url = `${normalizeServerUrl(serverUrl)}/packs/${encodeURIComponent(packId)}/types`;
90
+ try {
91
+ const response = await fetch(url, {
92
+ method: 'GET',
93
+ headers: {
94
+ 'Accept': 'text/plain'
95
+ }
96
+ });
97
+ if (response.status === 404) {
98
+ return null; // Pack not found
99
+ }
100
+ if (!response.ok) {
101
+ throw new Error(`Server returned ${response.status}`);
102
+ }
103
+ return await response.text();
104
+ }
105
+ catch (error) {
106
+ throw new Error(`Failed to fetch types for ${packId}: ${error instanceof Error ? error.message : error}`);
107
+ }
108
+ }
109
+ /**
110
+ * Fetch types for all dependencies from the server.
111
+ */
112
+ async function fetchDependencyTypes(serverUrl, dependencies, typesDir, verbose) {
113
+ const fetched = [];
114
+ const failed = [];
115
+ for (const dep of dependencies) {
116
+ const parsed = parseDependency(dep);
117
+ if (!parsed) {
118
+ if (verbose) {
119
+ console.log(chalk.yellow(` Skipping invalid dependency: ${dep}`));
120
+ }
121
+ failed.push(dep);
122
+ continue;
123
+ }
124
+ const { packId } = parsed;
125
+ if (verbose) {
126
+ console.log(` Fetching types for ${packId}...`);
127
+ }
128
+ try {
129
+ const types = await fetchPackTypes(serverUrl, packId);
130
+ if (types === null) {
131
+ if (verbose) {
132
+ console.log(chalk.yellow(` Pack not found on server: ${packId}`));
133
+ }
134
+ failed.push(packId);
135
+ continue;
136
+ }
137
+ // Write types to .kustom/types/{packId}.d.ts
138
+ const typesPath = path.join(typesDir, `${packId}.d.ts`);
139
+ fs.writeFileSync(typesPath, types);
140
+ fetched.push(packId);
141
+ if (verbose) {
142
+ console.log(chalk.green(` Saved: ${packId}.d.ts`));
143
+ }
144
+ }
145
+ catch (error) {
146
+ if (verbose) {
147
+ console.log(chalk.red(` Error: ${error instanceof Error ? error.message : error}`));
148
+ }
149
+ failed.push(packId);
150
+ }
151
+ }
152
+ return { fetched, failed };
153
+ }
154
+ /**
155
+ * Generate an index.d.ts that re-exports all dependency types.
156
+ */
157
+ function generateDependencyIndex(packIds) {
158
+ if (packIds.length === 0) {
159
+ return `// Auto-generated by kustom-mc prepare
160
+ // No dependencies configured
161
+ export {};
162
+ `;
163
+ }
164
+ const exports = packIds.map(id => `export * as ${id.replace(/-/g, '_')} from './${id}.js';`);
165
+ return `// Auto-generated by kustom-mc prepare
166
+ // Re-exports types from all dependencies
167
+
168
+ ${exports.join('\n')}
169
+ `;
170
+ }
171
+ /**
172
+ * Run the prepare command - scan project and generate types.
173
+ */
174
+ export async function prepare(projectDir, options) {
175
+ const config = loadConfig(projectDir);
176
+ const verbose = options?.verbose ?? false;
177
+ // === Local Types (dist/types/) ===
178
+ const distTypesDir = path.join(projectDir, 'dist', 'types');
179
+ // Ensure dist/types directory exists
180
+ if (!fs.existsSync(distTypesDir)) {
181
+ fs.mkdirSync(distTypesDir, { recursive: true });
182
+ }
183
+ // Scan sounds
184
+ const sounds = await scanSounds(projectDir);
185
+ // Generate and write sounds types
186
+ const soundTypesContent = generateSoundTypes(sounds);
187
+ const soundTypesPath = path.join(distTypesDir, 'sounds.d.ts');
188
+ fs.writeFileSync(soundTypesPath, soundTypesContent);
189
+ // Generate main index.d.ts with global declarations
190
+ const indexTypesContent = generateIndexTypes(sounds);
191
+ const indexTypesPath = path.join(distTypesDir, 'index.d.ts');
192
+ fs.writeFileSync(indexTypesPath, indexTypesContent);
193
+ if (sounds.length > 0) {
194
+ console.log(`Generated types for ${sounds.length} sound(s)`);
195
+ }
196
+ // === Dependency Types (.kustom/types/) ===
197
+ if (options?.skipServer) {
198
+ if (verbose) {
199
+ console.log(chalk.gray('Skipping server dependency fetch (--skip-server)'));
200
+ }
201
+ return;
202
+ }
203
+ const dependencies = config.dependencies || [];
204
+ if (dependencies.length === 0) {
205
+ if (verbose) {
206
+ console.log(chalk.gray('No dependencies configured'));
207
+ }
208
+ return;
209
+ }
210
+ // Check for server URL
211
+ const serverUrl = config.server?.url;
212
+ if (!serverUrl) {
213
+ console.log(chalk.yellow('Warning: Dependencies configured but no server.url in config'));
214
+ console.log(chalk.yellow(' Skipping dependency type fetching'));
215
+ return;
216
+ }
217
+ // Check if logged in (optional, types endpoint may not require auth)
218
+ const token = getServerToken(serverUrl);
219
+ if (!token && verbose) {
220
+ console.log(chalk.gray('Not logged in - some types may not be available'));
221
+ }
222
+ // Create .kustom/types directory
223
+ const kustomTypesDir = path.join(projectDir, '.kustom', 'types');
224
+ if (!fs.existsSync(kustomTypesDir)) {
225
+ fs.mkdirSync(kustomTypesDir, { recursive: true });
226
+ }
227
+ console.log(`\nFetching dependency types from ${serverUrl}...`);
228
+ const { fetched, failed } = await fetchDependencyTypes(serverUrl, dependencies, kustomTypesDir, verbose);
229
+ // Generate index for dependencies
230
+ if (fetched.length > 0) {
231
+ const depIndexContent = generateDependencyIndex(fetched);
232
+ const depIndexPath = path.join(kustomTypesDir, 'index.d.ts');
233
+ fs.writeFileSync(depIndexPath, depIndexContent);
234
+ }
235
+ // Summary
236
+ if (fetched.length > 0) {
237
+ console.log(chalk.green(`Fetched types for ${fetched.length} dependency(ies)`));
238
+ }
239
+ if (failed.length > 0) {
240
+ console.log(chalk.yellow(`Failed to fetch ${failed.length} dependency(ies): ${failed.join(', ')}`));
241
+ }
242
+ // Add .kustom to .gitignore if not already there
243
+ const gitignorePath = path.join(projectDir, '.gitignore');
244
+ if (fs.existsSync(gitignorePath)) {
245
+ const gitignore = fs.readFileSync(gitignorePath, 'utf-8');
246
+ if (!gitignore.includes('.kustom/')) {
247
+ fs.appendFileSync(gitignorePath, '\n# Kustom SDK cache\n.kustom/\n');
248
+ if (verbose) {
249
+ console.log(chalk.gray('Added .kustom/ to .gitignore'));
250
+ }
251
+ }
252
+ }
253
+ }
254
+ export const prepareCommand = new Command('prepare')
255
+ .description('Generate TypeScript definitions from project structure and fetch dependency types')
256
+ .option('-v, --verbose', 'Show detailed output')
257
+ .option('--skip-server', 'Skip fetching types from server')
258
+ .action(async (options) => {
259
+ const projectDir = process.cwd();
260
+ console.log('Preparing project types...');
261
+ await prepare(projectDir, options);
262
+ console.log('\nTypes generated in dist/types/');
263
+ const config = loadConfig(projectDir);
264
+ if (config.dependencies && config.dependencies.length > 0) {
265
+ console.log('Dependency types in .kustom/types/');
266
+ }
267
+ });
@@ -0,0 +1,9 @@
1
+ import { Command } from 'commander';
2
+ /**
3
+ * Push command - bundle and upload pack to server.
4
+ *
5
+ * Usage:
6
+ * npx kustom-mc push # Push to configured server
7
+ * npx kustom-mc push --server <url> # Push to specific server
8
+ */
9
+ export declare const pushCommand: Command;
@@ -0,0 +1,205 @@
1
+ import { Command } from 'commander';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import chalk from 'chalk';
5
+ import { glob } from 'glob';
6
+ import archiver from 'archiver';
7
+ import { loadConfig, generateManifest, validateManifest } from '../config.js';
8
+ import { getServerToken, normalizeServerUrl, getServerCredential } from '../credentials.js';
9
+ /**
10
+ * Create a zip bundle in memory and return as Buffer.
11
+ */
12
+ async function createBundle(projectDir) {
13
+ const config = loadConfig(projectDir);
14
+ // Validate manifest
15
+ const errors = validateManifest(config);
16
+ if (errors.length > 0) {
17
+ throw new Error(`Manifest validation failed:\n ${errors.join('\n ')}`);
18
+ }
19
+ const packId = config.manifest?.id || 'kustompack';
20
+ const version = config.manifest?.version || '1.0.0';
21
+ return new Promise((resolve, reject) => {
22
+ const chunks = [];
23
+ const archive = archiver('zip', { zlib: { level: 9 } });
24
+ archive.on('data', (chunk) => chunks.push(chunk));
25
+ archive.on('end', () => resolve({
26
+ buffer: Buffer.concat(chunks),
27
+ packId,
28
+ version
29
+ }));
30
+ archive.on('error', reject);
31
+ // Generate and add manifest
32
+ const manifestContent = generateManifest(config);
33
+ archive.append(manifestContent, { name: 'kustompack.json' });
34
+ // Add files based on bundle config
35
+ const includePatterns = config.bundle?.include || [
36
+ '**/*.js',
37
+ 'textures/**/*',
38
+ 'gui/**/*',
39
+ 'models/**/*',
40
+ 'sounds/**/*'
41
+ ];
42
+ const addFiles = async () => {
43
+ const addedFiles = new Set();
44
+ for (const pattern of includePatterns) {
45
+ const files = await glob(pattern, {
46
+ cwd: projectDir,
47
+ nodir: true,
48
+ ignore: ['node_modules/**', 'dist/**', '*.ts', 'kustompack.json']
49
+ });
50
+ for (const file of files) {
51
+ if (addedFiles.has(file))
52
+ continue;
53
+ const filePath = path.resolve(projectDir, file);
54
+ if (fs.existsSync(filePath)) {
55
+ archive.file(filePath, { name: file });
56
+ addedFiles.add(file);
57
+ }
58
+ }
59
+ }
60
+ archive.finalize();
61
+ };
62
+ addFiles().catch(reject);
63
+ });
64
+ }
65
+ /**
66
+ * Upload a pack to the server.
67
+ *
68
+ * Currently sends raw zip bytes for simplicity and performance.
69
+ *
70
+ * TODO: If a web UI is added later, consider supporting multipart/form-data
71
+ * uploads on the server side (Option 2). This would require:
72
+ * - Adding multipart parsing to PackRegistryServer.java (Apache Commons FileUpload or manual)
73
+ * - Keeping raw zip support for CLI (faster)
74
+ * - Adding multipart support for browser uploads
75
+ */
76
+ async function uploadPack(serverUrl, token, buffer, packId) {
77
+ const url = `${normalizeServerUrl(serverUrl)}/packs/upload`;
78
+ // Send raw zip bytes (faster than multipart, server already supports this)
79
+ const response = await fetch(url, {
80
+ method: 'POST',
81
+ headers: {
82
+ 'Authorization': `Bearer ${token}`,
83
+ 'Content-Type': 'application/zip',
84
+ 'X-Pack-Id': packId // Optional hint for server
85
+ },
86
+ body: buffer
87
+ });
88
+ if (response.status === 401) {
89
+ throw new Error('Authentication failed. Token may be invalid or expired. Run: npx kustom-mc login');
90
+ }
91
+ if (response.status === 403) {
92
+ throw new Error('Permission denied. You may not have upload permissions.');
93
+ }
94
+ const result = await response.json();
95
+ if (!response.ok) {
96
+ throw new Error(result.error || result.message || `Server returned ${response.status}`);
97
+ }
98
+ return result;
99
+ }
100
+ /**
101
+ * Push command - bundle and upload pack to server.
102
+ *
103
+ * Usage:
104
+ * npx kustom-mc push # Push to configured server
105
+ * npx kustom-mc push --server <url> # Push to specific server
106
+ */
107
+ export const pushCommand = new Command('push')
108
+ .description('Bundle and upload pack to the connected server')
109
+ .option('-s, --server <url>', 'Server URL (overrides config)')
110
+ .option('--dry-run', 'Create bundle but do not upload')
111
+ .option('-y, --yes', 'Skip confirmation prompt')
112
+ .action(async (options) => {
113
+ const projectDir = process.cwd();
114
+ const config = loadConfig(projectDir);
115
+ // Determine server URL
116
+ let serverUrl;
117
+ if (options?.server) {
118
+ serverUrl = options.server;
119
+ }
120
+ else {
121
+ if (!config.server?.url) {
122
+ console.error(chalk.red('Error: No server URL configured'));
123
+ console.log('Set server.url in kustom.config.json or use --server flag');
124
+ process.exit(1);
125
+ }
126
+ serverUrl = config.server.url;
127
+ }
128
+ serverUrl = normalizeServerUrl(serverUrl);
129
+ // Check authentication
130
+ const token = getServerToken(serverUrl);
131
+ if (!token && !options?.dryRun) {
132
+ console.error(chalk.red('Error: Not logged in to server'));
133
+ console.log(`Run: npx kustom-mc login ${serverUrl} <token>`);
134
+ console.log('Get a token by running /kustom token in-game.');
135
+ process.exit(1);
136
+ }
137
+ const cred = getServerCredential(serverUrl);
138
+ console.log(chalk.blue('Preparing pack for upload...'));
139
+ console.log(` Server: ${serverUrl}`);
140
+ if (cred?.playerName) {
141
+ console.log(` As: ${cred.playerName}`);
142
+ }
143
+ console.log();
144
+ try {
145
+ // Create bundle
146
+ console.log('Creating bundle...');
147
+ const { buffer, packId, version } = await createBundle(projectDir);
148
+ const sizeKB = (buffer.length / 1024).toFixed(2);
149
+ console.log(chalk.green(` Pack: ${packId} v${version}`));
150
+ console.log(chalk.green(` Size: ${sizeKB} KB`));
151
+ if (options?.dryRun) {
152
+ console.log(chalk.yellow('\n--dry-run: Bundle created but not uploaded.'));
153
+ // Optionally save the bundle locally
154
+ const outputPath = path.join(projectDir, 'dist', `${packId}.zip`);
155
+ const outputDir = path.dirname(outputPath);
156
+ if (!fs.existsSync(outputDir)) {
157
+ fs.mkdirSync(outputDir, { recursive: true });
158
+ }
159
+ fs.writeFileSync(outputPath, buffer);
160
+ console.log(` Saved to: ${outputPath}`);
161
+ return;
162
+ }
163
+ // Confirmation
164
+ if (!options?.yes) {
165
+ console.log(chalk.yellow(`\nThis will upload ${packId} v${version} to ${serverUrl}`));
166
+ console.log(chalk.yellow('If a pack with this ID exists, it will be replaced.'));
167
+ // Simple confirmation using readline
168
+ const readline = await import('readline');
169
+ const rl = readline.createInterface({
170
+ input: process.stdin,
171
+ output: process.stdout
172
+ });
173
+ const answer = await new Promise((resolve) => {
174
+ rl.question('Continue? [y/N] ', resolve);
175
+ });
176
+ rl.close();
177
+ if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
178
+ console.log('Cancelled.');
179
+ process.exit(0);
180
+ }
181
+ }
182
+ // Upload
183
+ console.log('\nUploading...');
184
+ const result = await uploadPack(serverUrl, token, buffer, packId);
185
+ if (result.success) {
186
+ console.log(chalk.green('\nUpload successful!'));
187
+ if (result.message) {
188
+ console.log(` ${result.message}`);
189
+ }
190
+ console.log(`\nPack ${packId} v${version} is now available on the server.`);
191
+ console.log(chalk.gray('Run /kustom pack reload to apply changes in-game.'));
192
+ }
193
+ else {
194
+ console.error(chalk.red('\nUpload failed'));
195
+ if (result.error) {
196
+ console.error(chalk.red(` ${result.error}`));
197
+ }
198
+ process.exit(1);
199
+ }
200
+ }
201
+ catch (error) {
202
+ console.error(chalk.red(`\nFailed: ${error instanceof Error ? error.message : error}`));
203
+ process.exit(1);
204
+ }
205
+ });
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare const validateCommand: Command;