Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,17 @@

## 1.0.0

Initial release.
Initial public release.

- Compact VM details on Enter, with a shared hostname, resource summary, metadata
grid, and browser/SSH buttons; no duplicate SSH/web address blocks.
- Live human-readable creation age beside the VM name.
- Confirmed restart, pause/resume, and copy operations with admin checks and
bounded background commands.
- E opens the exe.dev REPL in a new floating terminal.
- Row-wide click action: SSH by default, configurable to browser or details.
- Persistent S/B shortcut guide with no per-row action buttons.
- Details track VM identity across refreshes and preserve the filtered list on return.

- Native Omarchy Quattro bar widget for exe.dev virtual machines.
- Tag-grouped VM list with browser and SSH actions.
Expand Down
18 changes: 16 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ There are no npm dependencies or build step. Use Node.js 22 or newer:
```sh
node --test tests/*.test.cjs
omarchy plugin validate .
qmllint -I "$OMARCHY_PATH/shell" Widget.qml VmService.qml
qmllint -I "$OMARCHY_PATH/shell" Widget.qml VmService.qml VmNavigation.qml VmDetail.qml ShortcutHint.qml
git diff --check
```

Expand All @@ -35,7 +35,21 @@ On a supported Omarchy session, test:
letters does not activate them; Enter returns to the list without launching.
- Arrow navigation, scrolling, browser/SSH selection, and hover selection
target the same displayed VM, including after a refresh or filter change.
- Both launch actions, access-disabled buttons, refresh, and middle-click.
- S/B launch from both list and details; list Enter opens details; details Enter
launches the selected action. Escape returns to the same query and selection.
- Row click defaults to SSH; test browser/details click settings, including
access-denied actions. There must be no action buttons on list rows.
- Detail refresh tracks the same VM by name; removal returns to the list safely.
- SSH/browser buttons, access-disabled buttons, refresh, and middle-click.
- Creation age boundaries (just now/minutes/hours/days), live age updates, and
missing/invalid timestamps; the exact date should only appear on hover.
- E opens a new floating terminal in the exe.dev REPL from the list, including
an empty account. While filtering, E must type normally.
- Management actions require explicit admin access and named confirmation.
Escape cancels; Enter confirms once; no S/B shortcut may bypass confirmation.
Check success/error/timeout messages and the refreshed list afterward.
Exercise restart, pause/resume, and copy only on a disposable VM with the
account owner's permission; copy can consume additional capacity.
- Empty account, failed authentication, unavailable network, malformed response,
and recovery; stale results remain visible after a failed refresh.
- Disable/re-enable, Git-based update, and removal. Do not remove a checkout
Expand Down
102 changes: 102 additions & 0 deletions Model.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,79 @@ function normalizeVm(raw) {
// The requested canonical VM URL, not arbitrary server-provided schemes.
url: "https://" + name + ".exe.xyz/",
status: String(raw.status || "unknown"),
cpus: metadataNumber(raw.allocated_cpus),
memoryBytes: metadataNumber(raw.memory_capacity_bytes),
diskBytes: metadataNumber(raw.disk_capacity_bytes),
region: typeof raw.region === "string" ? raw.region : "",
regionDisplay: typeof raw.region_display === "string" ? raw.region_display : "",
image: typeof raw.image === "string" ? raw.image : "",
createdAt: typeof raw.created_at === "string" ? raw.created_at : "",
proxyPort: Number.isInteger(raw.proxy_port) && raw.proxy_port > 0 && raw.proxy_port <= 65535 ? raw.proxy_port : 0,
egressPolicy: typeof raw.egress_policy === "string" ? raw.egress_policy : "",
emailReceiveEnabled: typeof raw.email_receive_enabled === "boolean" ? raw.email_receive_enabled : null,
publicWeb: raw.proxy_share === "public" || !!(raw.sharing && raw.sharing.public_proxy),
adminAllowed: !!(raw.access && raw.access.admin === true),
shellAllowed: !(raw.access && raw.access.shell === false),
webAllowed: !(raw.access && raw.access.web === false)
};
}

// Optional display metadata must not invent resource values when absent.
function metadataNumber(value) {
return typeof value === "number" && isFinite(value) && value > 0 ? value : 0;
}

function formatBytes(bytes) {
if (!metadataNumber(bytes)) return "";
var units = ["B", "KiB", "MiB", "GiB", "TiB"];
var unit = 0;
while (bytes >= 1024 && unit < units.length - 1) { bytes /= 1024; unit++; }
return Math.round(bytes * 10) / 10 + " " + units[unit];
}

