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.
- package/README.md +809 -0
- package/dist/commands/build.d.ts +2 -0
- package/dist/commands/build.js +447 -0
- package/dist/commands/bundle.d.ts +2 -0
- package/dist/commands/bundle.js +134 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +219 -0
- package/dist/commands/list.d.ts +10 -0
- package/dist/commands/list.js +167 -0
- package/dist/commands/login.d.ts +9 -0
- package/dist/commands/login.js +167 -0
- package/dist/commands/new.d.ts +2 -0
- package/dist/commands/new.js +132 -0
- package/dist/commands/prepare.d.ts +9 -0
- package/dist/commands/prepare.js +267 -0
- package/dist/commands/push.d.ts +9 -0
- package/dist/commands/push.js +205 -0
- package/dist/commands/validate.d.ts +2 -0
- package/dist/commands/validate.js +191 -0
- package/dist/compiler/async-transform.d.ts +21 -0
- package/dist/compiler/async-transform.js +158 -0
- package/dist/compiler/inline.d.ts +32 -0
- package/dist/compiler/inline.js +87 -0
- package/dist/compiler/postprocess.d.ts +19 -0
- package/dist/compiler/postprocess.js +134 -0
- package/dist/compiler/rhino-plugin.d.ts +17 -0
- package/dist/compiler/rhino-plugin.js +324 -0
- package/dist/compiler/transform.d.ts +18 -0
- package/dist/compiler/transform.js +59 -0
- package/dist/config.d.ts +86 -0
- package/dist/config.js +166 -0
- package/dist/credentials.d.ts +65 -0
- package/dist/credentials.js +136 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +28 -0
- package/dist/runtime.d.ts +116 -0
- package/dist/runtime.js +96 -0
- package/dist/types/globals.d.ts +80 -0
- package/dist/types/globals.js +10 -0
- package/dist/types/index.d.ts +2094 -0
- package/dist/types/index.js +9 -0
- package/package.json +57 -0
- package/templates/project/kustom.config.json +26 -0
- package/templates/project/scripts/example.ts +17 -0
- package/templates/project/scripts/lib/utils.ts +19 -0
- package/templates/project/tsconfig.json +27 -0
- package/templates/scripts/block.ts.hbs +14 -0
- package/templates/scripts/gui.ts.hbs +28 -0
- package/templates/scripts/item.ts.hbs +13 -0
- 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
|
+
});
|