diff --git a/scripts/build.luau b/scripts/build.luau index 97052a5..15ccf51 100644 --- a/scripts/build.luau +++ b/scripts/build.luau @@ -27,5 +27,5 @@ fs.write(versionFile, `return \{ version="{version}", hash="{hash}" \}`) fs.close(versionFile) log.info("Building rocale-cli...") -runProcess({ "lute", "compile", "src/cli.luau", "--output", output_path, "--show-require-graph", "--bundle-stats" }) +runProcess("lute", "compile", "src/cli.luau", "--output", output_path, "--show-require-graph", "--bundle-stats") log.info("Build complete: " .. output_path) diff --git a/scripts/test.luau b/scripts/test.luau index 2d3b2b2..920e225 100644 --- a/scripts/test.luau +++ b/scripts/test.luau @@ -6,7 +6,7 @@ log.setLevel("debug") local runProcess = require("../src/util/runProcess") -runProcess({ +runProcess( "build/rocale-cli", "run", "--script", @@ -21,8 +21,8 @@ runProcess({ "HELLO_GLOBAL=hello,WORLD_GLOBAL=world!", "--binaryOutput", "build/testOutput.txt", - "--verbose", -}) + "--verbose" +) local testOutput = fs.readFileToString("build/testOutput.txt") fs.remove("build/testOutput.txt") @@ -30,7 +30,7 @@ assert(testOutput == "hello world!", `Expected binary output to be "hello world! log.info("Rerunning tests using load.version and without optional variables") -runProcess({ +runProcess( "build/rocale-cli", "run", "--script", @@ -41,5 +41,5 @@ runProcess({ process.env.TEST_UNIVERSE_ID, "--load.version", "0", - "--verbose", -}) + "--verbose" +) diff --git a/src/commands/run.luau b/src/commands/run.luau index 815e7a9..e56437b 100644 --- a/src/commands/run.luau +++ b/src/commands/run.luau @@ -34,6 +34,10 @@ FLAGS: ]]) end +local function toBoolNum(v: any): number + return if v then 1 else 0 +end + return function(...) local fs = require("@std/fs") local process = require("@lute/process") @@ -83,7 +87,7 @@ return function(...) local globals = args:get("lua.globals") assert(globals, "Expected globals to be a comma-separated list of key=value pairs") - for _, pair in ipairs(string.split(globals, ",")) do + for _, pair in string.split(globals, ",") do local key, value = pair:match("([^=]+)=([^=]+)") if key and value then luaGlobals[key] = value @@ -98,11 +102,12 @@ return function(...) placeId = tonumber(args:get("placeId")) or log.fatal("Missing a place ID"), } + --stylua: ignore if ( - (args:get("load.place") and 1 or 0) - + (args:get("load.version") and 1 or 0) - + (args:get("load.project") and 1 or 0) + toBoolNum(args:get("load.place")) + + toBoolNum(args:get("load.version")) + + toBoolNum(args:get("load.project")) ) > 1 then log.fatal("Cannot specify more than one --load option") @@ -126,16 +131,16 @@ return function(...) assert(outputPath, "Missing output file path") if projectFile:match("%.project%.json$") then - runProcess({ "rojo", "build", projectFile, "--output", outputPath }) + runProcess("rojo", "build", projectFile, "--output", outputPath) elseif projectFile:match("%.rbxp$") then - runProcess({ + runProcess( "robloxdev-cli", "pack", "--input", projectFile, "--output", - outputPath, - }) + outputPath + ) else log.fatal(`Unsupported project file: '{projectFile}'. Must be a 'project.json' or '.rbxp' file.`) end diff --git a/src/core/ocaleSdk.luau b/src/core/ocaleSdk.luau index a15d5e9..994bd8d 100644 --- a/src/core/ocaleSdk.luau +++ b/src/core/ocaleSdk.luau @@ -6,6 +6,13 @@ local task = require("@lute/task") local log = require("../util/log") local richterm = require("@batteries/richterm") +local boldYellowFormatter = richterm.combine(richterm.yellow, richterm.bold) +local spinnerChars = table.freeze({ "|", "/", "-", "\\" }) + +local function formatSeconds(s: number): string + return string.format("%.1fs", s) +end + local OcaleSDK = {} export type BinaryInputResponse = { path: string, size: number, uploadUri: string } @@ -104,14 +111,12 @@ function OcaleSDK.createTask( -- inject globals at the top of the entry script if luaGlobals and next(luaGlobals) then - local keys: { string } = {} - local values: { string } = {} - for key, value in pairs(luaGlobals) do - table.insert(keys, `_G[{string.format("%q", key)}]`) - table.insert(values, string.format("%q", value)) + local globalAssignments: { string } = {} + + for key, value in luaGlobals do + table.insert(globalAssignments, `_G["{key}"]="{value}";`) end - local assignGlobals = table.concat(keys, ",") .. "=" .. table.concat(values, ",") .. " " - entryScript = assignGlobals .. entryScript + entryScript = table.concat(globalAssignments) .. entryScript end local req: client.Metadata = { @@ -143,21 +148,19 @@ function OcaleSDK.pollTaskCompletion( local timeRemaining = timeout or 300 local interval = pollInterval or 2 - local spinnerChars = { "|", "/", "-", "\\" } - local spinnerIndex = 1 - local spinner = richterm.yellow(spinnerChars[spinnerIndex], log.noColor()) - local startTime = os.clock() - local elapsed = string.format("%.1f", os.clock() - startTime) log.info(`Polling for OCALE task completion`) if not log.noColor() then task.spawn(function() + local spinnerIndex = 1 + while timeRemaining > 0 do - spinner = richterm.bold(richterm.yellow(spinnerChars[spinnerIndex], log.noColor())) - elapsed = string.format("%.1f", os.clock() - startTime) - log.update(`{spinner} {richterm.dim(`[{elapsed}s]`, log.noColor())} Polling for OCALE task completion`) - spinnerIndex = spinnerIndex % #spinnerChars + 1 + local spinner = boldYellowFormatter(spinnerChars[spinnerIndex], log.noColor()) + local elapsed = formatSeconds(os.clock() - startTime) + + log.update(`{spinner} {richterm.dim(`[{elapsed}]`, log.noColor())} Polling for OCALE task completion`) + spinnerIndex = (spinnerIndex % 4) + 1 task.wait(0.1) end end) @@ -179,9 +182,9 @@ function OcaleSDK.pollTaskCompletion( if body.state ~= "PROCESSING" then timeRemaining = 0 task.wait(0.1) - elapsed = string.format("%.1f", os.clock() - startTime) + local elapsed = formatSeconds(os.clock() - startTime) log.info( - `{richterm.green("✓", log.noColor())} {richterm.dim(`[{elapsed}s]`, log.noColor())} OCALE task completed` + `{richterm.green("✓", log.noColor())} {richterm.dim(`[{elapsed}]`, log.noColor())} OCALE task completed` ) return body end @@ -190,9 +193,10 @@ function OcaleSDK.pollTaskCompletion( end timeRemaining = 0 - elapsed = string.format("%.1f", os.clock() - startTime) + local elapsed = formatSeconds(os.clock() - startTime) + --stylua: ignore return log.fatal( - `{richterm.red("✗", log.noColor())} {richterm.dim(`[{elapsed}s]`, log.noColor())} Task timed out` + `{richterm.red("✗", log.noColor())} {richterm.dim(`[{elapsed}]`, log.noColor())} Task timed out` ) end @@ -219,4 +223,4 @@ function OcaleSDK.getTaskLogs(apiKey: string, taskPath: string): { string } return logs end -return OcaleSDK +return table.freeze(OcaleSDK) diff --git a/src/util/log.luau b/src/util/log.luau index ae4e93f..5938d1d 100644 --- a/src/util/log.luau +++ b/src/util/log.luau @@ -9,6 +9,7 @@ local log = {} type Level = "trace" | "debug" | "info" | "warn" | "error" +local dimBlueFormatter = richterm.combine(richterm.blue, richterm.dim) local currentLevel: Level = "info" local colorsEnabled = true -- set to false to disable colors @@ -18,122 +19,69 @@ end local showCaller = true -- set to false to disable caller information -local levels = { +local levels = table.freeze({ trace = 1, debug = 2, info = 3, warn = 4, error = 5, -} :: { [Level]: number } +}) :: { [Level]: number } -function log.should(level: Level) - return levels[level] >= levels[currentLevel] +local function stringifyVararg(...: any): string + return table.concat({ ... }, " ") end --- Get caller information from debug traceback. -local function getCallerInfo() - if not showCaller then - return "" - end - local traceback = debug.traceback() - local lines = string.split(traceback, "\n") - - -- Look for the first line that's not in this logger module. - for _, line in ipairs(lines) do - if line:find("util/log") then - continue - end - - return line - end - - return "[unknown]" +function log.should(level: Level) + return levels[level] >= levels[currentLevel] end function log.trace(...) if log.should("trace") then - local l: string = "" - if colorsEnabled then - l = richterm.combine(richterm.blue, richterm.dim)("[TRACE] ") - print(l .. richterm.dim(... or "")) - else - l = "[TRACE] " - print(l .. (... or "")) - end + local prefix = dimBlueFormatter("[TRACE] ") + print(prefix .. richterm.dim(stringifyVararg(...), colorsEnabled)) end end function log.debug(...) if log.should("debug") then - local l: string = "" - if colorsEnabled then - l = richterm.cyan("[DEBUG] ") - print(l .. richterm.dim(... or "")) - else - l = "[DEBUG] " - print(l .. (... or "")) - end + local prefix = richterm.cyan("[DEBUG] ", colorsEnabled) + print(prefix .. richterm.dim(stringifyVararg(...), colorsEnabled)) end end function log.info(...) if log.should("info") then - local l: string = "" - if colorsEnabled then - l = richterm.green("[INFO] ") - else - l = "[INFO] " - end - print(l .. (... or "")) + local prefix = richterm.green("[INFO] ", colorsEnabled) + print(prefix .. stringifyVararg(...)) end end function log.update(...) if log.should("info") then - local l: string = "" - if colorsEnabled then - l = richterm.green("[INFO] ") - print("\x1b[1A\x1b[2K" .. l .. (... or "")) - else - l = "[INFO] " - print(l .. (... or "")) - end + local prefix = richterm.green("[INFO] ", colorsEnabled) + print("\x1b[1A\x1b[2K" .. prefix .. stringifyVararg(...)) end end function log.warn(...) if log.should("warn") then - local l: string = "" - if colorsEnabled then - l = richterm.yellow("[WARN] ") - else - l = "[WARN] " - end - print(l .. (... or "")) + local prefix = richterm.yellow("[WARN] ", colorsEnabled) + print(prefix .. stringifyVararg(...)) end end function log.error(...) if log.should("error") then - local l: string = "" - if colorsEnabled then - l = richterm.red("[ERROR] ") - else - l = "[ERROR] " - end - print(l .. (... or "")) + local prefix = richterm.red("[ERROR] ", colorsEnabled) + print(prefix .. stringifyVararg(...)) end end function log.fatal(...): never - local caller = getCallerInfo() - local l: string = "" - if colorsEnabled then - l = richterm.red("[FATAL] ") - else - l = "[FATAL] " - end - print(caller .. " | " .. l .. (... or "") .. "\n") + local prefix = richterm.red("[FATAL] ", colorsEnabled) + local message = prefix .. stringifyVararg(...) + + print(if showCaller then debug.traceback(message, 2) else message) process.exit(1) return nil :: any end @@ -167,26 +115,28 @@ function log.noColor() end function log.pretty(tbl: any, indent: number?): string - local i = indent or 0 - local spacing = string.rep(" ", i) if type(tbl) ~= "table" then return tostring(tbl) end - local result = "{\n" - for key, value in pairs(tbl) do - local keyStr = type(key) == "string" and key or "[" .. tostring(key) .. "]" - result = result .. spacing .. " " .. keyStr .. ": " - if type(value) == "table" then - result = result .. log.pretty(value, i + 1) - elseif type(value) == "string" then - result = result .. '"' .. value .. '"' - else - result = result .. tostring(value) - end - result = result .. "\n" + + indent = indent or 0 + local spacing = string.rep(" ", indent) + local result = { "{" } + + for key, value in tbl do + local keyStr = if type(key) == "string" then key else `["{key}"]` + --stylua: ignore + local valueStr = if type(value) == "table" then + log.pretty(value, indent + 1) + elseif type(value) == "string" then + `"{value}"` + else + value + + table.insert(result, `{keyStr}: {valueStr}`) end - result = result .. spacing .. "}" - return result + table.insert(result, "}") + return table.concat(result, `\n{spacing}`) end -return log +return table.freeze(log) diff --git a/src/util/runProcess.luau b/src/util/runProcess.luau index 2bf51bb..1e856b6 100644 --- a/src/util/runProcess.luau +++ b/src/util/runProcess.luau @@ -1,26 +1,31 @@ -local process = require("@lute/process") -local system = require("@lute/system") +local process = require("@std/process") +local system = require("@std/system") local log = require("../util/log") -return function(cmd: { string }) - local isWin32 = string.lower(system.os) == "windows_nt" +local isWin32 = string.lower(system.os) == "windows_nt" + +local function runProcess(...: string): string + local cmd = { ... } + if isWin32 then - cmd[1] = cmd[1] .. ".exe" + cmd[1] ..= ".exe" end local cmdStr = table.concat(cmd, " ") log.debug(`Running command: {cmdStr}`) - local ok, result = pcall(function() - return process.run( - cmd, - { stdio = "inherit", env = { PATH = `{process.cwd()}{if isWin32 then ";" else ":"}{process.env.PATH}` } } - ) - end) + local result = process.run(cmd, { + stdio = "inherit", + env = { + PATH = `{process.cwd()}{if isWin32 then ";" else ":"}{process.env.PATH}`, + }, + }) - if ok and result.ok then + if result.ok then return result.stdout else - return log.fatal(`Error while running command: {cmdStr}\n{result.stderr or result}`) + return log.fatal(`Error while running command: {cmdStr}\n{result.stderr}`) end end + +return runProcess