function resourceSummary(vm) {
if (!vm) return "";
var parts = [];
if (vm.cpus) parts.push(vm.cpus + (vm.cpus === 1 ? " vCPU" : " vCPUs"));
if (vm.memoryBytes) parts.push(formatBytes(vm.memoryBytes) + " RAM");
if (vm.diskBytes) parts.push(formatBytes(vm.diskBytes) + " disk");
return parts.join(" · ");
}

function networkSummary(vm) {
if (!vm) return "";
var parts = [];
if (vm.proxyPort) parts.push("Port " + vm.proxyPort);
parts.push(vm.publicWeb ? "Public web" : "Private web");
if (vm.egressPolicy)
parts.push("Egress: " + (vm.egressPolicy === "all" ? "all destinations" : vm.egressPolicy === "integrations" ? "integrations only" : vm.egressPolicy));
return parts.join(" · ");
}

function createdAgo(createdAt, now) {
if (!createdAt) return "";
var timestamp = Date.parse(createdAt);
if (!isFinite(timestamp)) return "";
if (typeof now !== "number" || !isFinite(now)) now = Date.now();
var seconds = Math.max(0, (now - timestamp) / 1000);
if (seconds < 60) return "Created just now";
var count, unit;
if (seconds < 3600) { count = Math.floor(seconds / 60); unit = "minute"; }
else if (seconds < 86400) { count = Math.floor(seconds / 3600); unit = "hour"; }
else { count = Math.floor(seconds / 86400); unit = "day"; }
return "Created " + count + " " + unit + (count === 1 ? "" : "s") + " ago";
}

function detailFacts(vm) {
if (!vm) return [];
var facts = [];
if (vm.region || vm.regionDisplay)
facts.push({label: "Region", value: vm.region ? vm.region.toUpperCase() : vm.regionDisplay, tooltip: vm.regionDisplay || ""});
if (typeof vm.emailReceiveEnabled === "boolean")
facts.push({label: "Email", value: vm.emailReceiveEnabled ? "Receive on" : "Receive off"});
return facts;
}

