kustom-mc 0.1.3 → 0.1.4

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.
@@ -26,11 +26,9 @@ export const initCommand = new Command('init')
26
26
  process.exit(1);
27
27
  }
28
28
  }
29
- // Create directory structure
29
+ // Create directory structure (all scripts go in scripts/, type determined by __type)
30
30
  const dirs = [
31
31
  'scripts/lib',
32
- 'blocks',
33
- 'items',
34
32
  'textures',
35
33
  'gui',
36
34
  'models',
@@ -68,8 +66,6 @@ export const initCommand = new Command('init')
68
66
  },
69
67
  include: [
70
68
  "scripts/**/*.ts",
71
- "blocks/**/*.ts",
72
- "items/**/*.ts",
73
69
  "dist/types/**/*.d.ts",
74
70
  ".kustom/types/**/*.d.ts",
75
71
  "node_modules/kustom-mc/dist/types/globals.d.ts"
@@ -80,9 +76,7 @@ export const initCommand = new Command('init')
80
76
  // Write kustom.config.json with manifest
81
77
  const kustomConfig = {
82
78
  include: [
83
- "scripts/**/*.ts",
84
- "blocks/**/*.ts",
85
- "items/**/*.ts"
79
+ "scripts/**/*.ts"
86
80
  ],
87
81
  exclude: [
88
82
  "**/*.test.ts",
@@ -189,11 +183,9 @@ dist/
189
183
  ├── kustom.config.json # Pack configuration & manifest
190
184
  ├── tsconfig.json # TypeScript configuration
191
185
  ├── package.json # npm configuration
192
- ├── scripts/ # General scripts
186
+ ├── scripts/ # All scripts (type determined by defineScript/defineBlock/defineItem)
193
187
  │ ├── example.ts # Example script
194
188
  │ └── lib/ # Shared utilities
195
- ├── blocks/ # Block definitions
196
- ├── items/ # Item definitions
197
189
  ├── textures/ # Texture files
198
190
  ├── models/ # Model files
199
191
  ├── sounds/ # Sound files
@@ -1,6 +1,7 @@
1
1
  import { Command } from 'commander';
2
+ import * as readline from 'readline';
2
3
  import chalk from 'chalk';
3
- import { loadConfig } from '../config.js';
4
+ import { loadConfig, saveConfigServerUrl } from '../config.js';
4
5
  import { saveServerToken, getServerCredential, normalizeServerUrl, listStoredServers, removeServerCredential } from '../credentials.js';
5
6
  /**
6
7
  * Validate a token with the server.
@@ -60,6 +61,7 @@ export const loginCommand = new Command('login')
60
61
  // Determine server URL
61
62
  let serverUrl;
62
63
  let token;
64
+ let serverUrlExplicit = false; // true when user provided the URL as a CLI argument
63
65
  // If only one argument provided, it might be the token (use config for server)
64
66
  if (serverUrlArg && !tokenArg && !serverUrlArg.includes('://') && !serverUrlArg.includes(':')) {
65
67
  // Looks like a token, not a URL
@@ -76,6 +78,7 @@ export const loginCommand = new Command('login')
76
78
  else if (serverUrlArg) {
77
79
  serverUrl = serverUrlArg;
78
80
  token = tokenArg;
81
+ serverUrlExplicit = true;
79
82
  }
80
83
  else {
81
84
  // No arguments - use config
@@ -159,6 +162,29 @@ export const loginCommand = new Command('login')
159
162
  console.log(` Token expires: ${result.expiresAt}`);
160
163
  }
161
164
  console.log(`\nCredentials saved to ~/.kustom/credentials.json`);
165
+ // Offer to update kustom.config.json if the user explicitly provided a server URL
166
+ if (serverUrlExplicit) {
167
+ const config = loadConfig(process.cwd());
168
+ const configServerUrl = config.server?.url ? normalizeServerUrl(config.server.url) : undefined;
169
+ if (configServerUrl !== serverUrl) {
170
+ const rl = readline.createInterface({
171
+ input: process.stdin,
172
+ output: process.stdout
173
+ });
174
+ const answer = await new Promise((resolve) => {
175
+ rl.question(chalk.yellow(`Update server.url in kustom.config.json to ${serverUrl}? [y/N] `), resolve);
176
+ });
177
+ rl.close();
178
+ if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
179
+ if (saveConfigServerUrl(process.cwd(), serverUrl)) {
180
+ console.log(chalk.green('Updated server.url in kustom.config.json'));
181
+ }
182
+ else {
183
+ console.log(chalk.yellow('Could not update kustom.config.json (file not found or not writable)'));
184
+ }
185
+ }
186
+ }
187
+ }
162
188
  }
163
189
  catch (error) {
164
190
  console.error(chalk.red(`Login failed: ${error instanceof Error ? error.message : error}`));
@@ -22,32 +22,37 @@ export default defineScript({
22
22
  }
23
23
  });
24
24
  `,
25
- block: `import { defineScript } from 'kustom-mc';
25
+ block: `import { defineBlock } from 'kustom-mc';
26
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();
27
+ export default defineBlock({
28
+ id: "{{name}}",
29
+ model: "kustom:block/{{name}}",
30
+ collision: "full",
31
+ autoPlace: true,
32
+
33
+ onClick(event) {
34
+ event.player.sendMessage("You clicked {{name}}!");
35
+ },
36
+
37
+ onBreak(event) {
38
+ event.player.sendMessage("{{name}} broken!");
39
+ },
40
+
41
+ onPlace(event) {
42
+ event.player.sendMessage("{{name}} placed!");
37
43
  }
38
44
  });
39
45
  `,
40
- item: `import { defineScript } from 'kustom-mc';
46
+ item: `import { defineItem } from 'kustom-mc';
41
47
 
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();
48
+ export default defineItem({
49
+ id: "{{name}}",
50
+ material: "PAPER",
51
+ model: "kustom:item/{{name}}",
52
+ displayName: "{{name}}",
53
+
54
+ onRightClick(event) {
55
+ event.player.sendMessage("You used {{name}}!");
51
56
  }
52
57
  });
53
58
  `,
@@ -94,10 +99,11 @@ export const newCommand = new Command('new')
94
99
  process.exit(1);
95
100
  }
96
101
  // Determine output directory and file
102
+ // All script types go in scripts/ — type is determined by defineScript/defineBlock/defineItem
97
103
  const dirMap = {
98
104
  script: 'scripts',
99
- block: 'blocks',
100
- item: 'items',
105
+ block: 'scripts',
106
+ item: 'scripts',
101
107
  gui: 'scripts'
102
108
  };
103
109
  const dir = dirMap[type];
@@ -30,7 +30,7 @@ export const validateCommand = new Command('validate')
30
30
  }
31
31
  console.log('Validating scripts...');
32
32
  // Find all TypeScript files
33
- const patterns = config.include || ['scripts/**/*.ts', 'blocks/**/*.ts', 'items/**/*.ts'];
33
+ const patterns = config.include || ['scripts/**/*.ts'];
34
34
  const excludePatterns = config.exclude || [];
35
35
  const files = [];
36
36
  for (const pattern of patterns) {
package/dist/config.d.ts CHANGED
@@ -80,3 +80,9 @@ export declare function parseCrossPackImport(importPath: string): {
80
80
  * Create a fully qualified namespaced ID.
81
81
  */
82
82
  export declare function createNamespacedId(packId: string, type: string, name: string): string;
83
+ /**
84
+ * Update the server.url in kustom.config.json.
85
+ * Preserves all other config properties.
86
+ * Returns true if the file was updated, false if no config file exists.
87
+ */
88
+ export declare function saveConfigServerUrl(cwd: string, serverUrl: string): boolean;
package/dist/config.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  const defaultConfig = {
4
- include: ['scripts/**/*.ts', 'blocks/**/*.ts', 'items/**/*.ts'],
4
+ include: ['scripts/**/*.ts'],
5
5
  exclude: ['**/*.test.ts', '**/*.spec.ts'],
6
6
  outDir: 'dist',
7
7
  lib: ['scripts/lib'],
@@ -159,3 +159,27 @@ export function parseCrossPackImport(importPath) {
159
159
  export function createNamespacedId(packId, type, name) {
160
160
  return `${packId}:${type}/${name}`;
161
161
  }
162
+ /**
163
+ * Update the server.url in kustom.config.json.
164
+ * Preserves all other config properties.
165
+ * Returns true if the file was updated, false if no config file exists.
166
+ */
167
+ export function saveConfigServerUrl(cwd, serverUrl) {
168
+ const configPath = path.join(cwd, 'kustom.config.json');
169
+ if (!fs.existsSync(configPath)) {
170
+ return false;
171
+ }
172
+ try {
173
+ const content = fs.readFileSync(configPath, 'utf-8');
174
+ const config = JSON.parse(content);
175
+ if (!config.server || typeof config.server !== 'object') {
176
+ config.server = {};
177
+ }
178
+ config.server.url = serverUrl;
179
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
180
+ return true;
181
+ }
182
+ catch {
183
+ return false;
184
+ }
185
+ }
package/dist/runtime.d.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  * plain objects with __type property injected.
9
9
  */
10
10
  import './types/globals.js';
11
- import type { ScriptConfig, ExportedScript, PropSchema, EventSchema, EmptyEventSchema, BlockConfig, BlockDefinition, ItemConfig, ItemDefinition, ScreenConstructor, ContainerConstructor, ElementFactory, ScriptableLocationFactory, PropsAPI, CellStateUtils } from './types/index.js';
11
+ import type { ScriptConfig, ScriptConfigWithMethods, ExportedScript, PropSchema, EventSchema, EmptyEventSchema, BlockConfig, BlockDefinition, ItemConfig, ItemDefinition, ScreenConstructor, ContainerConstructor, ElementFactory, ScriptableLocationFactory, PropsAPI, CellStateUtils, MethodMap } from './types/index.js';
12
12
  /**
13
13
  * Screen constructor - create new screens for GUIs.
14
14
  * Actual value is injected by Java at runtime.
@@ -59,13 +59,31 @@ export declare const ScriptableLocation: ScriptableLocationFactory;
59
59
  * onSuccess: Props.Boolean(),
60
60
  * onError: Props.String(),
61
61
  * },
62
- * run({ process, props }) {
62
+ * methods: {
63
+ * greet({ executor, props }) {
64
+ * executor.asPlayer()?.sendMessage(props.message);
65
+ * },
66
+ * },
67
+ * run({ process, props, methods }) {
63
68
  * const player = process.getPlayer(props.player);
64
69
  * player.sendMessage(props.message);
70
+ * methods.greet(); // ctx auto-injected!
65
71
  * process.emit("onSuccess", true); // Typed!
66
72
  * }
67
73
  * });
68
74
  */
75
+ /**
76
+ * Define a kustom script with type safety and strongly-typed methods.
77
+ *
78
+ * Overload 1: Script WITH methods — M is inferred from the methods object,
79
+ * providing full autocomplete and type safety for ctx.methods.
80
+ */
81
+ export declare function defineScript<P extends PropSchema, E extends EventSchema, M extends MethodMap<P, E>>(config: ScriptConfigWithMethods<P, E, M>): ExportedScript<P, E>;
82
+ /**
83
+ * Define a kustom script with type safety.
84
+ *
85
+ * Overload 2: Script WITHOUT methods — simpler signature, backward compatible.
86
+ */
69
87
  export declare function defineScript<P extends PropSchema = Record<string, never>, E extends EventSchema = EmptyEventSchema>(config: ScriptConfig<P, E>): ExportedScript<P, E>;
70
88
  /**
71
89
  * Define a custom block with type safety.
@@ -113,4 +131,4 @@ export declare function defineBlock(config: BlockConfig): BlockDefinition;
113
131
  * });
114
132
  */
115
133
  export declare function defineItem(config: ItemConfig): ItemDefinition;
116
- export type { ScriptConfig, ScriptDefinition, ExportedScript, ScriptContext, PropSchema, PropAPI, InferProps, PropsAPI, EventSchema, EmptyEventSchema, InferEventPayload, ScriptModule, ScriptProcess, TypedScriptProcess, InferScriptModule, McModuleAPI, TypedMcModuleAPI, ProcessExecutorAPI, CameraAPI, SessionAPI, ChatGUIAPI, CommandManager, BetterModelEntityAPI, ShaperAPI, ItemAPI, DefinitionBuilder, ScriptablePlayer, BlockConfig, BlockDefinition, BlockClickEvent, BlockBreakEvent, BlockPlaceEvent, ItemConfig, ItemDefinition, ScreenConstructor, ContainerConstructor, ElementFactory, ScriptableLocationFactory, CellStateUtils, } from './types/index.js';
134
+ export type { ScriptConfig, ScriptDefinition, ExportedScript, BaseScriptContext, ScriptContext, PropSchema, PropAPI, InferProps, PropsAPI, WrappedMethods, MethodMap, EventSchema, EmptyEventSchema, InferEventPayload, ScriptModule, ScriptProcess, TypedScriptProcess, InferScriptModule, McModuleAPI, TypedMcModuleAPI, ProcessExecutorAPI, CameraAPI, SessionAPI, ChatGUIAPI, CommandManager, BetterModelEntityAPI, ShaperAPI, ItemAPI, DefinitionBuilder, ScriptablePlayer, BlockConfig, BlockDefinition, BlockClickEvent, BlockBreakEvent, BlockPlaceEvent, ItemConfig, ItemDefinition, ScreenConstructor, ContainerConstructor, ElementFactory, ScriptableLocationFactory, CellStateUtils, } from './types/index.js';
package/dist/runtime.js CHANGED
@@ -9,36 +9,11 @@
9
9
  */
10
10
  // Import globals for side effects - makes console, setTimeout, etc. available
11
11
  import './types/globals.js';
12
- /**
13
- * Define a kustom script with type safety.
14
- *
15
- * This is a type-only helper that returns its input unchanged.
16
- * The Java ProcessManager extracts the `run` function and calls it
17
- * with the ScriptContext.
18
- *
19
- * @example
20
- * import { defineScript, Props } from 'kustom-mc';
21
- *
22
- * export default defineScript({
23
- * props: {
24
- * player: Props.String(),
25
- * message: Props.String("Hello!"),
26
- * },
27
- * events: {
28
- * onSuccess: Props.Boolean(),
29
- * onError: Props.String(),
30
- * },
31
- * run({ process, props }) {
32
- * const player = process.getPlayer(props.player);
33
- * player.sendMessage(props.message);
34
- * process.emit("onSuccess", true); // Typed!
35
- * }
36
- * });
37
- */
12
+ // Implementation
38
13
  export function defineScript(config) {
39
14
  // Simply return the config - the Java side will handle execution
40
15
  // The return type is ExportedScript which combines:
41
- // - Definition metadata (props, events, __type) for the Java runtime
16
+ // - Definition metadata (props, events, methods, __type) for the Java runtime
42
17
  // - ScriptModule interface with .run(props) method for TypeScript imports
43
18
  return config;
44
19
  }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Debug: Check what M resolves to
3
+ */
4
+ export {};
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Debug: Check what M resolves to
3
+ */
4
+ import { defineScript } from '../../runtime.js';
5
+ const Props = {
6
+ String: (defaultValue) => ({ __brand: 'StringPropAPI', defaultValue }),
7
+ };
8
+ const result = defineScript({
9
+ props: {
10
+ message: Props.String("Hello!"),
11
+ },
12
+ methods: {
13
+ toto({ executor }) {
14
+ executor.asPlayer()?.sendMessage("toto!");
15
+ },
16
+ },
17
+ run({ methods }) {
18
+ // Let's see what type methods is
19
+ const m = methods;
20
+ const _check = m;
21
+ console.log(_check);
22
+ },
23
+ });
24
+ const _r = result;
25
+ console.log(_r);
@@ -0,0 +1,27 @@
1
+ /**
2
+ * DX test for strongly-typed defineScript methods.
3
+ *
4
+ * This simulates the EXACT user experience — calling defineScript()
5
+ * with methods and verifying autocomplete/type safety.
6
+ *
7
+ * Run: npx tsc --noEmit
8
+ * If this file compiles, the DX is correct.
9
+ */
10
+ declare const _default: import("../index.js").ExportedScript<{
11
+ message: {
12
+ __brand: "StringPropAPI";
13
+ defaultValue: string | undefined;
14
+ };
15
+ count: {
16
+ __brand: "NumberPropAPI";
17
+ defaultValue: number | undefined;
18
+ };
19
+ }, import("../index.js").EventSchema>;
20
+ export default _default;
21
+ export declare const noMethodsScript: import("../index.js").ExportedScript<{
22
+ name: {
23
+ __brand: "StringPropAPI";
24
+ defaultValue: string | undefined;
25
+ };
26
+ }, import("../index.js").EmptyEventSchema>;
27
+ export declare const bareScript: import("../index.js").ExportedScript<Record<string, never>, import("../index.js").EmptyEventSchema>;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * DX test for strongly-typed defineScript methods.
3
+ *
4
+ * This simulates the EXACT user experience — calling defineScript()
5
+ * with methods and verifying autocomplete/type safety.
6
+ *
7
+ * Run: npx tsc --noEmit
8
+ * If this file compiles, the DX is correct.
9
+ */
10
+ import { defineScript } from '../../runtime.js';
11
+ // Simulate Props (these are runtime-injected, so we cast)
12
+ const Props = {
13
+ String: (defaultValue) => ({ __brand: 'StringPropAPI', defaultValue }),
14
+ Number: (defaultValue) => ({ __brand: 'NumberPropAPI', defaultValue }),
15
+ Boolean: (defaultValue) => ({ __brand: 'BooleanPropAPI', defaultValue }),
16
+ };
17
+ // ============================================
18
+ // Test: Full DX with methods
19
+ // ============================================
20
+ export default defineScript({
21
+ props: {
22
+ message: Props.String("Hello!"),
23
+ count: Props.Number(5),
24
+ },
25
+ methods: {
26
+ toto({ executor }) {
27
+ // ✅ executor is typed as ProcessExecutorAPI
28
+ executor.asPlayer()?.sendMessage("toto!");
29
+ },
30
+ greet({ executor, props }) {
31
+ // ✅ props is typed: props.message is string, props.count is number
32
+ const _msg = props.message;
33
+ const _cnt = props.count;
34
+ // Note: methods receive BaseScriptContext (no `methods` field)
35
+ // to avoid circular generic inference. Only run() gets methods.
36
+ executor.asPlayer()?.sendMessage("greet!");
37
+ },
38
+ doSomething({ executor }) {
39
+ // ✅ Methods receive BaseScriptContext with all subsystems
40
+ executor.asPlayer()?.sendMessage("doSomething!");
41
+ },
42
+ },
43
+ run({ executor, props, methods }) {
44
+ // ✅ props is typed
45
+ const _msg = props.message;
46
+ const _cnt = props.count;
47
+ // ✅ methods are typed with autocomplete
48
+ methods.toto();
49
+ methods.greet();
50
+ methods.doSomething();
51
+ // ✅ executor is typed
52
+ executor.asPlayer()?.sendMessage(props.message);
53
+ },
54
+ });
55
+ // ============================================
56
+ // Test: Script without methods (backward compat)
57
+ // ============================================
58
+ export const noMethodsScript = defineScript({
59
+ props: {
60
+ name: Props.String("World"),
61
+ },
62
+ run({ props }) {
63
+ // ✅ props is typed
64
+ const _name = props.name;
65
+ console.log(_name);
66
+ },
67
+ });
68
+ // ============================================
69
+ // Test: Script with no props and no methods
70
+ // ============================================
71
+ export const bareScript = defineScript({
72
+ run() {
73
+ // ✅ Compiles fine with no props/methods
74
+ console.log("bare script");
75
+ },
76
+ });
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Negative type test for strongly-typed defineScript methods.
3
+ *
4
+ * Uses @ts-expect-error to verify that invalid method calls
5
+ * produce type errors. If a @ts-expect-error line does NOT
6
+ * produce an error, tsc will report "Unused '@ts-expect-error' directive".
7
+ *
8
+ * Run: npx tsc --noEmit
9
+ */
10
+ declare const _default: import("../index.js").ExportedScript<{
11
+ message: {
12
+ __brand: "StringPropAPI";
13
+ defaultValue: string | undefined;
14
+ };
15
+ }, import("../index.js").EventSchema>;
16
+ export default _default;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Negative type test for strongly-typed defineScript methods.
3
+ *
4
+ * Uses @ts-expect-error to verify that invalid method calls
5
+ * produce type errors. If a @ts-expect-error line does NOT
6
+ * produce an error, tsc will report "Unused '@ts-expect-error' directive".
7
+ *
8
+ * Run: npx tsc --noEmit
9
+ */
10
+ import { defineScript } from '../../runtime.js';
11
+ const Props = {
12
+ String: (defaultValue) => ({ __brand: 'StringPropAPI', defaultValue }),
13
+ };
14
+ export default defineScript({
15
+ props: {
16
+ message: Props.String("Hello!"),
17
+ },
18
+ methods: {
19
+ toto({ executor }) {
20
+ executor.asPlayer()?.sendMessage("toto!");
21
+ },
22
+ greet({ executor }) {
23
+ // Methods receive BaseScriptContext — no `methods` field.
24
+ // This avoids circular generic inference.
25
+ executor.asPlayer()?.sendMessage("greet!");
26
+ },
27
+ },
28
+ run({ methods }) {
29
+ methods.toto(); // ✅ valid
30
+ methods.greet(); // ✅ valid
31
+ // @ts-expect-error — 'nope' does not exist on methods
32
+ methods.nope();
33
+ // @ts-expect-error — 'doesNotExist' does not exist on methods
34
+ methods.doesNotExist();
35
+ },
36
+ });
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Type-level test for strongly-typed defineScript methods.
3
+ *
4
+ * This file is NOT executed — it only verifies that TypeScript
5
+ * correctly infers method types and provides autocomplete.
6
+ *
7
+ * Run: npx tsc --noEmit
8
+ * If this file compiles, the types are correct.
9
+ */
10
+ export {};
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Type-level test for strongly-typed defineScript methods.
3
+ *
4
+ * This file is NOT executed — it only verifies that TypeScript
5
+ * correctly infers method types and provides autocomplete.
6
+ *
7
+ * Run: npx tsc --noEmit
8
+ * If this file compiles, the types are correct.
9
+ */
10
+ // These should be the stripped signatures:
11
+ const _test1 = {
12
+ greet: (name) => name, // ctx stripped, (name: string) => string
13
+ wave: () => { }, // ctx stripped, () => void
14
+ };
15
+ // Verify the config type accepts methods and that ctx.methods inside run() is strongly typed.
16
+ const _config = {
17
+ props: undefined,
18
+ methods: {
19
+ toto(ctx) {
20
+ ctx.executor.asPlayer()?.sendMessage("toto!");
21
+ },
22
+ greet(ctx) {
23
+ // Methods receive BaseScriptContext — no `methods` field
24
+ ctx.executor.asPlayer()?.sendMessage("greet!");
25
+ },
26
+ },
27
+ run(ctx) {
28
+ ctx.methods.toto();
29
+ ctx.methods.greet();
30
+ // ctx.props should be typed
31
+ const _msg = ctx.props.message;
32
+ const _cnt = ctx.props.count;
33
+ },
34
+ };
35
+ const _emptyCtx = {};
36
+ // Should be Record<string, never> — no methods
37
+ const _emptyWrapped = {};
38
+ console.log("Type tests passed (compile-time only)", _test1, _config, _emptyCtx, _emptyWrapped);
39
+ export {};
@@ -97,6 +97,22 @@ export type EmptyEventSchema = Record<string, never>;
97
97
  * Infer the event payload type from an EventSchema.
98
98
  */
99
99
  export type InferEventPayload<E extends EventSchema, K extends keyof E> = InferPropType<E[K]>;
100
+ /**
101
+ * Strips the first ctx parameter from each method, producing the type
102
+ * available as ctx.methods inside scripts.
103
+ *
104
+ * Given methods like `{ greet(ctx: ScriptContext, ...args): void }`,
105
+ * produces `{ greet(...args): void }` — ctx is auto-injected at runtime.
106
+ */
107
+ export type WrappedMethods<M> = {
108
+ [K in keyof M]: M[K] extends (ctx: never, ...args: infer A) => infer R ? (...args: A) => R : () => unknown;
109
+ };
110
+ /**
111
+ * Script configuration for defineScript() — without methods.
112
+ *
113
+ * @typeParam P - Props schema type
114
+ * @typeParam E - Events schema type
115
+ */
100
116
  export interface ScriptConfig<P extends PropSchema, E extends EventSchema = EmptyEventSchema> {
101
117
  /** Props schema with types and defaults */
102
118
  props?: P;
@@ -124,6 +140,47 @@ export interface ScriptConfig<P extends PropSchema, E extends EventSchema = Empt
124
140
  */
125
141
  run: (this: TypedMcModuleAPI<E>, ctx: ScriptContext<InferProps<P>, E>) => void;
126
142
  }
143
+ /**
144
+ * Script configuration for defineScript() — with strongly-typed methods.
145
+ *
146
+ * Uses a generic `M` to capture the literal shape of the methods object.
147
+ * Each method receives a `ScriptContext` with `WrappedMethods<M>` providing
148
+ * full autocomplete for `ctx.methods.*`.
149
+ *
150
+ * The `methods` field uses a mapped type over `keyof M` so TypeScript
151
+ * can infer `M` from the object literal keys, then constrains each
152
+ * method's signature.
153
+ *
154
+ * @typeParam P - Props schema type
155
+ * @typeParam E - Events schema type
156
+ * @typeParam M - Methods record type (inferred from the methods object literal)
157
+ */
158
+ /** Constraint for the methods record — each method receives BaseScriptContext */
159
+ export type MethodMap<P extends PropSchema, E extends EventSchema> = Record<string, (ctx: BaseScriptContext<InferProps<P>, E>, ...args: unknown[]) => unknown>;
160
+ export type ScriptConfigWithMethods<P extends PropSchema, E extends EventSchema, M extends MethodMap<P, E>> = {
161
+ props?: P;
162
+ events?: E;
163
+ /**
164
+ * Named methods that can be called externally via `process.call("methodName", ...args)`
165
+ * or internally via `ctx.methods.methodName()`.
166
+ *
167
+ * Each method receives `(ctx: BaseScriptContext, ...extraArgs)` — a context without `methods`
168
+ * to avoid circular generic inference. At runtime, Java injects the full context.
169
+ *
170
+ * When called via `ctx.methods` in `run()`, the `ctx` parameter is auto-injected.
171
+ *
172
+ * @example
173
+ * ```typescript
174
+ * methods: {
175
+ * greet({ executor }) {
176
+ * executor.asPlayer()?.sendMessage("Hello!");
177
+ * },
178
+ * }
179
+ * ```
180
+ */
181
+ methods: M;
182
+ run: (this: TypedMcModuleAPI<E>, ctx: ScriptContext<InferProps<P>, E, WrappedMethods<M>>) => void;
183
+ };
127
184
  export interface ScriptDefinition<P extends PropSchema, E extends EventSchema = EmptyEventSchema> {
128
185
  props?: P;
129
186
  events?: E;
@@ -260,10 +317,14 @@ export interface ItemDefinition extends ItemConfig {
260
317
  readonly __type: "item";
261
318
  }
262
319
  /**
263
- * Context passed to script run function.
264
- * Contains all APIs and subsystems available to scripts.
320
+ * Base context passed to method functions.
321
+ * Same as ScriptContext but without `methods` to avoid circular generic inference.
322
+ * At runtime, Java actually injects the full context including methods.
323
+ *
324
+ * @typeParam P - Inferred props type
325
+ * @typeParam E - Event schema type
265
326
  */
266
- export interface ScriptContext<P = Record<string, unknown>, E extends EventSchema = EmptyEventSchema> {
327
+ export interface BaseScriptContext<P = Record<string, unknown>, E extends EventSchema = EmptyEventSchema> {
267
328
  /** Process API for script management (typed for events if declared) */
268
329
  process: TypedMcModuleAPI<E>;
269
330
  /** Validated props */
@@ -289,6 +350,33 @@ export interface ScriptContext<P = Record<string, unknown>, E extends EventSchem
289
350
  /** Persistent storage system */
290
351
  storage: StorageAPI;
291
352
  }
353
+ /**
354
+ * Full context passed to the script `run()` function.
355
+ * Extends BaseScriptContext with strongly-typed `methods`.
356
+ *
357
+ * @typeParam P - Inferred props type
358
+ * @typeParam E - Event schema type
359
+ * @typeParam Methods - Strongly-typed methods map (auto-inferred from defineScript)
360
+ */
361
+ export interface ScriptContext<P = Record<string, unknown>, E extends EventSchema = EmptyEventSchema, Methods = Record<string, never>> extends BaseScriptContext<P, E> {
362
+ /**
363
+ * Callable methods defined in defineScript({ methods: { ... } }).
364
+ * These are wrapped so `ctx` is auto-injected — call them directly without passing ctx.
365
+ *
366
+ * Fully typed: autocomplete shows only the methods you defined,
367
+ * and calling a non-existent method is a type error.
368
+ *
369
+ * @example
370
+ * ```typescript
371
+ * run({ methods }) {
372
+ * methods.greet(); // ✅ ctx is auto-injected, autocomplete works
373
+ * methods.farewell("arg"); // ✅ extra args passed through
374
+ * methods.nope(); // ❌ type error — not defined
375
+ * }
376
+ * ```
377
+ */
378
+ methods: Methods;
379
+ }
292
380
  export interface PropSchema {
293
381
  [key: string]: PropAPI;
294
382
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kustom-mc",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "CLI and type library for kustompack development",
5
5
  "type": "module",
6
6
  "main": "dist/runtime.js",