- Introduced a new upload workflow for the Atlas plugin to Roblox using Lune and OpenCloud. - Added `UploadDetails.json` to store plugin metadata. - Updated `build.rs` to include `UploadDetails.json` in the plugin snapshot. - Enhanced `plugin.project.json` to reference the new `UploadDetails`. - Added a new submodule for `opencloud-execute` to facilitate plugin uploads. - Created scripts for publishing the plugin via both PowerShell and Bash. - Updated GitHub Actions workflow to include steps for installing Rokit and uploading the plugin. These changes aim to streamline the plugin release process and improve integration with Roblox's marketplace.
17 KiB
| name | overview | todos | isProject | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Rokit Install Audit Fixes | Fix the EPERM crash when deleting a globally installed atlas.exe, and harden the entire rokit installation pipeline in the VS Code extension for reliability across Windows, macOS, and Linux. |
|
false |
Rokit Installation Audit and Fixes
Audit Findings
How Rokit Installation Works (No Elevation Needed)
Rokit installs entirely to ~/.rokit/ (user-level directory). rokit self-install modifies user-level PATH environment variables (HKCU on Windows, shell rc files on Unix). Neither rokit installation nor rokit add requires elevation.
The only scenario requiring elevation is the "Switch to Rokit" migration flow, which tries to delete an existing globally installed atlas.exe from a protected directory like C:\Program Files\Atlas (written by install.ps1) or /usr/local/bin/atlas (written by install.sh).
Issues Found (by severity)
BUG 1 -- CRITICAL: fs.unlink fails without elevation, crashes the entire flow
In vscode-rojo/src/commands/openMenu.ts line 133:
return fs.unlink(install.resolvedPath)
When atlas.exe is in C:\Program Files\Atlas or /usr/local/bin/, this throws EPERM/EACCES. The .then() error handler on line 139 catches it but just shows a generic message and the flow dies. Additionally, on Windows the exe may be locked by a running process (atlas serve), causing EBUSY even with elevation.
Fix: Add an elevatedDelete() helper that:
- Tries
fs.unlink()first - On EPERM/EACCES, uses platform-specific elevated delete:
- Windows:
powershell -Command "Start-Process powershell -ArgumentList '-Command Remove-Item \"path\"' -Verb RunAs -Wait"(triggers UAC prompt) - macOS:
osascript -e 'do shell script "rm path" with administrator privileges'(triggers password dialog) - Linux:
pkexec rm path(triggers PolicyKit dialog)
- On EBUSY (Windows exe locked), shows a specific error message telling the user to stop atlas serve first
BUG 2 -- HIGH: No progress indicator during installation
In vscode-rojo/src/installRojo.ts installRojo():
The entire flow (download rokit zip ~3.5MB, extract, self-install, rokit add atlas) runs silently. Users see zero feedback and may think the extension froze.
Fix: Wrap the body of installRojo() in vscode.window.withProgress() with ProgressLocation.Notification and cancellable: false. Report progress at each stage:
- "Downloading Rokit..."
- "Installing Rokit..."
- "Installing Atlas via Rokit..."
BUG 3 -- HIGH: Third-party release URL is a single point of failure
In vscode-rojo/src/installRojo.ts line 113:
const latestReleaseResponse = await fetch(
"https://latest-github-release.eryn.io/rojo-rbx/rokit",
)
This third-party proxy (eryn.io) is not under our control. If it goes down, installation breaks. The official rokit install scripts use the GitHub API directly.
Fix: Replace with the official GitHub API URL: https://api.github.com/repos/rojo-rbx/rokit/releases/latest. Verified: response schema is identical, both return the same GitHubRelease shape. Add an Accept: application/vnd.github+json header per GitHub API best practice.
BUG 4 -- MEDIUM: Temp files never cleaned up
In vscode-rojo/src/installRojo.ts line 143-170:
The rokit binary is downloaded to os.tmpdir() but never deleted after rokit self-install succeeds.
Fix: Add fs.promises.unlink(tempPath).catch(() => {}) after the exec(self-install) call succeeds.
BUG 5 -- MEDIUM: PATH update is fragile
In vscode-rojo/src/installRojo.ts lines 177-181:
if ("PATH" in process.env) {
const envPath = process.env.PATH!.split(path.delimiter)
envPath.push(path.join(os.homedir(), ".rokit", "bin"))
process.env.PATH = envPath.join(path.delimiter)
}
This only updates the extension's own process environment. But the subsequent rokit add --global on line 184 runs via exec() which inherits this env, so it should work. The concern is:
- It doesn't check if the path is already present (could add duplicates on repeated attempts)
- The rokit binary path might differ on some systems
Fix: Check for duplicates before appending. Also verify the rokit binary is actually accessible after the PATH update by doing a quick which("rokit") check before proceeding to rokit add.
BUG 6 -- LOW: Stream error handling is fragile
In vscode-rojo/src/installRojo.ts lines 51-60, 149-163:
promisifyStream resolves on whichever of "close"/"finish"/"end" fires first. The pipeline/pipe chain has multiple potential failure modes that aren't properly propagated.
Fix: Use stream.promises.pipeline() (available in Node 15+, which VS Code ships with) instead of manually piping and tracking events. This gives proper error propagation and backpressure handling. Alternatively, simplify to await promisify(stream.pipeline)(download.body!, unzipper.ParseOne(), fs.createWriteStream(tempPath)).
BUG 7 -- HIGH: rokit add --global crashes if atlas is already configured
In vscode-rojo/src/installRojo.ts line 184:
await exec("rokit add --global UserGeneratedLLC/rojo atlas")
This is the only action after rokit is confirmed installed. It fails if atlas is already in the project's rokit.toml or already added globally. The scenario:
- Project has
rokit.tomlwith atlas listed butrokit installhasn't been run yet which("atlas")finds the rokit shim at~/.rokit/bin/atlasatlas --versionfails (rokit says tool not installed), stderr contains "rokit" ->getRojoInstallreturns null- User sees "Install Atlas now", clicks it
isRokitInstalled()returns true (skips rokit download)rokit add --globalfails because atlas is already configured -> onboarding dies
Fix: Use --force to handle "already exists", trust the tool, then verify and install the plugin:
await exec("rokit add --global --force UserGeneratedLLC/rojo atlas")
await exec("rokit trust UserGeneratedLLC/rojo")
await exec("atlas --version")
// Install the Studio plugin (non-fatal -- don't crash onboarding if Studio isn't installed)
try {
await exec("atlas plugin install")
} catch (e: any) {
vscode.window.showWarningMessage(
`Atlas installed successfully, but the Studio plugin could not be installed: ${e.stderr || e}. ` +
`You can install it later from the Atlas menu or by running "atlas plugin install".`
)
}
--forceonrokit addhandles the "already exists" case cleanly -- no try/catch needed, it re-adds/updates regardlessrokit trustmarksUserGeneratedLLC/rojoas trusted so rokit won't prompt the user for confirmation when running atlas (without trust,rokit installand first-run shims can block waiting for user input)atlas --versionverifies the tool is actually usable after addatlas plugin installwrites the embedded Studio plugin to disk (non-fatal if Studio isn't installed yet)
FEATURE: "Check for Updates" menu item for Rokit-managed installs
In vscode-rojo/src/commands/openMenu.ts, the generateProjectMenu() return array (line 315) has action items like Studio, Syncback, Sourcemap, Install Plugin. There is no way to update Atlas from the extension.
generateProjectMenu() already tracks installType (line 168) and whether it's InstallType.rokit. We use this to conditionally show the item.
Implementation:
- Add a menu item in
generateProjectMenu()return array, conditionally included wheninstallType === InstallType.rokit:
...(installType === InstallType.rokit
? [
{
label: "$(sync) Check for Updates",
description: `Atlas v${allRojoVersions[0]}`,
action: "checkUpdates",
projectFile: projectFiles[0],
},
]
: []),
- Add
"checkUpdates"case in theonDidAccepthandler (after the"install"case). The handler needs to support both project-levelrokit.tomland global installations:
case "checkUpdates": {
if (!selectedItem.projectFile) return
const updateFolder = path.dirname(selectedItem.projectFile.path.fsPath)
input.hide()
await vscode.window.withProgress(
{ location: vscode.ProgressLocation.Notification, title: "Checking for Atlas updates..." },
async (progress) => {
// Try project-level update first, fall back to global
let updateOutput: string
try {
const result = await exec("rokit update atlas", { cwd: updateFolder })
updateOutput = result.stdout || result.stderr
} catch {
try {
const result = await exec("rokit update --global atlas")
updateOutput = result.stdout || result.stderr
} catch (e: any) {
vscode.window.showErrorMessage(`Could not update Atlas: ${e.stderr || e}`)
return
}
}
// Re-install the Studio plugin so it matches the new binary.
// The plugin is embedded in the atlas binary at compile time
// (include_bytes! in src/cli/plugin.rs), so updating the binary
// without re-installing leaves a stale AtlasManagedPlugin.rbxm
// in Studio's plugins folder. Mismatched versions break live sync
// due to protocol version enforcement.
progress.report({ message: "Reinstalling Studio plugin..." })
try {
await exec("atlas plugin install")
} catch (e: any) {
vscode.window.showWarningMessage(
`Atlas updated, but plugin reinstall failed: ${e.stderr || e}. ` +
`Run "atlas plugin install" manually to avoid version mismatch.`
)
}
vscode.window.showInformationMessage(updateOutput.trim() || "Atlas is up to date.")
},
)
break
}
The strategy: try rokit update atlas in the project directory first. If atlas isn't in the project's rokit.toml, this fails and we fall back to rokit update --global atlas. Rokit's output tells the user whether an update was applied or they're already current.
After a successful update, immediately runs atlas plugin install to overwrite the stale AtlasManagedPlugin.rbxm in Studio's plugins folder. The plugin is baked into the atlas binary via include_bytes! at compile time (src/cli/plugin.rs:12), so the .rbxm in Studio never auto-updates -- it must be explicitly reinstalled. If the plugin reinstall fails (e.g. Studio not installed), a warning is shown with manual instructions rather than failing the whole update.
FEATURE: Background update check on extension activation
In vscode-rojo/src/extension.ts activate():
Rokit has rokit update atlas --check which checks for updates without installing. Supports both project-level (rokit update atlas --check in the project dir, reads rokit.toml) and global (rokit update --global atlas --check).
Implementation: Add a new file vscode-rojo/src/checkForUpdates.ts with a checkForAtlasUpdates(context) function, called fire-and-forget from activate():
export async function checkForAtlasUpdates(context: vscode.ExtensionContext) {
// Throttle: once per day
const lastCheck = context.globalState.get<number>("atlas::lastUpdateCheck")
const ONE_DAY = 24 * 60 * 60 * 1000
if (lastCheck && Date.now() - lastCheck < ONE_DAY) return
// Check if rokit is available
const rokitPath = await which("rokit").catch(() => null)
if (!rokitPath) return
const workspaceFolder = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
// rokit update --check prints update info if available, or "up to date" if current.
// Check both project-level and global.
let checkOutput: string | null = null
let updateScope: "project" | "global" = "global"
// Try project-level first (if workspace is open)
if (workspaceFolder) {
try {
const result = await exec("rokit update atlas --check", { cwd: workspaceFolder })
const output = (result.stdout + result.stderr).trim()
if (output && !output.toLowerCase().includes("up to date")) {
checkOutput = output
updateScope = "project"
}
} catch {
// atlas not in project rokit.toml -- fall through
}
}
// Try global if no project-level update found
if (!checkOutput) {
try {
const result = await exec("rokit update --global atlas --check")
const output = (result.stdout + result.stderr).trim()
if (output && !output.toLowerCase().includes("up to date")) {
checkOutput = output
updateScope = "global"
}
} catch {
// atlas not installed globally via rokit
}
}
context.globalState.update("atlas::lastUpdateCheck", Date.now())
if (!checkOutput) return
const choice = await vscode.window.showInformationMessage(
`An Atlas update is available. ${checkOutput}`,
"Update",
"Dismiss",
)
if (choice !== "Update") return
await vscode.window.withProgress(
{ location: vscode.ProgressLocation.Notification, title: "Updating Atlas..." },
async (progress) => {
try {
if (updateScope === "project" && workspaceFolder) {
await exec("rokit update atlas", { cwd: workspaceFolder })
} else {
await exec("rokit update --global atlas")
}
} catch (e: any) {
vscode.window.showErrorMessage(`Could not update Atlas: ${e.stderr || e}`)
return
}
progress.report({ message: "Reinstalling Studio plugin..." })
try {
await exec("atlas plugin install")
} catch {
vscode.window.showWarningMessage(
"Atlas updated, but Studio plugin reinstall failed. Run 'atlas plugin install' manually.",
)
}
vscode.window.showInformationMessage("Atlas updated successfully.")
},
)
}
Called from activate() as fire-and-forget (no await) so it never blocks extension startup:
// In activate(), after all synchronous setup is done:
checkForAtlasUpdates(context).catch(() => {})
Key design decisions:
- Uses
rokit update --check: Lets rokit handle version resolution natively -- no GitHub API calls, no manual semver parsing - Project-level first, then global: Checks the project's
rokit.tomlfirst (if workspace is open), falls back to global. When updating, uses the same scope that found the update - Throttle: Once per day via
globalState, so frequent restarts don't spam the user - Silent failures: Every step is wrapped in try/catch that returns early. Missing rokit, no atlas configured, network errors -- all silently ignored
- Non-blocking: Called without
awaitso extension activation is never delayed
Summary of What's Working Correctly
- Asset name regex (
-(?<platform>\w+)-(?<arch>\w+)\.zip$) correctly matches all 6 rokit 1.2.0 release assets (linux/macos/windows x x86_64/aarch64, all.zip) - Platform/arch mapping covers all common Node.js platforms (win32, darwin, linux) and architectures (x64, arm64)
rokit self-installcorrectly sets up~/.rokit/binin user PATH on all platformschmod 0o755is correctly applied on Unix after extractiongetRojoInstallcorrectly detects rokit-managed vs globally installed atlas
Files to Change
- vscode-rojo/src/installRojo.ts -- Bugs 2, 3, 4, 5, 6, 7
- vscode-rojo/src/commands/openMenu.ts -- Bug 1, Check for Updates menu item
- vscode-rojo/src/checkForUpdates.ts -- NEW: background update check logic
- vscode-rojo/src/extension.ts -- Call checkForAtlasUpdates() on activation