function parseVms(text) {
var data = JSON.parse(text);
if (!data || !Array.isArray(data.vms))
Expand Down Expand Up @@ -123,6 +190,41 @@ function sshCommand(vm) {
return ["omarchy-launch-terminal", "ssh", "--", safe.sshDest];
}

// Fixed command/host and validated arguments only: SSH joins remote arguments
// into a command string, so never pass arbitrary metadata or an arbitrary verb.
function managementFailure(exitCode, detail, exitStatus) {
// QProcess::NormalExit is 0; CrashExit carries the raw signal number as
// exitCode, not necessarily the shell-style 128 + signal encoding.
var crashed = exitStatus !== undefined && exitStatus !== 0;
var uncertain = crashed || exitCode === 124 || exitCode >= 128 || exitCode < 0;
var message = "";
if (uncertain) {
message = (exitCode === 124 || exitCode === 137 ? "Request timed out" : "Connection lost or command interrupted")
+ "; the operation may have completed remotely. Refresh and check the VM inventory before retrying, especially before making another copy.";
}
if (detail) message += (message ? "\n" : "") + String(detail);
return {uncertain: uncertain, message: (message || "exe.dev command failed (exit " + exitCode + ").").slice(0, 500)};
}

function managementCommand(vm, action) {
if (["restart", "pause", "resume", "copy"].indexOf(action) < 0)
throw new Error("Unsupported VM action");
if (!vm || vm.adminAllowed !== true) throw new Error("VM administration is not allowed");
var safe = normalizeVm({vm_name: vm.name});
return ["timeout", "--kill-after=2s", "120s", "ssh",
"-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=yes",
"-o", "ConnectTimeout=8", "-o", "ConnectionAttempts=1",
"-o", "ServerAliveInterval=5", "-o", "ServerAliveCountMax=2",
"exe.dev", action === "copy" ? "cp" : action, safe.name, "--json"];
}

function replCommand() {
// Omarchy floats this app-id. xdg-terminal-exec adapts it to the user's
// configured terminal; no transient Hyprland rule or shell wrapper needed.
return ["omarchy-launch-terminal", "--app-id=org.omarchy.terminal",
"--title=exe.dev", "ssh", "exe.dev"];
}

function browserCommand(vm) {
var safe = normalizeVm({vm_name: vm.name});
return ["omarchy-launch-browser", safe.url];
Expand Down
85 changes: 74 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ An independent community plugin, not an official exe.dev or Omarchy product.
- Lists VMs grouped by their first tag, sorted by name within each group.
- Press `/` to fuzzy-find by VM name, with a flat, best-match-first result list.
- Shows emoji, description, tags, status, and public-web visibility.
- Separate browser and SSH actions, respecting access restrictions from the API.
- Minimal VM details on Enter, with browser/SSH buttons and full connection info.
- Always-visible B/S shortcut guide; quick-launch actions respect API access restrictions.
- Click a VM to SSH by default, configurable to browser or details.
- Relative creation age beside the VM name; restart, pause/resume, and copy actions
with explicit confirmation and administration-access checks.
- E opens the exe.dev REPL in a new floating terminal.
- Keyboard navigation, scrolling, and theme-aware styling.
- Refreshes on open and periodically while open; retains the last successful list
in memory when a query fails.
Expand All @@ -24,7 +29,8 @@ An independent community plugin, not an official exe.dev or Omarchy product.
Tested on Omarchy **4.0.6-1**; older Waybar-based releases are not supported.
- OpenSSH (`ssh`) and GNU coreutils (`timeout`).
- Omarchy's `omarchy-launch-browser` and `omarchy-launch-terminal` commands, and
your configured browser and terminal.
your configured browser and terminal. The REPL shortcut uses
`--app-id=org.omarchy.terminal`, which Omarchy's stock window rules float.
- An exe.dev account and SSH authentication usable by the desktop shell.

These desktop tools ship with the supported Omarchy environment. Node.js is only
Expand Down Expand Up @@ -78,13 +84,50 @@ omarchy bar move cmk.exe-dev --section right
## Usage

Click the bar's server icon to open the list. The header globe opens the exe.dev
dashboard; the refresh button reloads the VMs. Each VM has separate browser and
SSH buttons. Middle-click the bar icon to refresh without opening the panel.
dashboard; the refresh button reloads the VMs. Middle-click the bar icon to
refresh without opening the panel.

Hover or use ↑/↓ to select a VM. **S opens SSH; B opens the browser**, directly
from either the list or details. The footer displays these shortcuts without
adding buttons to every row.

**Enter opens details**: the VM's name, emoji and human-readable creation age
become the panel title (for example, "Created just now" or "Created 3 days ago").
The age updates while details are open; hover for the exact timestamp.
Hostname, status, web port, sharing, and egress policy are grouped together
directly below, followed by the description and tags. A dense metadata grid
shows CPU/RAM/disk, region, email receiving and image, when returned by the API. Full image references and clipped values are available on hover.
SSH and Browser buttons retain their full destinations in tooltips.
In details, use the arrow keys to select an action and Enter to activate it,
or use S/B directly for SSH/browser.
Escape or the back arrow returns to the same filtered list and selection.

**Clicking a VM opens SSH by default.** You can change this to browser or details
in the plugin settings; the setting affects mouse clicks only.

The browser action opens `https://<vm-name>.exe.xyz/`. The terminal action runs
SSH using the API's validated `ssh_dest`, including username-routed destinations.
Neither action changes a VM's web-sharing or access settings.

### VM management

Details also provide **Copy**, **Restart**, and **Pause** (or **Resume** for a
paused VM). These controls require the API's administration permission and ask
for confirmation naming the target VM. Enter confirms; Escape cancels without
executing a command.

Copy uses exe.dev's auto-generated name and consumes additional VM capacity.
Restart interrupts running work and connections; pause suspends the VM until
resumed. Commands run in the background, display their outcome, and refresh the
list afterward. Only one management command runs at a time. Requests have a
120-second deadline. Timeouts or SSH disconnects can leave the outcome unknown:
management stays disabled until a subsequent inventory refresh succeeds. Check
the refreshed inventory before retrying, especially before making another copy.
Confirmation ignores held-key repeats and requires a fresh Enter press.

On the main list, **E** opens a new floating terminal running `ssh exe.dev`.
It does not act on the selected VM. While typing a filter, E is ordinary text.

### Keyboard

| Key | Action |
Expand All @@ -93,9 +136,13 @@ Neither action changes a VM's web-sharing or access settings.
| Enter while filtering | Return focus to the filtered list without opening a VM |
| Escape while filtering | Clear the filter and return focus to the list |
| ↑ / ↓ or K / J | Select VM |
| ← / → or H / L | Select browser / SSH action |
| Enter / Space | Open the selected action (initially SSH) |
| Enter / Space in the list | Open details for the selected VM |
| Arrows or H / J / K / L in details | Select a launch or management action |
| Enter / Space in details | Launch the selected action |
| Escape in details | Return to the list, keeping the query and selection |
| B / S | Open the selected VM in browser / SSH |
| E in the list | Open the exe.dev REPL in a floating terminal |
| Enter / Escape at a management confirmation | Confirm / cancel the named operation |
| R | Refresh |
| Escape in the list | Close the popout |
| Tab / Shift+Tab in the list | Switch to an adjacent Omarchy panel |
Expand All @@ -112,13 +159,27 @@ query. Navigation and actions always refer to the displayed results.

## Settings

Choose what happens when you click a VM:

```sh
omarchy bar set cmk.exe-dev clickAction ssh # default
omarchy bar set cmk.exe-dev clickAction browser
omarchy bar set cmk.exe-dev clickAction details
```

Enter in the list always opens details, regardless of this setting. S and B
always launch SSH and browser respectively. If the API denies the chosen action,
the plugin does not launch it or silently substitute another action.

Configure periodic refresh:

```sh
omarchy bar set cmk.exe-dev refreshIntervalSec 120
```

The default is 60 seconds; periodic refresh is clamped to at least 15 seconds and
only runs while the popout is open. There is also an initial query when the
widget loads and a refresh on opening. Overlapping queries are skipped.
widget loads and a refresh on opening. Overlapping refresh requests are coalesced.

Queries have an 8-second connection timeout and a 20-second total deadline, with
forced termination after a further 2 seconds if needed. A successful response
Expand Down Expand Up @@ -170,7 +231,7 @@ sharing logs or filing an issue.
```sh
node --test tests/*.test.cjs
omarchy plugin validate .
qmllint -I "$OMARCHY_PATH/shell" Widget.qml VmService.qml
qmllint -I "$OMARCHY_PATH/shell" Widget.qml VmService.qml VmNavigation.qml VmDetail.qml ShortcutHint.qml
```

CI runs the dependency-free Node tests, manifest/repository contract checks, and
Expand All @@ -183,9 +244,11 @@ release checklist, and [CHANGELOG.md](CHANGELOG.md) for release notes.

## Security and scope

The plugin lists VMs and launches browser/SSH actions. It does not create, delete,
stop, resize, unlock, or change sharing on VMs. Commands use argument arrays, not
a shell or the API's `ssh_command` string. VM names and SSH destinations are
The plugin lists VMs, launches browser/SSH sessions and the lobby REPL, and can
restart, pause/resume, or copy a VM after explicit confirmation. It does not
delete, resize, unlock, or change sharing on VMs. Management requires current
administration permission and uses a fixed allowlist of commands. Commands use
argument arrays, not a local shell or the API's `ssh_command` string. VM names and SSH destinations are
validated before launch, and server metadata is rendered as plain text.

Plugins are **not sandboxed**. These boundaries are implementation choices, not
Expand Down
16 changes: 15 additions & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,22 @@ The plugin:
- Uses argument arrays and an SSH option terminator instead of shell-evaluated
VM metadata; it never evaluates the API's `ssh_command`.
- Renders API-provided metadata as plain text.
- Offers restart, pause/resume, and copy only after a confirmation naming the
target VM, and only when the latest API record explicitly grants admin access.
These commands use the fixed exe.dev host and allowlisted verbs; names are
validated before remote SSH command serialization. Copy creates a VM and may
consume additional capacity; restart/pause can interrupt running work.
- Serializes management requests and bounds them to 120 seconds plus a 2-second
forced-termination grace period. A timeout or SSH disconnect does not prove
the remote operation failed: management remains disabled until an inventory
query started afterward succeeds. Check that inventory before retrying,
particularly before copying. Confirmation rejects held-key autorepeat.
- Opens the lobby REPL on E using a new terminal with Omarchy's stock floating
app-id. Commands typed by the user in that REPL are outside the plugin's
allowlist and confirmation UI.
- Does not install packages, request elevated privileges, or change SSH config,
keys, known-hosts entries, VM lifecycle, access, or sharing settings.
keys, known-hosts entries, VM access, or sharing settings. No delete action is
exposed.

Opening SSH intentionally gives you an interactive session on the selected VM.
Opening a website exposes the browser to that VM's content. SSH forwarding,
Expand Down
40 changes: 40 additions & 0 deletions ShortcutHint.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import QtQuick
import qs.Commons
import qs.Commons as Commons

// A guide, not a button: no pointer handler or keyboard focus.
Row {
id: root
required property string shortcut
required property string label
property color foreground: Commons.Color.foreground
property string fontFamily: Style.font.family
spacing: Style.space(6)

Rectangle {
width: keyText.implicitWidth + Style.space(10)
height: keyText.implicitHeight + Style.space(6)
radius: Style.space(4)
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.045)
border.width: 1
border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.2)

Text {
id: keyText
anchors.centerIn: parent
text: root.shortcut
textFormat: Text.PlainText
color: root.foreground
font.family: root.fontFamily
font.pixelSize: Style.font.caption
}
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: root.label
textFormat: Text.PlainText
color: Qt.darker(root.foreground, 1.4)
font.family: root.fontFamily
font.pixelSize: Style.font.caption
}
}
Loading