@weaver-wow/cli 1.0.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 ADDED
@@ -0,0 +1,28 @@
1
+ # Weaver CLI
2
+
3
+ The developer toolchain for Weaver addons.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g @weaver/cli
9
+ ```
10
+
11
+ ## Getting Started
12
+
13
+ Initialize a new Weaver project:
14
+
15
+ ```bash
16
+ wvr init
17
+ ```
18
+
19
+ ## Commands
20
+
21
+ - `wvr init`: Scaffolds a new Weaver project in the current directory.
22
+ - `wvr build`: Compiles your TypeScript/JSX into a valid WoW addon.
23
+ - `wvr dev`: Watches for changes and rebuilds automatically.
24
+ - `wvr link`: Links your project to the WoW AddOns directory.
25
+
26
+ ## License
27
+
28
+ MIT
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@weaver-wow/cli",
3
+ "version": "1.0.0",
4
+ "description": "Command line toolchain for building Weaver addons",
5
+ "type": "module",
6
+ "bin": {
7
+ "wvr": "src/cli/index.ts"
8
+ },
9
+ "files": [
10
+ "src",
11
+ "README.md"
12
+ ],
13
+ "license": "MIT",
14
+ "author": "Mike Eling <mike.eling97@gmail.com>",
15
+ "dependencies": {
16
+ "@clack/prompts": "^1.0.0",
17
+ "commander": "^14.0.3",
18
+ "picocolors": "^1.1.1",
19
+ "zod": "^4.3.6",
20
+ "@weaver-wow/core": "^1.0.0",
21
+ "typescript-to-lua": "^1.33.2"
22
+ },
23
+ "scripts": {
24
+ "build": "echo 'Building weaver CLI...'"
25
+ },
26
+ "publishConfig": {
27
+ "access": "public"
28
+ }
29
+ }
@@ -0,0 +1,280 @@
1
+ import { $ } from "bun";
2
+ import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync, unlinkSync } from "node:fs";
3
+ import { dirname, join, relative } from "node:path";
4
+ import { WeaverConfig } from "../schema";
5
+ import picocolors from "picocolors";
6
+ import { createRequire } from "node:module";
7
+
8
+ const require = createRequire(import.meta.url);
9
+
10
+ const GENERATED_TSTL_CONFIG = ".tstl.build.json";
11
+
12
+ /**
13
+ * Build the addon project.
14
+ *
15
+ * The builder:
16
+ * 1. Reads the base tsconfig.tstl.json template
17
+ * 2. Generates a temporary build config with the correct entry point and include
18
+ * 3. Runs TSTL to compile TypeScript → Lua
19
+ * 4. Post-processes the Lua output (namespace wrapping)
20
+ * 5. Generates the .toc manifest
21
+ * 6. Scans for assets
22
+ */
23
+ export async function buildProject(config: WeaverConfig | null, silent = false) {
24
+ if (!silent) console.log("Building...");
25
+
26
+ let pkg: any = {};
27
+ if (existsSync("package.json")) {
28
+ try {
29
+ pkg = JSON.parse(readFileSync("package.json", "utf-8"));
30
+ } catch {}
31
+ }
32
+
33
+ const addonName = config?.addonName || (pkg.name ? pkg.name.charAt(0).toUpperCase() + pkg.name.slice(1) : "MyAddon");
34
+ const interfaceVersion = config?.interface || "110000";
35
+ const author = pkg.author || "Unknown";
36
+ const version = pkg.version || "1.0.0";
37
+ const description = pkg.description || "Generated by Weaver";
38
+ // Pre-flight checks
39
+ if (!existsSync("tsconfig.json") && !existsSync("tsconfig.tstl.json")) {
40
+ throw new Error(`Missing ${picocolors.bold("tsconfig.json")}. \n Run ${picocolors.cyan("wvr init")} to set up your project structure.`);
41
+ }
42
+
43
+ // Discovery of entry point if not specified
44
+ let entryPoint = config?.entry;
45
+ if (!entryPoint) {
46
+ const potentialEntries = ["src/App.tsx", "src/index.tsx", "src/App.ts", "src/index.ts"];
47
+ for (const pe of potentialEntries) {
48
+ if (existsSync(pe)) {
49
+ entryPoint = pe;
50
+ break;
51
+ }
52
+ }
53
+ }
54
+
55
+ // Fallback to default if still not found
56
+ const finalEntryPoint = entryPoint || "src/App.tsx";
57
+
58
+ if (!existsSync(finalEntryPoint)) {
59
+ const others = ["src/App.tsx", "src/index.tsx", "src/App.ts", "src/index.ts"].filter(pe => pe !== finalEntryPoint && existsSync(pe));
60
+ let suggestion = "";
61
+ if (others.length > 0) {
62
+ suggestion = `\n ${picocolors.yellow("Suggestion:")} Found ${picocolors.cyan(others[0])}. Did you mean to use that?`;
63
+ }
64
+ throw new Error(`Entry point not found: ${picocolors.red(finalEntryPoint)}${suggestion}\n Check your ${picocolors.bold("weaver.config.ts")} or ensure the source file exists.`);
65
+ }
66
+
67
+ if (!existsSync("dist")) mkdirSync("dist");
68
+
69
+ // 1. Generate build-time TSTL config
70
+ const buildConfig = generateBuildConfig(finalEntryPoint);
71
+
72
+ try {
73
+ // 2. Run TSTL with the generated config
74
+ if (silent) {
75
+ await $`bun x tstl -p ${GENERATED_TSTL_CONFIG}`.quiet();
76
+ } else {
77
+ await $`bun x tstl -p ${GENERATED_TSTL_CONFIG}`;
78
+ }
79
+ } catch (e: any) {
80
+ let rawError = (e.stdout?.toString() || "") + (e.stderr?.toString() || "") + (e.message || "");
81
+
82
+ // Parse TSTL/TS errors for pretty printing
83
+ const lines = rawError.split('\n');
84
+ const formattedErrors: string[] = [];
85
+
86
+ for (const line of lines) {
87
+ // Match pattern: path/to/file.tsx(line,col): error TSXXXX: message
88
+ const match = line.match(/^(.+)\((\d+),(\d+)\): error (TS\d+): (.+)$/);
89
+ if (match) {
90
+ const [_, file, lineNum, colNum, code, msg] = match;
91
+ let processedMsg = msg;
92
+
93
+ if (msg.includes("Cannot find module '@weaver-wow/core'")) {
94
+ processedMsg += `\n ${picocolors.yellow("Tip:")} Run ${picocolors.bold("bun install")} to link the Weaver runtime.`;
95
+ }
96
+
97
+ formattedErrors.push(
98
+ ` ${picocolors.dim(file)}:${picocolors.cyan(lineNum)}:${picocolors.cyan(colNum)}\n` +
99
+ ` ${picocolors.red("✖")} ${picocolors.white(processedMsg)} ${picocolors.dim(`[${code}]`)}`
100
+ );
101
+ } else if (line.includes("error TSTL")) {
102
+ let msg = line.trim();
103
+ let tip = "";
104
+ if (msg.includes("'@weaver-wow/core'")) {
105
+ tip = `\n ${picocolors.yellow("Tip:")} Run ${picocolors.bold("bun install")} or ensure Weaver is in your node_modules.`;
106
+ }
107
+ formattedErrors.push(` ${picocolors.red("✖")} ${picocolors.white(msg)}${tip}`);
108
+ }
109
+ }
110
+
111
+ if (formattedErrors.length > 0) {
112
+ throw new Error(`Compilation failed:\n\n${formattedErrors.join('\n\n')}`);
113
+ }
114
+
115
+ throw new Error(`Compilation failed with unknown error:\n${rawError}`);
116
+ } finally {
117
+ // Always clean up the temporary config
118
+ // cleanupBuildConfig();
119
+ }
120
+
121
+ // 3. Post-Process — wrap in addon namespace
122
+ const bundlePath = "./dist/App.lua";
123
+ let luaContent = "";
124
+
125
+ if (existsSync(bundlePath)) {
126
+ luaContent = readFileSync(bundlePath, "utf-8");
127
+ } else {
128
+ throw new Error(`Could not find generated Lua file (${bundlePath})`);
129
+ }
130
+
131
+ const wrappedLua = `
132
+ local addonName, ns = ...
133
+ -- [WEAVER RUNTIME STUB]
134
+ ${luaContent}
135
+ `.trim();
136
+
137
+ writeFileSync("./dist/Core.lua", wrappedLua);
138
+
139
+ // Remove intermediate bundle (Core.lua is the final artifact)
140
+ try { unlinkSync(bundlePath); } catch {}
141
+
142
+ // 4. Generate TOC
143
+ const tocContent = `## Interface: ${interfaceVersion}
144
+ ## Title: ${addonName}
145
+ ## Notes: ${description}
146
+ ## Author: ${author}
147
+ ## Version: ${version}
148
+
149
+ Core.lua
150
+ `;
151
+ writeFileSync(`./dist/${addonName}.toc`, tocContent);
152
+
153
+ // 5. Assets
154
+ let assetCount = 0;
155
+ if (existsSync("assets")) {
156
+ const files = readdirSync("assets");
157
+ for (const file of files) {
158
+ if (file.endsWith(".png")) {
159
+ assetCount++;
160
+ }
161
+ }
162
+ }
163
+
164
+ return { addonName, assetCount };
165
+ }
166
+
167
+ /**
168
+ * Generate a temporary TSTL config that extends the base template
169
+ * with the correct entry point and include paths.
170
+ */
171
+ function generateBuildConfig(entryPoint: string): string {
172
+ console.log("BUILDER UPDATED V2");
173
+ // Read the base template
174
+ const baseConfigPath = existsSync("tsconfig.tstl.json") ? "tsconfig.tstl.json" : "tsconfig.json";
175
+ const baseConfig = JSON.parse(readFileSync(baseConfigPath, "utf-8"));
176
+
177
+ // Determine the entry's directory for include patterns
178
+ const entryDir = dirname(entryPoint);
179
+
180
+ // Extend include with the entry point
181
+ const include = [
182
+ ...(baseConfig.include || []),
183
+ entryPoint,
184
+ ];
185
+
186
+ // Build the final config
187
+ // Heuristic:
188
+ // 1. If CWD has package.json with "weaver" dependency, use node_modules (Consumer Project)
189
+ // 2. Else if we are in the monorepo structure, use relative source paths (Monorepo specific)
190
+
191
+ let isConsumerProject = false;
192
+ console.log("CWD:", process.cwd());
193
+ if (existsSync("package.json")) {
194
+ try {
195
+ const pkgC = readFileSync("package.json", "utf-8");
196
+ const pkg = JSON.parse(pkgC);
197
+ console.log("Found package.json with deps:", Object.keys(pkg.dependencies || {}));
198
+ if ((pkg.dependencies && pkg.dependencies["@weaver-wow/core"]) || (pkg.devDependencies && pkg.devDependencies["@weaver-wow/core"])) {
199
+ isConsumerProject = true;
200
+ }
201
+ } catch (e) { console.log("Error reading pkg", e); }
202
+ }
203
+ console.log("isConsumerProject:", isConsumerProject);
204
+
205
+ let weaverPath: string;
206
+ let jsxRuntimePath: string;
207
+ let includePaths: string[] = [];
208
+
209
+ // Check if we are running from source (monorepo) or installed package
210
+ const absoluteRootDir = join(dirname(import.meta.url.replace("file:///", "")), "..", "..", "..", "..");
211
+
212
+ if (isConsumerProject) {
213
+ // We are in a consumer project, resolve from node_modules
214
+ // The project's node_modules should have weaver installed
215
+ weaverPath = "node_modules/@weaver-wow/core/src/index.ts";
216
+ jsxRuntimePath = "node_modules/@weaver-wow/core/src/types/react-jsx-runtime.d.ts";
217
+ includePaths = [
218
+ "node_modules/@weaver-wow/core/src/**/*.ts",
219
+ "node_modules/@weaver-wow/core/src/**/*.tsx"
220
+ ];
221
+ } else if (existsSync(join(absoluteRootDir, "packages", "weaver"))) {
222
+ // We are in the monorepo (e.g. running sandbox tests)
223
+ const rootDir = relative(process.cwd(), absoluteRootDir) || ".";
224
+ weaverPath = join(rootDir, "packages/weaver/src/index.ts");
225
+ jsxRuntimePath = join(rootDir, "packages/weaver/src/types/react-jsx-runtime.d.ts");
226
+ includePaths = [
227
+ join(rootDir, "packages/weaver/src/**/*.ts"),
228
+ join(rootDir, "packages/weaver/src/**/*.tsx"),
229
+ ];
230
+ } else {
231
+ // Fallback: linked but not detecting monorepo (should be covered by isConsumerProject if linked correctly)
232
+ // or strange install location. Default to node_modules
233
+ weaverPath = "node_modules/@weaver-wow/core/src/index.ts";
234
+ jsxRuntimePath = "node_modules/@weaver-wow/core/src/types/react-jsx-runtime.d.ts";
235
+ includePaths = [
236
+ "node_modules/@weaver-wow/core/src/**/*.ts",
237
+ "node_modules/@weaver-wow/core/src/**/*.tsx"
238
+ ];
239
+ }
240
+
241
+ const buildConfig = {
242
+ ...baseConfig,
243
+ compilerOptions: {
244
+ ...baseConfig.compilerOptions,
245
+ preserveSymlinks: true,
246
+ paths: {
247
+ ...(baseConfig.compilerOptions?.paths || {}),
248
+ "@weaver-wow/core": [weaverPath],
249
+ "react/jsx-runtime": [jsxRuntimePath]
250
+ }
251
+ },
252
+ tstl: {
253
+ ...baseConfig.tstl,
254
+ luaBundleEntry: entryPoint,
255
+ },
256
+ include: [
257
+ ...include,
258
+ ...includePaths
259
+ ],
260
+ };
261
+
262
+ // Write the temporary config
263
+ if (process.env.DEBUG) console.log("Generated config:", JSON.stringify(buildConfig, null, 2));
264
+ writeFileSync(GENERATED_TSTL_CONFIG, JSON.stringify(buildConfig, null, 2));
265
+
266
+ return GENERATED_TSTL_CONFIG;
267
+ }
268
+
269
+ /**
270
+ * Clean up the temporary build config file.
271
+ */
272
+ function cleanupBuildConfig(): void {
273
+ try {
274
+ if (existsSync(GENERATED_TSTL_CONFIG)) {
275
+ unlinkSync(GENERATED_TSTL_CONFIG);
276
+ }
277
+ } catch {
278
+ // Non-critical — don't block build on cleanup failure
279
+ }
280
+ }
@@ -0,0 +1,34 @@
1
+
2
+ import { intro, outro, spinner } from "@clack/prompts";
3
+ import picocolors from "picocolors";
4
+ import { loadConfig } from "../utils/config";
5
+ import { buildProject } from "../builder";
6
+
7
+ export async function buildCommand() {
8
+ intro(picocolors.bgBlue(" 🧵 Weaver Build "));
9
+
10
+ const s = spinner();
11
+ s.start("Reading configuration...");
12
+
13
+ const config = await loadConfig();
14
+
15
+ s.message(`Building configured project...`);
16
+
17
+ try {
18
+ const result = await buildProject(config, true);
19
+ s.stop(picocolors.green("Build complete!"));
20
+ outro(`Built ${picocolors.cyan(result.addonName)} successfully to ${picocolors.bold("dist/")}`);
21
+ } catch (e: any) {
22
+ s.stop(picocolors.red("Build failed"));
23
+
24
+ console.log("");
25
+ const message = e.message || String(e);
26
+ message.split('\n').forEach(line => {
27
+ console.log(` ${line}`);
28
+ });
29
+ console.log("");
30
+
31
+ outro(picocolors.red("Build failed. Fix the issues above and try again."));
32
+ process.exit(1);
33
+ }
34
+ }
@@ -0,0 +1,92 @@
1
+
2
+ import { watch } from "node:fs";
3
+ import { intro, outro, spinner } from "@clack/prompts";
4
+ import picocolors from "picocolors";
5
+ import { loadConfig } from "../utils/config";
6
+ import { buildProject } from "../builder";
7
+ import { join } from "node:path";
8
+
9
+ export async function devCommand() {
10
+ intro(picocolors.bgBlue(" 🧵 Weaver Dev "));
11
+
12
+ const s = spinner();
13
+ s.start("Starting development server...");
14
+
15
+ const config = await loadConfig();
16
+
17
+ // Initial build
18
+ try {
19
+ await buildProject(config, true);
20
+ s.stop(picocolors.green("Initial build complete!"));
21
+ } catch (e) {
22
+ s.stop(picocolors.red("Initial build failed"));
23
+ console.error(e);
24
+ // Continue watching even if build fails? Yes, to fix errors.
25
+ }
26
+
27
+ console.log(picocolors.cyan(`Watching for changes in ${picocolors.bold("src/")}...`));
28
+
29
+ let isBuilding = false;
30
+
31
+ // Recursive watch on src/
32
+ // Debounce simply by ignoring triggers while building?
33
+ watch("src", { recursive: true }, async (event, filename) => {
34
+ if (filename && (filename.endsWith(".ts") || filename.endsWith(".tsx"))) {
35
+ if (isBuilding) return;
36
+ isBuilding = true;
37
+
38
+ console.log(picocolors.dim(`File changed: ${filename}`));
39
+ const rebuildSpinner = spinner();
40
+ rebuildSpinner.start("Rebuilding...");
41
+
42
+ try {
43
+ // Determine if we need full build or partial?
44
+ // For MVP, full build via buildProject.
45
+ const start = performance.now();
46
+ await buildProject(config, true);
47
+ const end = performance.now();
48
+ rebuildSpinner.stop(picocolors.green(`Rebuilt in ${(end - start).toFixed(0)}ms`));
49
+ } catch (e) {
50
+ rebuildSpinner.stop(picocolors.red("Rebuild failed"));
51
+ if (e instanceof Error) console.error(e.message);
52
+ } finally {
53
+ isBuilding = false;
54
+ }
55
+ }
56
+ });
57
+
58
+ // Also watch sandbox if present?
59
+ // Since we moved sandbox to root, we should watch it too if configured.
60
+ // However, sandbox usually lives outside src in this setup.
61
+ // Let's watch 'sandbox' too if it exists.
62
+ // Or watch root/sandbox.
63
+
64
+ // Using fs.watch implies we need a process keep-alive?
65
+ // Node.js keeps process alive as long as watcher is active.
66
+
67
+ // Watch sandbox
68
+ try {
69
+ watch("sandbox", { recursive: true }, async (event, filename) => {
70
+ if (filename && (filename.endsWith(".ts") || filename.endsWith(".tsx"))) {
71
+ if (isBuilding) return;
72
+ isBuilding = true;
73
+ console.log(picocolors.dim(`Sandbox file changed: ${filename}`));
74
+ const rb = spinner();
75
+ rb.start("Rebuilding...");
76
+ try {
77
+ const start = performance.now();
78
+ await buildProject(config, true);
79
+ const end = performance.now();
80
+ rb.stop(picocolors.green(`Rebuilt (sandbox) in ${(end - start).toFixed(0)}ms`));
81
+ } catch (e) {
82
+ rb.stop(picocolors.red("Rebuild failed"));
83
+ if (e instanceof Error) console.error(e.message);
84
+ } finally {
85
+ isBuilding = false;
86
+ }
87
+ }
88
+ });
89
+ } catch {
90
+ // sandbox might not exist or verify failed
91
+ }
92
+ }
@@ -0,0 +1,228 @@
1
+
2
+ import { intro, outro, text, select, confirm, spinner } from "@clack/prompts";
3
+ import { findWoWPath } from "../utils/wow-path";
4
+ import { writeFileSync, existsSync, mkdirSync } from "node:fs";
5
+ import picocolors from "picocolors";
6
+
7
+ export async function initCommand(options: any) {
8
+ intro(picocolors.bgBlue(" 🧵 Weaver Init "));
9
+
10
+ // 1. Metadata Discovery
11
+ let defaultName = "MyAddon";
12
+ let defaultAuthor = "Me";
13
+
14
+ if (existsSync("package.json")) {
15
+ try {
16
+ const pkg = JSON.parse(await Bun.file("package.json").text());
17
+ if (pkg.name) defaultName = pkg.name;
18
+ if (pkg.author) defaultAuthor = pkg.author;
19
+ } catch {}
20
+ }
21
+
22
+ let wowPath = "";
23
+ let addonName = defaultName;
24
+ let flavor = "_retail_";
25
+
26
+ if (options.yes) {
27
+ // NON-INTERACTIVE MODE
28
+ wowPath = (await findWoWPath()) || "C:\\Program Files (x86)\\World of Warcraft";
29
+ } else {
30
+ // INTERACTIVE MODE
31
+ // 2. WoW Path Discovery
32
+ const path = await findWoWPath();
33
+ wowPath = path || "";
34
+
35
+ if (path) {
36
+ const confirmPath = await confirm({
37
+ message: `Found WoW at ${picocolors.cyan(path)}. Use this?`,
38
+ initialValue: true
39
+ });
40
+
41
+ if (!confirmPath) {
42
+ const manualPath = await text({
43
+ message: "Enter the path to your World of Warcraft installation:",
44
+ placeholder: "C:\\Program Files (x86)\\World of Warcraft",
45
+ validate(value) {
46
+ if (value.length === 0) return "Path is required";
47
+ }
48
+ });
49
+ if (typeof manualPath === 'symbol') { process.exit(0); }
50
+ wowPath = manualPath;
51
+ }
52
+ } else {
53
+ const manualPath = await text({
54
+ message: "Could not auto-detect WoW. Enter the path manually:",
55
+ placeholder: "C:\\Program Files (x86)\\World of Warcraft",
56
+ validate(value) {
57
+ if (value.length === 0) return "Path is required";
58
+ }
59
+ });
60
+ if (typeof manualPath === 'symbol') { process.exit(0); }
61
+ wowPath = manualPath;
62
+ }
63
+
64
+ // 3. Project Configuration
65
+ const addonNameInput = await text({
66
+ message: "Addon Name (used for folder name and TOC):",
67
+ placeholder: defaultName,
68
+ validate(value) {
69
+ if (value && !/^[a-zA-Z0-9]+$/.test(value)) return "Only alphanumeric characters allowed";
70
+ }
71
+ });
72
+ if (typeof addonNameInput === 'symbol') { process.exit(0); }
73
+ addonName = (addonNameInput || defaultName) as string;
74
+
75
+ const flavorInput = await select({
76
+ message: "Target Game Version:",
77
+ options: [
78
+ { value: "_retail_", label: "Retail (The War Within)" },
79
+ { value: "_classic_", label: "Cataclysm Classic" },
80
+ { value: "_classic_era_", label: "Classic Era / Hardcore" },
81
+ ],
82
+ }) as string;
83
+ if (typeof flavorInput === 'symbol') { process.exit(0); }
84
+ flavor = flavorInput;
85
+ }
86
+
87
+ // Map flavor to default interface version
88
+ const interfaceMap: Record<string, string> = {
89
+ "_retail_": "110007",
90
+ "_classic_": "40400",
91
+ "_classic_era_": "11503",
92
+ };
93
+ const interfaceVersion = interfaceMap[flavor] || "110000";
94
+
95
+ const s = spinner();
96
+ s.start("Scaffolding your Weaver project...");
97
+
98
+ // 4. File Generation
99
+
100
+ // weaver.config.ts
101
+ const configContent = `
102
+ import type { WeaverConfig } from "@weaver-wow/core";
103
+
104
+ const config: WeaverConfig = {
105
+ addonName: "${addonName}",
106
+ flavor: "${flavor}",
107
+ interface: "${interfaceVersion}",
108
+ wowPath: String.raw\`${wowPath}\`,
109
+ };
110
+
111
+ export default config;
112
+ `.trim();
113
+ writeFileSync("weaver.config.ts", configContent);
114
+
115
+ // tsconfig.json (Standard TSTL + Weaver setup)
116
+ const tsconfigContent = `
117
+ {
118
+ "compilerOptions": {
119
+ "target": "ESNext",
120
+ "lib": ["ESNext"],
121
+ "moduleResolution": "Node",
122
+ "types": [],
123
+ "jsx": "react",
124
+ "jsxFactory": "createElement",
125
+ "strict": false,
126
+ "skipLibCheck": true,
127
+ "sourceMap": false,
128
+ "baseUrl": ".",
129
+ "paths": {
130
+ "react/jsx-runtime": ["node_modules/@weaver-wow/core/src/types/react-jsx-runtime.d.ts"]
131
+ }
132
+ },
133
+ "tstl": {
134
+ "luaTarget": "5.1",
135
+ "luaBundle": "dist/App.lua",
136
+ "noHeader": true
137
+ },
138
+ "include": ["src/**/*.tsx", "src/**/*.ts", "src/**/*.d.ts"]
139
+ }
140
+ `.trim();
141
+ if (!existsSync("tsconfig.json")) {
142
+ writeFileSync("tsconfig.json", tsconfigContent);
143
+ }
144
+
145
+ // src/App.tsx (Starter code)
146
+ if (!existsSync("src")) {
147
+ mkdirSync("src");
148
+ }
149
+
150
+ if (!existsSync("src/App.tsx") && !existsSync("src/index.tsx")) {
151
+ const appContent = `
152
+ import {
153
+ useState,
154
+ useEffect,
155
+ Frame,
156
+ Texture,
157
+ Button,
158
+ mount,
159
+ createElement
160
+ } from '@weaver-wow/core';
161
+
162
+ const ${addonName} = () => {
163
+ const [clicks, setClicks] = useState("clickCount", 0);
164
+
165
+ useEffect(() => {
166
+ print("${addonName} initialized!");
167
+ }, []);
168
+
169
+ return (
170
+ <Frame name="${addonName}Frame" size={[220, 100]} setPoint={["CENTER", 0, 0]}>
171
+ <Texture setAllPoints={true} color={[0, 0, 0, 0.6]} />
172
+ <Button
173
+ text={\`Clicks: \${clicks}\`}
174
+ onClick={() => setClicks(clicks + 1)}
175
+ />
176
+ </Frame>
177
+ );
178
+ };
179
+
180
+ mount("${addonName}", () => ${addonName}());
181
+ `.trim();
182
+ writeFileSync("src/App.tsx", appContent);
183
+ }
184
+
185
+ // package.json (if missing)
186
+ if (!existsSync("package.json")) {
187
+ const packageContent = {
188
+ name: addonName.toLowerCase(),
189
+ version: "0.1.0",
190
+ private: true,
191
+ type: "module",
192
+ scripts: {
193
+ "build": "bun x wvr build",
194
+ "dev": "bun x wvr dev",
195
+ "link": "bun x wvr link"
196
+ },
197
+ dependencies: {
198
+ "@weaver-wow/core": "^1.0.0"
199
+ },
200
+ devDependencies: {
201
+ "typescript-to-lua": "latest"
202
+ }
203
+ };
204
+ writeFileSync("package.json", JSON.stringify(packageContent, null, 2));
205
+ }
206
+
207
+ // .gitignore
208
+ const gitignoreContent = `
209
+ node_modules/
210
+ dist/
211
+ .tstl.build.json
212
+ *.log
213
+ `.trim();
214
+ if (!existsSync(".gitignore")) {
215
+ writeFileSync(".gitignore", gitignoreContent);
216
+ }
217
+
218
+ s.stop("Project initialized successfully!");
219
+
220
+ outro(`
221
+ ${picocolors.green("Project layout created:")}
222
+ - ${picocolors.cyan("src/App.tsx")} (Addon source)
223
+ - ${picocolors.cyan("weaver.config.ts")} (Addon metadata)
224
+ - ${picocolors.cyan("tsconfig.json")} (Compiler settings)
225
+
226
+ Run ${picocolors.bold("bun install")} and then ${picocolors.bold("bun run dev")} to start coding!
227
+ `);
228
+ }
@@ -0,0 +1,126 @@
1
+
2
+ import { symlinkSync, existsSync, rmSync, statSync } from "node:fs";
3
+ import { join, resolve } from "node:path";
4
+ import { intro, outro, spinner, confirm, text } from "@clack/prompts";
5
+ import picocolors from "picocolors";
6
+ import { loadConfig } from "../utils/config";
7
+ import { findWoWPath } from "../utils/wow-path";
8
+
9
+ export async function linkCommand() {
10
+ intro(picocolors.bgBlue(" 🧵 Weaver Link "));
11
+
12
+ const s = spinner();
13
+ s.start("Reading configuration...");
14
+
15
+ let config = await loadConfig();
16
+
17
+ // Determine Addon Name
18
+ let addonName = config?.addonName;
19
+ if (!addonName && existsSync("package.json")) {
20
+ try {
21
+ const pkg = JSON.parse(await Bun.file("package.json").text());
22
+ addonName = pkg.name ? pkg.name.charAt(0).toUpperCase() + pkg.name.slice(1) : "MyAddon";
23
+ } catch {}
24
+ }
25
+ addonName = addonName || "MyAddon";
26
+
27
+ // Determine WoW Path
28
+ let wowPath = config?.wowPath;
29
+ if (!wowPath) {
30
+ s.stop("Searching for WoW installation...");
31
+ wowPath = await findWoWPath();
32
+
33
+ if (!wowPath) {
34
+ const manualPath = await text({
35
+ message: "Could not auto-detect WoW. Enter path manually:",
36
+ placeholder: "C:\\Program Files (x86)\\World of Warcraft"
37
+ });
38
+ if (typeof manualPath === 'symbol') process.exit(0);
39
+ wowPath = manualPath as string;
40
+ } else {
41
+ const useFound = await confirm({
42
+ message: `Found WoW at ${picocolors.cyan(wowPath)}. Use this?`,
43
+ initialValue: true
44
+ });
45
+ if (!useFound) {
46
+ const manualPath = await text({
47
+ message: "Enter path manually:",
48
+ placeholder: "C:\\Program Files (x86)\\World of Warcraft"
49
+ });
50
+ if (typeof manualPath === 'symbol') process.exit(0);
51
+ wowPath = manualPath as string;
52
+ }
53
+ }
54
+ }
55
+
56
+ if (!wowPath) {
57
+ console.error(picocolors.red("WoW path is required."));
58
+ process.exit(1);
59
+ }
60
+
61
+ // Determine target directory (Interface/AddOns)
62
+ // Default to retail for now, or use config.flavor
63
+ const flavor = config?.flavor || "_retail_";
64
+ const addonsDir = join(wowPath, flavor, "Interface", "AddOns");
65
+
66
+ if (!existsSync(addonsDir)) {
67
+ console.error(picocolors.red(`Could not find AddOns directory at: ${addonsDir}`));
68
+ console.log(picocolors.yellow(`(Make sure you have run the game at least once to create folders)`));
69
+ process.exit(1);
70
+ }
71
+
72
+ const targetLinkPath = join(addonsDir, addonName);
73
+ const sourceDistPath = resolve("dist"); // Absolute path to dist
74
+
75
+ if (!existsSync(sourceDistPath)) {
76
+ console.error(picocolors.red("dist/ folder does not exist. Run 'bun wvr build' first."));
77
+ process.exit(1);
78
+ }
79
+
80
+ s.start(`Linking ${picocolors.cyan(addonName)} to WoW...`);
81
+
82
+ // Check if link already exists
83
+ if (existsSync(targetLinkPath)) {
84
+ const stats = statSync(targetLinkPath);
85
+ if (stats.isSymbolicLink()) {
86
+ s.stop("Link already exists.");
87
+ // Maybe check where it points?
88
+ outro("Addon is already linked.");
89
+ return;
90
+ } else {
91
+ // It's a real folder?
92
+ s.stop("Target exists as a real folder.");
93
+ const overwrite = await confirm({
94
+ message: `Folder ${targetLinkPath} already exists (not a link). Delete and link?`,
95
+ initialValue: false
96
+ });
97
+ if (!overwrite) {
98
+ outro("Aborted.");
99
+ return;
100
+ }
101
+ s.start("Removing existing folder...");
102
+ try {
103
+ rmSync(targetLinkPath, { recursive: true, force: true }); // Dangerous?
104
+ } catch (e) {
105
+ s.stop(picocolors.red("Failed to remove existing folder."));
106
+ console.error(e);
107
+ process.exit(1);
108
+ }
109
+ }
110
+ }
111
+
112
+ // Create Symlink
113
+ try {
114
+ // Use "junction" for Windows directory links without admin
115
+ const type = process.platform === "win32" ? "junction" : "dir";
116
+ symlinkSync(sourceDistPath, targetLinkPath, type);
117
+ } catch (e) {
118
+ s.stop(picocolors.red("Failed to create symlink"));
119
+ console.error("Error details:", e);
120
+ console.log(picocolors.yellow("Try running as Administrator if on Windows."));
121
+ process.exit(1);
122
+ }
123
+
124
+ s.stop(picocolors.green("Linked successfully!"));
125
+ outro(`Dev changes in ${picocolors.bold("dist/")} will now reflect in WoW immediately.`);
126
+ }
@@ -0,0 +1,31 @@
1
+ import { Command } from "commander";
2
+ import { initCommand } from "./commands/init";
3
+ import { buildCommand } from "./commands/build";
4
+ import { devCommand } from "./commands/dev";
5
+ import { linkCommand } from "./commands/link";
6
+
7
+ const program = new Command();
8
+
9
+ program
10
+ .name("wvr")
11
+ .description("Weaver Addon Toolchain")
12
+ .version("1.0.0");
13
+
14
+ program.command("init")
15
+ .description("Initialize a new Weaver project")
16
+ .option("-y, --yes", "Skip prompts and use defaults")
17
+ .action((options) => initCommand(options));
18
+
19
+ program.command("build")
20
+ .description("Build the addon for production")
21
+ .action(() => buildCommand());
22
+
23
+ program.command("dev")
24
+ .description("Start development server (watch mode)")
25
+ .action(() => devCommand());
26
+
27
+ program.command("link")
28
+ .description("Symlink dist folder to WoW AddOns")
29
+ .action(() => linkCommand());
30
+
31
+ program.parse(process.argv);
@@ -0,0 +1,16 @@
1
+
2
+ import { existsSync } from "node:fs";
3
+ import { WeaverConfig } from "../../schema";
4
+
5
+ export async function loadConfig(): Promise<WeaverConfig | null> {
6
+ if (existsSync("weaver.config.ts")) {
7
+ try {
8
+ const configPath = Bun.pathToFileURL("weaver.config.ts").href;
9
+ const configModule = await import(configPath);
10
+ return configModule.default;
11
+ } catch (e) {
12
+ console.warn("Failed to load weaver.config.ts:", e);
13
+ }
14
+ }
15
+ return null;
16
+ }
@@ -0,0 +1,44 @@
1
+
2
+ import { $ } from "bun";
3
+ import { existsSync } from "node:fs";
4
+ import { join } from "node:path";
5
+
6
+ export async function findWoWPath(): Promise<string | null> {
7
+ // 1. Try Registry (Windows only)
8
+ if (process.platform === "win32") {
9
+ try {
10
+ const result = await $`reg query "HKEY_LOCAL_MACHINE\\SOFTWARE\\WOW6432Node\\Blizzard Entertainment\\World of Warcraft" /v InstallPath`.text();
11
+ // Output looks like: " InstallPath REG_SZ C:\Program Files (x86)\World of Warcraft\"
12
+ const match = result.match(/REG_SZ\s+(.*)/);
13
+ if (match && match[1]) {
14
+ const path = match[1].trim();
15
+ if (existsSync(path)) return path;
16
+ }
17
+ } catch (e) {
18
+ // Registry key not found or error accessing reg command
19
+ }
20
+ }
21
+
22
+ // 2. Common Path Scan
23
+ const commonPaths = [
24
+ "C:\\Games\\World of Warcraft",
25
+ "D:\\Games\\World of Warcraft",
26
+ "E:\\Games\\World of Warcraft",
27
+ "C:\\Program Files (x86)\\World of Warcraft",
28
+ "C:\\Program Files\\World of Warcraft"
29
+ ];
30
+
31
+ for (const path of commonPaths) {
32
+ if (existsSync(path)) return path;
33
+ }
34
+
35
+ return null;
36
+ }
37
+
38
+ export function validateWoWPath(path: string): boolean {
39
+ const retail = join(path, "_retail_");
40
+ const classic = join(path, "_classic_");
41
+ const classicEra = join(path, "_classic_era_");
42
+
43
+ return existsSync(retail) || existsSync(classic) || existsSync(classicEra);
44
+ }
package/src/schema.ts ADDED
@@ -0,0 +1,18 @@
1
+
2
+ import { z } from "zod";
3
+ import type { WeaverConfig as IWeaverConfig } from "../../weaver/src/types/config";
4
+
5
+ export const WeaverConfigSchema = z.object({
6
+ addonName: z.string(),
7
+ flavor: z.enum(["_retail_", "_classic_", "_classic_era_"]),
8
+ interface: z.string(),
9
+ outDir: z.string().optional().default("Interface/AddOns"),
10
+ entry: z.string().optional().default("src/App.tsx"),
11
+ wowPath: z.string().optional().nullable(),
12
+ });
13
+
14
+ export type WeaverConfig = z.infer<typeof WeaverConfigSchema>;
15
+
16
+ // Verify it matches the interface
17
+ type Verification = WeaverConfig extends IWeaverConfig ? (IWeaverConfig extends WeaverConfig ? true : false) : false;
18
+ const _v: Verification = true;