From 25943eed1c7d89a94554ccd4720feb5844c2efbb Mon Sep 17 00:00:00 2001 From: Wyatt McCarthy Date: Fri, 30 Jan 2026 13:43:59 -0800 Subject: [PATCH 1/5] update lute auto-detection for more robust strategy --- src/astParser.ts | 195 ++++++++++++++++++++++++++++++++++++----------- src/extension.ts | 10 +-- 2 files changed, 155 insertions(+), 50 deletions(-) diff --git a/src/astParser.ts b/src/astParser.ts index aad7242..0343970 100644 --- a/src/astParser.ts +++ b/src/astParser.ts @@ -7,72 +7,177 @@ import * as os from "os"; const execAsync = promisify(exec); +interface ToolStorageConfig { + path: string; + nested: boolean; // true = rokit style (/lute), false = foreman style (flat files) + pattern?: RegExp; // optional filter for flat storage (e.g., /^luau-lang.*lute/i) +} + export class ASTParserAndPrinter { - private luteExecutable: string; + private luteExecutable: string = "lute"; // default fallback private extensionPath: string; - constructor(context: vscode.ExtensionContext) { - this.extensionPath = context.extensionPath; + private constructor(extensionPath: string) { + this.extensionPath = extensionPath; + } + + static async create( + context: vscode.ExtensionContext + ): Promise { + const instance = new ASTParserAndPrinter(context.extensionPath); // Get the Lute executable path from configuration const config = vscode.workspace.getConfiguration("luauAstExplorer"); const configuredPath = config.get("luteExecutable") as string; if (configuredPath) { - this.luteExecutable = configuredPath; + instance.luteExecutable = configuredPath; } else { // Auto-detect Lute executable - this.luteExecutable = this.detectLuteExecutable(); + instance.luteExecutable = await instance.detectLuteExecutable(); + } + + console.log( + `ASTParser: Using Lute executable at: ${instance.luteExecutable}` + ); + console.log(`ASTParser: Extension path: ${instance.extensionPath}`); + + return instance; + } + + private async isValidExecutable(path: string): Promise { + + if (!fs.existsSync(path)) { + return false; } - console.log(`ASTParser: Using Lute executable at: ${this.luteExecutable}`); - console.log(`ASTParser: Extension path: ${this.extensionPath}`); + const command = `${path} --version`; + try { + const { stdout, stderr } = await execAsync(command, { + timeout: 10000, // 10 second timeout + maxBuffer: 1024 * 1024, // 1MB buffer + }); + + if (stderr && !stdout) { + return false; + } + + return true; + } catch (error) { + return false; + } } - private detectLuteExecutable(): string { + /* + Lute Detection Strategy: + 1. Check rokit/foreman bin directories first (project-aware via config files) + 2. Scan rokit tool-storage (nested: ~/.rokit/tool-storage/luau-lang/lute//lute) + 3. Scan foreman tools (flat: ~/.foreman/tools/luau-lang__lute-) + 4. Fall back to system PATH + */ + private async detectLuteExecutable(): Promise { const homeDir = os.homedir(); - const possiblePaths = [ - // Foreman installation paths - path.join(homeDir, ".foreman", "bin", "lute"), - path.join(homeDir, ".foreman", "bin", "lute.exe"), // Windows - // Rokit installation paths - path.join( - homeDir, - ".rokit", - "tool-storage", - "luau-lang", - "lute", - "0.1.0-nightly.20250722", - "lute" - ), // To-Do: don't hard code version like this - path.join( - homeDir, - ".rokit", - "tool-storage", - "luau-lang", - "lute", - "0.1.0-nightly.20250722", - "lute.exe" - ), // Windows - // System paths (fallback) - "lute", - "/usr/local/bin/lute", - "/opt/homebrew/bin/lute", + const isWindows = process.platform === "win32"; + const ext = isWindows ? ".exe" : ""; + + // 1. First priority: Use rokit/foreman bin directories (project-aware) + const binPaths = [ + path.join(homeDir, ".rokit", "bin", `lute${ext}`), + path.join(homeDir, ".foreman", "bin", `lute${ext}`), ]; - for (const lutePath of possiblePaths) { - try { - if (fs.existsSync(lutePath)) { - console.log(`Found Lute executable at: ${lutePath}`); - return lutePath; - } - } catch (error) { - // Continue to next path + for (const binPath of binPaths) { + if (await this.isValidExecutable(binPath)) { + console.log(`Found Lute in toolchain bin: ${binPath}`); + return binPath; } } - console.log("Using fallback path: lute"); - return "lute"; // Fallback to PATH + // 2. Fallback: Scan tool storage directories + const toolStorageConfigs: ToolStorageConfig[] = [ + { + path: path.join(homeDir, ".rokit", "tool-storage", "luau-lang", "lute"), + nested: true, // rokit: /lute + }, + { + path: path.join(homeDir, ".foreman", "tools"), + nested: false, // foreman: luau-lang__lute- (flat) + pattern: /^luau-lang.*lute/i, + }, + ]; + + for (const config of toolStorageConfigs) { + const lutePath = await this.findLuteInToolStorage(config, ext); + if (lutePath) { + console.log(`Found Lute in tool storage: ${lutePath}`); + return lutePath; + } + } + + // 4. System PATH fallback + const systemPaths = ["/usr/local/bin/lute", "/opt/homebrew/bin/lute"]; + for (const sysPath of systemPaths) { + if (fs.existsSync(sysPath)) { + console.log(`Found Lute in system path: ${sysPath}`); + return sysPath; + } + } + + console.log("Using fallback: lute (from PATH)"); + return "lute"; + } + + /** + * Finds the latest lute executable in a tool storage directory. + * Supports both nested (rokit) and flat (foreman) storage structures. + */ + private async findLuteInToolStorage( + config: ToolStorageConfig, + ext: string + ): Promise { + try { + if (!fs.existsSync(config.path)) { + return null; + } + + const entries = fs.readdirSync(config.path); + if (entries.length === 0) { + return null; + } + + // Filter entries if pattern is specified (for flat storage) + const candidates = config.pattern + ? entries.filter((e) => config.pattern!.test(e)) + : entries; + + if (candidates.length === 0) { + return null; + } + + // Sort descending to get latest version first + candidates.sort().reverse(); + + for (const candidate of candidates) { + const candidatePath = path.join(config.path, candidate); + const stats = fs.statSync(candidatePath); + + if (config.nested && stats.isDirectory()) { + // Nested: look for lute executable inside version directory + const lutePath = path.join(candidatePath, `lute${ext}`); + if (await this.isValidExecutable(lutePath)) { + return lutePath; + } + } else if (!config.nested && stats.isFile()) { + // Flat: the candidate itself is the executable + if (await this.isValidExecutable(candidatePath)) { + return candidatePath; + } + } + } + } catch (error) { + console.warn(`Error scanning tool storage at ${config.path}: ${error}`); + } + return null; } async parseCode(code: string, languageId: string): Promise { diff --git a/src/extension.ts b/src/extension.ts index 46d2f19..058e53e 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,11 +1,11 @@ import * as vscode from "vscode"; import { ASTParserAndPrinter } from "./astParser"; -export function activate(context: vscode.ExtensionContext) { +export async function activate(context: vscode.ExtensionContext) { console.log("Luau AST Explorer is now active!"); // Create AST parser instance with extension context - const astParser = new ASTParserAndPrinter(context); + const astParser = await ASTParserAndPrinter.create(context); // Register the command to show AST let disposable = vscode.commands.registerCommand( @@ -274,7 +274,7 @@ async function handleParseAST( }); // Parse the code using existing ASTParser - const astParser = new ASTParserAndPrinter(context); + const astParser = await ASTParserAndPrinter.create(context); const astResult = await astParser.parseCode(code, "luau"); // Send success result to webview @@ -307,7 +307,7 @@ async function handleParseDiff( }); // Parse both code snippets using existing ASTParser - const astParser = new ASTParserAndPrinter(context); + const astParser = await ASTParserAndPrinter.create(context); // Parse before and after code separately const beforeAST = await astParser.parseCode(beforeCode, "luau"); @@ -344,7 +344,7 @@ async function handlePrintCode( nodeId: nodeId, }); - const astPrinter = new ASTParserAndPrinter(context); + const astPrinter = await ASTParserAndPrinter.create(context); const code = await astPrinter.printCode(nodeJson); // Send success result to webview From 6592be6df9b2aa3c50ea15a6cb39e1854fbeacb6 Mon Sep 17 00:00:00 2001 From: Wyatt McCarthy Date: Fri, 30 Jan 2026 13:48:32 -0800 Subject: [PATCH 2/5] improved error message --- src/astParser.ts | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/src/astParser.ts b/src/astParser.ts index 0343970..303b5ae 100644 --- a/src/astParser.ts +++ b/src/astParser.ts @@ -45,25 +45,11 @@ export class ASTParserAndPrinter { return instance; } - private async isValidExecutable(path: string): Promise { - - if (!fs.existsSync(path)) { - return false; - } - - const command = `${path} --version`; + private async isValidExecutable(execPath: string): Promise { try { - const { stdout, stderr } = await execAsync(command, { - timeout: 10000, // 10 second timeout - maxBuffer: 1024 * 1024, // 1MB buffer - }); - - if (stderr && !stdout) { - return false; - } - + await execAsync(`${execPath} --version`, { timeout: 5000 }); return true; - } catch (error) { + } catch { return false; } } @@ -246,7 +232,7 @@ export class ASTParserAndPrinter { error.message.includes("not recognized") ) { throw new Error( - `Lute executable not found at: ${this.luteExecutable}. Please install Lute using foreman or ensure it's in your PATH.` + `Lute executable not found at: ${this.luteExecutable}. Please install Lute using foreman / rokit or ensure it's in your PATH.` ); } throw new Error(`Lute execution failed: ${error.message}`); From b44b3d5d7912e467430e94fb59b55cb23fa90c0b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 30 Jan 2026 21:51:29 +0000 Subject: [PATCH 3/5] Add changelog file for PR #63 --- .changes/63-feat.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 .changes/63-feat.md diff --git a/.changes/63-feat.md b/.changes/63-feat.md new file mode 100644 index 0000000..05ca405 --- /dev/null +++ b/.changes/63-feat.md @@ -0,0 +1 @@ +Better Lute Autodetection From f6cf194928b07ee2d897f11434148544cc057357 Mon Sep 17 00:00:00 2001 From: Wyatt McCarthy <115899870+wmccrthy@users.noreply.github.com> Date: Fri, 30 Jan 2026 14:32:05 -0800 Subject: [PATCH 4/5] Update 63-feat.md --- .changes/63-feat.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changes/63-feat.md b/.changes/63-feat.md index 05ca405..c25b74b 100644 --- a/.changes/63-feat.md +++ b/.changes/63-feat.md @@ -1 +1 @@ -Better Lute Autodetection +Smarter Lute auto-detection; dynamically checks `foreman` / `rokit` bin and falls back to tool storage. From 2242a82be3019d1b93d8b6b6536057fb95318ca9 Mon Sep 17 00:00:00 2001 From: Wyatt McCarthy Date: Fri, 30 Jan 2026 14:38:05 -0800 Subject: [PATCH 5/5] add a warning abt inconsistent behavior when using out-dated / workspace lute versions --- src/astParser.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/astParser.ts b/src/astParser.ts index 303b5ae..066ee34 100644 --- a/src/astParser.ts +++ b/src/astParser.ts @@ -75,6 +75,9 @@ export class ASTParserAndPrinter { for (const binPath of binPaths) { if (await this.isValidExecutable(binPath)) { console.log(`Found Lute in toolchain bin: ${binPath}`); + vscode.window.showWarningMessage( + "Using workspace-configured Lute version. If you experience parsing issues, try bumping to the latest Lute version." + ); return binPath; } }