diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1c2e7f4..18311e0 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,39 +1,24 @@
name: CI
+# Runners: uvacompute (https://uvacompute.com)
+# To enable, set the UVA_RUNNER repo variable to the correct runner label.
+# runs-on: ${{ vars.UVA_RUNNER || 'ubuntu-latest' }}
+
on:
pull_request:
branches: [main]
push:
branches: [main]
workflow_dispatch:
- inputs:
- bump:
- description: Version bump type (only for workflow_dispatch)
- type: choice
- options:
- - patch
- - minor
- - major
- default: patch
- publish_npm:
- description: Publish to npm
- type: boolean
- default: true
- publish_crates:
- description: Publish to crates.io
- type: boolean
- default: true
-
-env:
- FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
permissions:
contents: write
+ packages: write
jobs:
changes:
name: Changes
- runs-on: [self-hosted, netty]
+ runs-on: ubuntu-latest
outputs:
rust: ${{ steps.check.outputs.rust }}
version: ${{ steps.version.outputs.version }}
@@ -52,11 +37,7 @@ jobs:
- 'tests/**'
- 'Cargo.toml'
- 'Cargo.lock'
- - 'npm/**'
- - 'flake.nix'
- - 'flake.lock'
- 'docker/**'
- - '.github/workflows/**'
- 'Makefile'
- name: Set outputs
@@ -72,36 +53,34 @@ jobs:
id: version
if: github.event_name != 'pull_request' && steps.check.outputs.rust == 'true'
run: |
- CURRENT=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/')
- IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT"
+ BASE=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/')
+ IFS='.' read -r MAJOR MINOR PATCH <<< "$BASE"
- BUMP="${{ inputs.bump || 'patch' }}"
- case "$BUMP" in
- major) MAJOR=$((MAJOR + 1)); MINOR=0; PATCH=0 ;;
- minor) MINOR=$((MINOR + 1)); PATCH=0 ;;
- patch)
- LATEST=$(git tag -l "v${MAJOR}.${MINOR}.*" | sort -V | tail -1)
- if [ -z "$LATEST" ]; then
- NEW_PATCH=$PATCH
- else
- LATEST_VER="${LATEST#v}"
- IFS='.' read -r _ _ LATEST_PATCH <<< "$LATEST_VER"
- NEW_PATCH=$((LATEST_PATCH + 1))
- fi
- PATCH=$NEW_PATCH
- ;;
- esac
+ LATEST=$(git tag -l "v${MAJOR}.${MINOR}.*" | sort -V | tail -1)
+
+ if [ -z "$LATEST" ]; then
+ NEW="$BASE"
+ else
+ LATEST_VER="${LATEST#v}"
+ IFS='.' read -r _ _ LATEST_PATCH <<< "$LATEST_VER"
+ NEW_PATCH=$((LATEST_PATCH + 1))
+ NEW="${MAJOR}.${MINOR}.${NEW_PATCH}"
+ fi
+
+ # Ensure the computed version does not already have a tag
+ while git rev-parse "v${NEW}" >/dev/null 2>&1; do
+ IFS='.' read -r MAJOR MINOR PATCH <<< "$NEW"
+ NEW="${MAJOR}.${MINOR}.$((PATCH + 1))"
+ done
- NEW="${MAJOR}.${MINOR}.${PATCH}"
echo "version=${NEW}" >> "$GITHUB_OUTPUT"
echo "tag=v${NEW}" >> "$GITHUB_OUTPUT"
- echo "Computed version: ${NEW} (v${NEW})"
validate:
name: Validate
needs: changes
if: needs.changes.outputs.rust == 'true'
- runs-on: [self-hosted, netty]
+ runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -125,6 +104,9 @@ jobs:
- name: Install site dependencies
run: pnpm --dir site install --frozen-lockfile
+ - name: Install system dependencies
+ run: sudo apt-get update && sudo apt-get install -y libx11-dev libxtst-dev
+
- name: Format check
run: make fmt-check
@@ -141,7 +123,7 @@ jobs:
name: Integration (Xvfb)
needs: changes
if: needs.changes.outputs.rust == 'true'
- runs-on: [self-hosted, netty]
+ runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -149,35 +131,79 @@ jobs:
- uses: Swatinem/rust-cache@v2
+ - name: Install system dependencies
+ run: sudo apt-get update && sudo apt-get install -y libx11-dev libxtst-dev xvfb
+
- name: Xvfb integration tests
run: make test-integration
- distribution:
- name: Distribution Validate
- needs: changes
- if: needs.changes.outputs.rust == 'true'
- runs-on: [self-hosted, netty]
+ build:
+ name: Build (${{ matrix.target }})
+ needs: [changes, validate, integration]
+ if: github.event_name != 'pull_request' && needs.changes.outputs.rust == 'true'
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: true
+ matrix:
+ target: [cargo, docker]
steps:
- uses: actions/checkout@v4
+ # --- Cargo steps ---
- uses: dtolnay/rust-toolchain@stable
+ if: matrix.target == 'cargo'
+ with:
+ components: clippy
- uses: Swatinem/rust-cache@v2
+ if: matrix.target == 'cargo'
- - uses: actions/setup-node@v4
+ - name: Install system dependencies
+ if: matrix.target == 'cargo'
+ run: sudo apt-get update && sudo apt-get install -y libx11-dev libxtst-dev
+
+ - name: Clippy
+ if: matrix.target == 'cargo'
+ run: cargo clippy -- -D warnings
+
+ - name: Build
+ if: matrix.target == 'cargo'
+ run: cargo build --release --locked
+
+ - uses: actions/upload-artifact@v4
+ if: matrix.target == 'cargo'
with:
- node-version: 22
+ name: deskctl-linux-x86_64
+ path: target/release/deskctl
+ retention-days: 7
- - name: Distribution validation
- run: make dist-validate
+ # --- Docker steps ---
+ - uses: docker/setup-buildx-action@v3
+ if: matrix.target == 'docker'
- # --- Release pipeline: update-manifests -> build -> release -> publish ---
- # These stay on ubuntu-latest for artifact upload/download and registry publishing.
+ - uses: docker/login-action@v3
+ if: matrix.target == 'docker'
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - uses: docker/build-push-action@v6
+ if: matrix.target == 'docker'
+ with:
+ context: .
+ file: docker/Dockerfile
+ push: true
+ tags: |
+ ghcr.io/${{ github.repository }}:latest
+ ghcr.io/${{ github.repository }}:${{ needs.changes.outputs.tag }}
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
update-manifests:
name: Update Manifests
- needs: [changes, validate, integration, distribution]
- if: github.event_name != 'pull_request' && needs.changes.outputs.rust == 'true'
+ needs: [changes, build]
+ if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -186,11 +212,7 @@ jobs:
- uses: dtolnay/rust-toolchain@stable
- - uses: actions/setup-node@v4
- with:
- node-version: 22
-
- - name: Update versions
+ - name: Update version in Cargo.toml
run: |
CURRENT=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/')
NEW="${{ needs.changes.outputs.version }}"
@@ -198,70 +220,26 @@ jobs:
sed -i "0,/^version = \"${CURRENT}\"/s//version = \"${NEW}\"/" Cargo.toml
cargo generate-lockfile
fi
- node -e '
- const fs = require("node:fs");
- const p = "npm/deskctl/package.json";
- const pkg = JSON.parse(fs.readFileSync(p, "utf8"));
- pkg.version = process.argv[1];
- fs.writeFileSync(p, JSON.stringify(pkg, null, 2) + "\n");
- ' "$NEW"
- name: Commit, tag, and push
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- git add Cargo.toml Cargo.lock npm/deskctl/package.json
- if ! git diff --cached --quiet; then
+
+ if ! git diff --quiet; then
+ git add Cargo.toml Cargo.lock
git commit -m "release: ${{ needs.changes.outputs.tag }} [skip ci]"
fi
- git tag "${{ needs.changes.outputs.tag }}"
- git push origin main --tags
- build:
- name: Build Release Asset
- needs: [changes, update-manifests]
- if: github.event_name != 'pull_request' && needs.changes.outputs.rust == 'true'
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- with:
- ref: ${{ needs.changes.outputs.tag }}
-
- - uses: dtolnay/rust-toolchain@stable
- with:
- components: clippy
-
- - uses: Swatinem/rust-cache@v2
-
- - name: Install system dependencies
- run: sudo apt-get update && sudo apt-get install -y libx11-dev libxtst-dev
-
- - name: Verify version
- run: |
- CARGO_VER=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/')
- EXPECTED="${{ needs.changes.outputs.version }}"
- if [ "$CARGO_VER" != "$EXPECTED" ]; then
- echo "Version mismatch: Cargo.toml=$CARGO_VER expected=$EXPECTED"
- exit 1
+ if ! git rev-parse "${{ needs.changes.outputs.tag }}" >/dev/null 2>&1; then
+ git tag "${{ needs.changes.outputs.tag }}"
fi
- echo "Building version $CARGO_VER"
-
- - name: Clippy
- run: cargo clippy -- -D warnings
-
- - name: Build
- run: cargo build --release --locked
-
- - uses: actions/upload-artifact@v4
- with:
- name: deskctl-linux-x86_64
- path: target/release/deskctl
- retention-days: 7
+ git push origin main --tags
release:
name: Release
needs: [changes, build, update-manifests]
- if: github.event_name != 'pull_request' && needs.changes.outputs.rust == 'true'
+ if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -278,87 +256,9 @@ jobs:
chmod +x artifacts/deskctl
mv artifacts/deskctl artifacts/deskctl-linux-x86_64
cd artifacts && sha256sum deskctl-linux-x86_64 > checksums.txt && cd ..
- if gh release view "${{ needs.changes.outputs.tag }}" >/dev/null 2>&1; then
- gh release upload "${{ needs.changes.outputs.tag }}" \
- artifacts/deskctl-linux-x86_64 \
- artifacts/checksums.txt \
- --clobber
- else
- gh release create "${{ needs.changes.outputs.tag }}" \
- --title "${{ needs.changes.outputs.tag }}" \
- --generate-notes \
- artifacts/deskctl-linux-x86_64 \
- artifacts/checksums.txt
- fi
- publish-npm:
- name: Publish npm
- needs: [changes, update-manifests, release]
- if: >-
- github.event_name != 'pull_request'
- && needs.changes.outputs.rust == 'true'
- && (inputs.publish_npm == true || inputs.publish_npm == '')
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- with:
- ref: ${{ needs.changes.outputs.tag }}
-
- - uses: actions/setup-node@v4
- with:
- node-version: 22
- registry-url: https://registry.npmjs.org
-
- - name: Check if already published
- id: published
- run: |
- VERSION="${{ needs.changes.outputs.version }}"
- if npm view "deskctl@${VERSION}" version >/dev/null 2>&1; then
- echo "npm=true" >> "$GITHUB_OUTPUT"
- else
- echo "npm=false" >> "$GITHUB_OUTPUT"
- fi
-
- - name: Validate npm package
- if: steps.published.outputs.npm != 'true'
- run: node npm/deskctl/scripts/validate-package.js
-
- - name: Publish npm
- if: steps.published.outputs.npm != 'true'
- env:
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- run: npm publish ./npm/deskctl --access public
-
- publish-crates:
- name: Publish crates.io
- needs: [changes, update-manifests, release]
- if: >-
- github.event_name != 'pull_request'
- && needs.changes.outputs.rust == 'true'
- && (inputs.publish_crates == true || inputs.publish_crates == '')
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- with:
- ref: ${{ needs.changes.outputs.tag }}
-
- - uses: dtolnay/rust-toolchain@stable
-
- - name: Install system dependencies
- run: sudo apt-get update && sudo apt-get install -y libx11-dev libxtst-dev
-
- - name: Check if already published
- id: published
- run: |
- VERSION="${{ needs.changes.outputs.version }}"
- if curl -fsSL "https://crates.io/api/v1/crates/deskctl/${VERSION}" >/dev/null 2>&1; then
- echo "crates=true" >> "$GITHUB_OUTPUT"
- else
- echo "crates=false" >> "$GITHUB_OUTPUT"
- fi
-
- - name: Publish crates.io
- if: steps.published.outputs.crates != 'true'
- env:
- CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
- run: cargo publish --locked
+ gh release create "${{ needs.changes.outputs.tag }}" \
+ --title "${{ needs.changes.outputs.tag }}" \
+ --generate-notes \
+ artifacts/deskctl-linux-x86_64 \
+ artifacts/checksums.txt
diff --git a/.gitignore b/.gitignore
index 40542a9..7406874 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,5 +5,3 @@ secret/
.claude/
.codex/
openspec/
-npm/deskctl/vendor/
-npm/deskctl/*.tgz
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 97e8c7c..7a1a2a2 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -21,7 +21,7 @@ pnpm --dir site install
- `src/` holds production code and unit tests
- `tests/` holds integration tests
- `tests/support/` holds shared X11 and daemon helpers for integration coverage
-- `docs/runtime-contract.md` is the stable-vs-best-effort runtime output contract for agent-facing CLI work
+- `docs/runtime-output.md` is the stable-vs-best-effort runtime output contract for agent-facing CLI work
Keep integration-only helpers out of `src/`.
@@ -35,15 +35,10 @@ make lint
make test-unit
make test-integration
make site-format-check
-make cargo-publish-dry-run
-make npm-package-check
-make nix-flake-check
-make dist-validate
make validate
```
`make validate` runs the full Phase 2 validation stack. It requires Linux, `xvfb-run`, and site dependencies to be installed.
-`make dist-validate` runs the distribution validation stack. It requires `npm`, `nix`, and Linux for the full npm runtime smoke path.
## Pre-commit Hooks
@@ -65,19 +60,6 @@ The hook config intentionally stays small:
- Site files reuse the existing `site/` Prettier setup
- Slower checks stay in CI or `make validate`
-## Distribution Work
-
-Distribution support currently ships through:
-
-- crate: `deskctl`
-- npm package: `deskctl`
-- repo flake: `flake.nix`
-- command name on every channel: `deskctl`
-
-For maintainer release and publish steps, see [docs/releasing.md](docs/releasing.md).
-
-Source-build and packaging work should keep Docker as a local Linux build convenience, not as the canonical registry release path.
-
## Integration Tests
Integration coverage is Linux/X11-only in this phase. The supported local entrypoint is:
diff --git a/Cargo.lock b/Cargo.lock
index eb0e2ce..1355d04 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
-version = 3
+version = 4
[[package]]
name = "ab_glyph"
@@ -241,9 +241,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
[[package]]
name = "cc"
-version = "1.2.58"
+version = "1.2.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1"
+checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423"
dependencies = [
"find-msvc-tools",
"jobserver",
@@ -400,7 +400,7 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]]
name = "deskctl"
-version = "0.1.14"
+version = "0.1.5"
dependencies = [
"ab_glyph",
"anyhow",
@@ -911,9 +911,9 @@ dependencies = [
[[package]]
name = "js-sys"
-version = "0.3.92"
+version = "0.3.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc4c90f45aa2e6eacbe8645f77fdea542ac97a494bcd117a67df9ff4d611f995"
+checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c"
dependencies = [
"once_cell",
"wasm-bindgen",
@@ -1039,9 +1039,9 @@ dependencies = [
[[package]]
name = "mio"
-version = "1.2.0"
+version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
+checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
dependencies = [
"libc",
"wasi",
@@ -1699,9 +1699,9 @@ dependencies = [
[[package]]
name = "simd-adler32"
-version = "0.3.9"
+version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
+checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
[[package]]
name = "simd_helpers"
@@ -1861,9 +1861,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
-version = "1.23.0"
+version = "1.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9"
+checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37"
dependencies = [
"getrandom 0.4.2",
"js-sys",
@@ -1907,9 +1907,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen"
-version = "0.2.115"
+version = "0.2.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6523d69017b7633e396a89c5efab138161ed5aafcbc8d3e5c5a42ae38f50495a"
+checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e"
dependencies = [
"cfg-if",
"once_cell",
@@ -1920,9 +1920,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
-version = "0.2.115"
+version = "0.2.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4e3a6c758eb2f701ed3d052ff5737f5bfe6614326ea7f3bbac7156192dc32e67"
+checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -1930,9 +1930,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
-version = "0.2.115"
+version = "0.2.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "921de2737904886b52bcbb237301552d05969a6f9c40d261eb0533c8b055fedf"
+checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3"
dependencies = [
"bumpalo",
"proc-macro2",
@@ -1943,9 +1943,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
-version = "0.2.115"
+version = "0.2.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a93e946af942b58934c604527337bad9ae33ba1d5c6900bbb41c2c07c2364a93"
+checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16"
dependencies = [
"unicode-ident",
]
@@ -2297,9 +2297,9 @@ dependencies = [
[[package]]
name = "zune-jpeg"
-version = "0.5.15"
+version = "0.5.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296"
+checksum = "0b7a1c0af6e5d8d1363f4994b7a091ccf963d8b694f7da5b0b9cceb82da2c0a6"
dependencies = [
"zune-core",
]
diff --git a/Cargo.toml b/Cargo.toml
index be051c7..023e18a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,23 +1,10 @@
[package]
name = "deskctl"
-version = "0.1.14"
+version = "0.1.5"
edition = "2021"
description = "X11 desktop control CLI for agents"
license = "MIT"
repository = "https://github.com/harivansh-afk/deskctl"
-homepage = "https://github.com/harivansh-afk/deskctl"
-readme = "README.md"
-keywords = ["x11", "desktop", "automation", "cli", "agent"]
-categories = ["command-line-utilities"]
-rust-version = "1.75"
-include = [
- "/Cargo.toml",
- "/Cargo.lock",
- "/README.md",
- "/LICENCE",
- "/assets/**",
- "/src/**",
-]
[dependencies]
clap = { version = "4", features = ["derive", "env"] }
diff --git a/Makefile b/Makefile
index 7e1f852..bb02037 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-.PHONY: fmt fmt-check lint test-unit test-integration site-format-check cargo-publish-dry-run npm-package-check nix-flake-check dist-validate validate
+.PHONY: fmt fmt-check lint test-unit test-integration site-format-check validate
fmt:
cargo fmt --all
@@ -30,34 +30,4 @@ site-format-check:
fi
pnpm --dir site format:check
-cargo-publish-dry-run:
- cargo publish --dry-run --allow-dirty --locked
-
-npm-package-check:
- @if ! command -v npm >/dev/null 2>&1; then \
- echo "npm is required for npm packaging validation."; \
- exit 1; \
- fi
- node npm/deskctl/scripts/validate-package.js
- rm -rf tmp/npm-pack tmp/npm-install
- mkdir -p tmp/npm-pack tmp/npm-install/bin
- npm pack ./npm/deskctl --pack-destination ./tmp/npm-pack >/dev/null
- @if [ "$$(uname -s)" != "Linux" ]; then \
- echo "Skipping npm package runtime smoke test on non-Linux host."; \
- else \
- cargo build && \
- PACK_TGZ=$$(ls ./tmp/npm-pack/*.tgz | head -n 1) && \
- DESKCTL_BINARY_PATH="$$(pwd)/target/debug/deskctl" npm install --prefix ./tmp/npm-install "$${PACK_TGZ}" && \
- ./tmp/npm-install/node_modules/.bin/deskctl --version; \
- fi
-
-nix-flake-check:
- @if ! command -v nix >/dev/null 2>&1; then \
- echo "nix is required for flake validation."; \
- exit 1; \
- fi
- nix flake check
-
-dist-validate: test-unit cargo-publish-dry-run npm-package-check nix-flake-check
-
validate: fmt-check lint test-unit test-integration site-format-check
diff --git a/README.md b/README.md
index dccbe04..6920615 100644
--- a/README.md
+++ b/README.md
@@ -1,46 +1,213 @@
# deskctl
-[](https://www.npmjs.com/package/deskctl)
-[](skills/deskctl)
-
-Desktop control cli for AI agents on X11.
-
-https://github.com/user-attachments/assets/e820787e-4d1a-463f-bdcf-a829588778bf
+Desktop control CLI for AI agents on Linux X11.
## Install
```bash
-npm install -g deskctl
+cargo install deskctl
```
+Build a Linux binary with Docker:
+
```bash
-deskctl doctor
-deskctl snapshot --annotate
+docker compose -f docker/docker-compose.yml run --rm build
```
-## Skill
+This writes `dist/deskctl-linux-x86_64`.
+
+Copy it to an SSH machine where `scp` is unavailable:
```bash
-npx skills add harivansh-afk/deskctl
+ssh -p 443 deskctl@ssh.agentcomputer.ai 'cat > ~/deskctl && chmod +x ~/deskctl' < dist/deskctl-linux-x86_64
```
-## Docs
-
-- runtime contract: [docs/runtime-contract.md](docs/runtime-contract.md)
-- releasing: [docs/releasing.md](docs/releasing.md)
-- contributing: [CONTRIBUTING.md](CONTRIBUTING.md)
-
-## Install paths
-
-Nix:
+Run it on an X11 session:
```bash
-nix run github:harivansh-afk/deskctl -- --help
-nix profile install github:harivansh-afk/deskctl
+DISPLAY=:1 XDG_SESSION_TYPE=x11 ~/deskctl --json snapshot --annotate
```
-Rust:
-
+Local source build requirements:
```bash
cargo build
```
+
+At the moment there are no extra native build dependencies beyond a Rust toolchain.
+
+## Quick Start
+
+```bash
+# Diagnose the environment first
+deskctl doctor
+
+# See the desktop
+deskctl snapshot
+
+# Query focused runtime state
+deskctl get active-window
+deskctl get monitors
+
+# Click a window
+deskctl click @w1
+
+# Type text
+deskctl type "hello world"
+
+# Wait for a window or focus transition
+deskctl wait window --selector 'title=Firefox' --timeout 10
+deskctl wait focus --selector 'class=firefox' --timeout 5
+
+# Focus by explicit selector
+deskctl focus 'title=Firefox'
+```
+
+## Architecture
+
+Client-daemon architecture over Unix sockets (NDJSON wire protocol).
+The daemon starts automatically on first command and keeps the X11 connection alive for fast repeated calls.
+
+Source layout:
+
+- `src/lib.rs` exposes the shared library target
+- `src/main.rs` is the thin CLI wrapper
+- `src/` contains production code and unit tests
+- `tests/` contains Linux/X11 integration tests
+- `tests/support/` contains shared integration helpers
+
+## Runtime Requirements
+
+- Linux with X11 session
+- Rust 1.75+ (for build)
+
+The binary itself only links the standard glibc runtime on Linux (`libc`, `libm`, `libgcc_s`).
+
+For deskctl to be fully functional on a fresh VM you still need:
+
+- an X11 server and an active `DISPLAY`
+- `XDG_SESSION_TYPE=x11` or an equivalent X11 session environment
+- a window manager or desktop environment that exposes standard EWMH properties such as `_NET_CLIENT_LIST_STACKING` and `_NET_ACTIVE_WINDOW`
+- an X server with the extensions needed for input simulation and screen metadata, which is standard on normal desktop X11 setups
+
+If setup fails, run:
+
+```bash
+deskctl doctor
+```
+
+## Contract Notes
+
+- `@wN` refs are short-lived handles assigned by `snapshot` and `list-windows`
+- `--json` output includes a stable `window_id` for programmatic targeting within the current daemon session
+- `list-windows` is a cheap read-only operation and does not capture or write a screenshot
+- the stable runtime JSON/error contract is documented in [docs/runtime-output.md](docs/runtime-output.md)
+
+## Read and Wait Surface
+
+The grouped runtime reads are:
+
+```bash
+deskctl get active-window
+deskctl get monitors
+deskctl get version
+deskctl get systeminfo
+```
+
+The grouped runtime waits are:
+
+```bash
+deskctl wait window --selector 'title=Firefox' --timeout 10
+deskctl wait focus --selector 'id=win3' --timeout 5
+```
+
+Successful `get active-window`, `wait window`, and `wait focus` responses return a `window` payload with:
+- `ref_id`
+- `window_id`
+- `title`
+- `app_name`
+- geometry (`x`, `y`, `width`, `height`)
+- state flags (`focused`, `minimized`)
+
+`get monitors` returns:
+- `count`
+- `monitors[]` with geometry and primary/automatic flags
+
+`get version` returns:
+- `version`
+- `backend`
+
+`get systeminfo` stays runtime-scoped and returns:
+- `backend`
+- `display`
+- `session_type`
+- `session`
+- `socket_path`
+- `screen`
+- `monitor_count`
+- `monitors`
+
+Wait timeout and selector failures are structured in `--json` mode so agents can recover without string parsing.
+
+## Output Policy
+
+Text mode is compact and follow-up-oriented, but JSON is the parsing contract.
+
+- use `--json` when an agent needs strict parsing
+- rely on `window_id`, selector-related fields, grouped read payloads, and structured error `kind` values for stable automation
+- treat monitor naming, incidental whitespace, and default screenshot file names as best-effort
+
+See [docs/runtime-output.md](docs/runtime-output.md) for the exact stable-vs-best-effort breakdown.
+
+## Selector Contract
+
+Explicit selector modes:
+
+```bash
+ref=w1
+id=win1
+title=Firefox
+class=firefox
+focused
+```
+
+Legacy refs remain supported:
+
+```bash
+@w1
+w1
+win1
+```
+
+Bare selectors such as `firefox` are still supported as fuzzy substring matches, but they now fail on ambiguity and return candidate windows instead of silently picking the first match.
+
+## Support Boundary
+
+`deskctl` supports Linux X11 in this phase. Wayland and Hyprland are explicitly out of scope for the current runtime contract.
+
+## Workflow
+
+Local validation uses the root `Makefile`:
+
+```bash
+make fmt-check
+make lint
+make test-unit
+make test-integration
+make site-format-check
+make validate
+```
+
+`make validate` is the full repo-quality check and requires Linux with `xvfb-run` plus `pnpm --dir site install`.
+
+The repository standardizes on `pre-commit` for fast commit-time checks:
+
+```bash
+pre-commit install
+pre-commit run --all-files
+```
+
+See [CONTRIBUTING.md](CONTRIBUTING.md) for the full contributor guide.
+
+## Acknowledgements
+
+- [@barrettruth](github.com/barrettruth) - i stole the website from [vimdoc](https://github.com/barrettruth/vimdoc-language-server)
diff --git a/demo/index.html b/demo/index.html
deleted file mode 100644
index 70ac230..0000000
--- a/demo/index.html
+++ /dev/null
@@ -1,969 +0,0 @@
-
-
-
-
-
-deskctl - Desktop Control for AI Agents
-
-
-
-
-
-
deskctl
-
desktop control CLI for AI agents
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 📝
- task_brief.txt
- 2.1 KB
-
-
- 📊
- nvda_q1_data.csv
- 48 KB
-
-
- 📄
- prev_report.pdf
- 1.2 MB
-
-
- 📁
- archive/
- --
-
-
-
- task: Prepare NVDA Q1 earnings summary
- source: finance.yahoo.com, local csv
- output: Google Docs report with chart
-
-
-
-
-
-
-
-
-
Chrome - Yahoo Finance
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Chrome - Google Docs
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Files
-
Yahoo Finance
-
Google Docs
-
-
-
-
-
-
-
-
-
AI agent controlling a live desktop via deskctl
-
-
-
-
-
-
diff --git a/docs/releasing.md b/docs/releasing.md
deleted file mode 100644
index 849d661..0000000
--- a/docs/releasing.md
+++ /dev/null
@@ -1,110 +0,0 @@
-# Releasing deskctl
-
-This document covers the operator flow for shipping `deskctl` across:
-
-- GitHub Releases
-- crates.io
-- npm
-- the repo flake
-
-GitHub Releases are the canonical binary source. The npm package consumes those release assets instead of building a separate binary.
-
-## Package Names
-
-- crate: `deskctl`
-- npm package: `deskctl`
-- installed command: `deskctl`
-
-## Prerequisites
-
-Before the first live publish on each registry:
-
-- npm ownership for `deskctl`
-- crates.io ownership for `deskctl`
-- repository secrets:
- - `NPM_TOKEN`
- - `CARGO_REGISTRY_TOKEN`
-
-These are user-owned prerequisites. The repo can validate and automate the rest, but it cannot create registry ownership for you.
-
-## Normal Release Flow
-
-1. Merge release-ready changes to `main`.
-2. Let CI run:
- - validation
- - integration
- - distribution validation
- - release asset build
-3. Confirm the GitHub Release exists for the version tag and includes:
- - `deskctl-linux-x86_64`
- - `checksums.txt`
-4. Trigger the `Publish Registries` workflow with:
- - `tag`
- - `publish_npm`
- - `publish_crates`
-5. Confirm the publish summary for each channel.
-
-## What CI Validates
-
-The repository validates:
-
-- `cargo publish --dry-run --locked`
-- npm package metadata and packability
-- npm install smoke path on Linux using the packaged `deskctl` command
-- repo flake evaluation/build
-
-The repository release workflow:
-
-- builds the Linux release binary
-- publishes the canonical GitHub Release asset
-- uploads `checksums.txt`
-
-The registry publish jobs (npm and crates.io run in parallel):
-
-- target an existing release tag
-- check whether that version is already published on the respective registry
-- skip already-published versions
-- both default to enabled; can be toggled via workflow_dispatch inputs
-
-## Rerun Safety
-
-Registry publishing is intentionally separate from release asset creation.
-
-If a partial failure happens:
-
-- GitHub Release assets remain the source of truth
-- rerun the `Publish Registries` workflow for the same tag
-- already-published channels are reported and skipped
-- remaining channels can still be published
-
-## Local Validation
-
-Run the distribution checks locally with:
-
-```bash
-make cargo-publish-dry-run
-make npm-package-check
-make nix-flake-check
-make dist-validate
-```
-
-Notes:
-
-- `make npm-package-check` does a runtime smoke test only on Linux
-- `make nix-flake-check` requires a local Nix installation
-- Docker remains a local Linux build convenience, not the canonical release path
-
-## Nix Boundary
-
-The repo-owned `flake.nix` is the supported Nix surface in this phase.
-
-In scope:
-
-- `nix run github:harivansh-afk/deskctl`
-- `nix profile install github:harivansh-afk/deskctl`
-- CI validation for the repo flake
-
-Out of scope for this phase:
-
-- `nixpkgs` upstreaming
-- extra distro packaging outside the repo
diff --git a/docs/runtime-contract.md b/docs/runtime-contract.md
deleted file mode 100644
index ee4727b..0000000
--- a/docs/runtime-contract.md
+++ /dev/null
@@ -1,70 +0,0 @@
-# deskctl runtime contract
-
-All commands support `--json` and use the same top-level envelope:
-
-```json
-{
- "success": true,
- "data": {},
- "error": null
-}
-```
-
-Use `--json` whenever you need to parse output programmatically.
-
-## Stable window fields
-
-Whenever a response includes a window payload, these fields are stable:
-
-- `ref_id`
-- `window_id`
-- `title`
-- `app_name`
-- `x`
-- `y`
-- `width`
-- `height`
-- `focused`
-- `minimized`
-
-Use `window_id` for stable targeting inside a live daemon session. Use
-`ref_id` or `@wN` for short-lived follow-up actions after `snapshot` or
-`list-windows`.
-
-## Stable grouped reads
-
-- `deskctl get active-window` -> `data.window`
-- `deskctl get monitors` -> `data.count`, `data.monitors`
-- `deskctl get version` -> `data.version`, `data.backend`
-- `deskctl get systeminfo` -> runtime-scoped diagnostic fields such as
- `backend`, `display`, `session_type`, `session`, `socket_path`, `screen`,
- `monitor_count`, and `monitors`
-
-## Stable waits
-
-- `deskctl wait window` -> `data.wait`, `data.selector`, `data.elapsed_ms`,
- `data.window`
-- `deskctl wait focus` -> `data.wait`, `data.selector`, `data.elapsed_ms`,
- `data.window`
-
-## Stable structured error kinds
-
-When a command fails with structured JSON data, these `kind` values are stable:
-
-- `selector_not_found`
-- `selector_ambiguous`
-- `selector_invalid`
-- `timeout`
-- `not_found`
-
-Wait failures may also include `window_not_focused` in the last observation
-payload.
-
-## Best-effort fields
-
-Treat these as useful but non-contractual:
-
-- exact monitor names
-- incidental text formatting in non-JSON mode
-- default screenshot file names when no explicit path was provided
-- environment-dependent ordering details from the window manager
diff --git a/docs/runtime-output.md b/docs/runtime-output.md
new file mode 100644
index 0000000..7312357
--- /dev/null
+++ b/docs/runtime-output.md
@@ -0,0 +1,178 @@
+# Runtime Output Contract
+
+This document defines the current output contract for `deskctl`.
+
+It is intentionally scoped to the current Linux X11 runtime surface.
+It does not promise stability for future Wayland or window-manager-specific features.
+
+## Goals
+
+- Keep `deskctl` fully non-interactive
+- Make text output actionable for quick terminal and agent loops
+- Make `--json` safe for agent consumption without depending on incidental formatting
+
+## JSON Envelope
+
+Every runtime command uses the same top-level JSON envelope:
+
+```json
+{
+ "success": true,
+ "data": {},
+ "error": null
+}
+```
+
+Stable top-level fields:
+
+- `success`
+- `data`
+- `error`
+
+`success` is always the authoritative success/failure bit.
+When `success` is `false`, the CLI exits non-zero in both text mode and `--json` mode.
+
+## Stable Fields
+
+These fields are stable for agent consumption in the current Phase 1 runtime contract.
+
+### Window Identity
+
+Whenever a runtime response includes a window payload, these fields are stable:
+
+- `ref_id`
+- `window_id`
+- `title`
+- `app_name`
+- `x`
+- `y`
+- `width`
+- `height`
+- `focused`
+- `minimized`
+
+`window_id` is the stable public identifier for a live daemon session.
+`ref_id` is a short-lived convenience handle for the current window snapshot/ref map.
+
+### Grouped Reads
+
+`deskctl get active-window`
+
+- stable: `data.window`
+
+`deskctl get monitors`
+
+- stable: `data.count`
+- stable: `data.monitors`
+- stable per monitor:
+ - `name`
+ - `x`
+ - `y`
+ - `width`
+ - `height`
+ - `width_mm`
+ - `height_mm`
+ - `primary`
+ - `automatic`
+
+`deskctl get version`
+
+- stable: `data.version`
+- stable: `data.backend`
+
+`deskctl get systeminfo`
+
+- stable: `data.backend`
+- stable: `data.display`
+- stable: `data.session_type`
+- stable: `data.session`
+- stable: `data.socket_path`
+- stable: `data.screen`
+- stable: `data.monitor_count`
+- stable: `data.monitors`
+
+### Waits
+
+`deskctl wait window`
+`deskctl wait focus`
+
+- stable: `data.wait`
+- stable: `data.selector`
+- stable: `data.elapsed_ms`
+- stable: `data.window`
+
+### Selector-Driven Action Success
+
+For selector-driven action commands that resolve a window target, these identifiers are stable when present:
+
+- `data.ref_id`
+- `data.window_id`
+- `data.title`
+- `data.selector`
+
+This applies to:
+
+- `click`
+- `dblclick`
+- `focus`
+- `close`
+- `move-window`
+- `resize-window`
+
+The exact human-readable text rendering of those commands is not part of the JSON contract.
+
+### Artifact-Producing Commands
+
+`snapshot`
+`screenshot`
+
+- stable: `data.screenshot`
+
+When the command also returns windows, `data.windows` uses the stable window payload documented above.
+
+## Stable Structured Error Kinds
+
+When a runtime command returns structured JSON failure data, these error kinds are stable:
+
+- `selector_not_found`
+- `selector_ambiguous`
+- `selector_invalid`
+- `timeout`
+- `not_found`
+- `window_not_focused` as `data.last_observation.kind` or equivalent observation payload
+
+Stable structured failure fields include:
+
+- `data.kind`
+- `data.selector` when selector-related
+- `data.mode` when selector-related
+- `data.candidates` for ambiguous selector failures
+- `data.message` for invalid selector failures
+- `data.wait`
+- `data.timeout_ms`
+- `data.poll_ms`
+- `data.last_observation`
+
+## Best-Effort Fields
+
+These values are useful but environment-dependent and should be treated as best-effort:
+
+- exact monitor naming conventions
+- EWMH/window-manager-dependent window ordering details
+- cosmetic text formatting in non-JSON mode
+- screenshot file names when the caller did not provide an explicit path
+- command stderr wording outside the structured `kind` classifications above
+
+## Text Mode Expectations
+
+Text mode is intended to stay compact and follow-up-useful.
+
+The exact whitespace/alignment of text output is not stable.
+The following expectations are stable at the behavioral level:
+
+- important runtime reads print actionable identifiers or geometry
+- selector failures print enough detail to recover without `--json`
+- artifact-producing commands print the artifact path
+- window listings print both `@wN` refs and `window_id` values
+
+If an agent needs strict parsing, it should use `--json`.
diff --git a/flake.lock b/flake.lock
deleted file mode 100644
index f194334..0000000
--- a/flake.lock
+++ /dev/null
@@ -1,61 +0,0 @@
-{
- "nodes": {
- "flake-utils": {
- "inputs": {
- "systems": "systems"
- },
- "locked": {
- "lastModified": 1731533236,
- "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
- "owner": "numtide",
- "repo": "flake-utils",
- "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
- "type": "github"
- },
- "original": {
- "owner": "numtide",
- "repo": "flake-utils",
- "type": "github"
- }
- },
- "nixpkgs": {
- "locked": {
- "lastModified": 1774386573,
- "narHash": "sha256-4hAV26quOxdC6iyG7kYaZcM3VOskcPUrdCQd/nx8obc=",
- "owner": "NixOS",
- "repo": "nixpkgs",
- "rev": "46db2e09e1d3f113a13c0d7b81e2f221c63b8ce9",
- "type": "github"
- },
- "original": {
- "owner": "NixOS",
- "ref": "nixos-unstable",
- "repo": "nixpkgs",
- "type": "github"
- }
- },
- "root": {
- "inputs": {
- "flake-utils": "flake-utils",
- "nixpkgs": "nixpkgs"
- }
- },
- "systems": {
- "locked": {
- "lastModified": 1681028828,
- "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
- "owner": "nix-systems",
- "repo": "default",
- "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
- "type": "github"
- },
- "original": {
- "owner": "nix-systems",
- "repo": "default",
- "type": "github"
- }
- }
- },
- "root": "root",
- "version": 7
-}
diff --git a/flake.nix b/flake.nix
deleted file mode 100644
index 1eafbaa..0000000
--- a/flake.nix
+++ /dev/null
@@ -1,77 +0,0 @@
-{
- description = "deskctl - Desktop control CLI for AI agents on Linux X11";
-
- inputs = {
- nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
- flake-utils.url = "github:numtide/flake-utils";
- };
-
- outputs =
- { self, nixpkgs, flake-utils }:
- flake-utils.lib.eachDefaultSystem (
- system:
- let
- pkgs = import nixpkgs { inherit system; };
- lib = pkgs.lib;
- cargoToml = builtins.fromTOML (builtins.readFile ./Cargo.toml);
-
- deskctl =
- pkgs.rustPlatform.buildRustPackage {
- pname = cargoToml.package.name;
- version = cargoToml.package.version;
- src = ./.;
- cargoLock.lockFile = ./Cargo.lock;
- nativeBuildInputs = [ pkgs.pkg-config ];
- buildInputs = lib.optionals pkgs.stdenv.isLinux [
- pkgs.libx11
- pkgs.libxtst
- ];
- doCheck = false;
-
- meta = with lib; {
- description = cargoToml.package.description;
- homepage = cargoToml.package.homepage;
- license = licenses.mit;
- mainProgram = "deskctl";
- platforms = platforms.linux;
- };
- };
- in
- {
- formatter = pkgs.nixfmt;
-
- packages = lib.optionalAttrs pkgs.stdenv.isLinux {
- inherit deskctl;
- default = deskctl;
- };
-
- apps = lib.optionalAttrs pkgs.stdenv.isLinux {
- default = flake-utils.lib.mkApp { drv = deskctl; };
- deskctl = flake-utils.lib.mkApp { drv = deskctl; };
- };
-
- checks = lib.optionalAttrs pkgs.stdenv.isLinux {
- build = deskctl;
- };
-
- devShells.default = pkgs.mkShell {
- packages =
- [
- pkgs.cargo
- pkgs.clippy
- pkgs.nodejs
- pkgs.nixfmt
- pkgs.pkg-config
- pkgs.pnpm
- pkgs.rustc
- pkgs.rustfmt
- ]
- ++ lib.optionals pkgs.stdenv.isLinux [
- pkgs.libx11
- pkgs.libxtst
- pkgs.xorg.xorgserver
- ];
- };
- }
- );
-}
diff --git a/npm/deskctl/README.md b/npm/deskctl/README.md
deleted file mode 100644
index 81f07f4..0000000
--- a/npm/deskctl/README.md
+++ /dev/null
@@ -1,48 +0,0 @@
-# deskctl
-
-`deskctl` installs the command for Linux X11 systems.
-
-## Install
-
-```bash
-npm install -g deskctl
-```
-
-After install, run:
-
-```bash
-deskctl --help
-```
-
-To upgrade version:
-
-```bash
-deskctl upgrade
-```
-
-For non-interactive use:
-
-```bash
-deskctl upgrade --yes
-```
-
-One-shot usage is also supported:
-
-```bash
-npx deskctl --help
-```
-
-## Runtime Support
-
-- Linux
-- X11 session
-- currently packaged release asset: `linux-x64`
-
-`deskctl` downloads the matching GitHub Release binary during install.
-Unsupported targets fail during install with a clear runtime support error instead of installing a broken command.
-
-If you want the Rust source-install path instead, use:
-
-```bash
-cargo install deskctl
-```
diff --git a/npm/deskctl/bin/deskctl.js b/npm/deskctl/bin/deskctl.js
deleted file mode 100644
index b8514cf..0000000
--- a/npm/deskctl/bin/deskctl.js
+++ /dev/null
@@ -1,36 +0,0 @@
-#!/usr/bin/env node
-
-const fs = require("node:fs");
-const { spawn } = require("node:child_process");
-
-const { readPackageJson, releaseTag, supportedTarget, vendorBinaryPath } = require("../scripts/support");
-
-function main() {
- const pkg = readPackageJson();
- const target = supportedTarget();
- const binaryPath = vendorBinaryPath(target);
-
- if (!fs.existsSync(binaryPath)) {
- console.error(
- [
- "deskctl binary is missing from the npm package install.",
- `Expected: ${binaryPath}`,
- `Package version: ${pkg.version}`,
- `Release tag: ${releaseTag(pkg)}`,
- "Try reinstalling deskctl or check that your target is supported."
- ].join("\n")
- );
- process.exit(1);
- }
-
- const child = spawn(binaryPath, process.argv.slice(2), { stdio: "inherit" });
- child.on("exit", (code, signal) => {
- if (signal) {
- process.kill(process.pid, signal);
- return;
- }
- process.exit(code ?? 1);
- });
-}
-
-main();
diff --git a/npm/deskctl/package.json b/npm/deskctl/package.json
deleted file mode 100644
index c676924..0000000
--- a/npm/deskctl/package.json
+++ /dev/null
@@ -1,36 +0,0 @@
-{
- "name": "deskctl",
- "version": "0.1.14",
- "description": "Installable deskctl package for Linux X11 agents",
- "license": "MIT",
- "homepage": "https://github.com/harivansh-afk/deskctl",
- "repository": {
- "type": "git",
- "url": "git+https://github.com/harivansh-afk/deskctl.git"
- },
- "bugs": {
- "url": "https://github.com/harivansh-afk/deskctl/issues"
- },
- "engines": {
- "node": ">=18"
- },
- "bin": {
- "deskctl": "bin/deskctl.js"
- },
- "files": [
- "README.md",
- "bin",
- "scripts"
- ],
- "scripts": {
- "postinstall": "node scripts/postinstall.js",
- "validate": "node scripts/validate-package.js"
- },
- "keywords": [
- "deskctl",
- "x11",
- "desktop",
- "automation",
- "cli"
- ]
-}
diff --git a/npm/deskctl/scripts/postinstall.js b/npm/deskctl/scripts/postinstall.js
deleted file mode 100644
index 1f43ad0..0000000
--- a/npm/deskctl/scripts/postinstall.js
+++ /dev/null
@@ -1,49 +0,0 @@
-const fs = require("node:fs");
-
-const {
- checksumsUrl,
- checksumForAsset,
- download,
- ensureVendorDir,
- installLocalBinary,
- readPackageJson,
- releaseAssetUrl,
- releaseTag,
- sha256,
- supportedTarget,
- vendorBinaryPath
-} = require("./support");
-
-async function main() {
- const pkg = readPackageJson();
- const target = supportedTarget();
- const targetPath = vendorBinaryPath(target);
-
- ensureVendorDir();
-
- if (process.env.DESKCTL_BINARY_PATH) {
- installLocalBinary(process.env.DESKCTL_BINARY_PATH, targetPath);
- return;
- }
-
- const tag = releaseTag(pkg);
- const assetUrl = releaseAssetUrl(tag, target.assetName);
- const checksumText = (await download(checksumsUrl(tag))).toString("utf8");
- const expectedSha = checksumForAsset(checksumText, target.assetName);
- const asset = await download(assetUrl);
- const actualSha = sha256(asset);
-
- if (actualSha !== expectedSha) {
- throw new Error(
- `Checksum mismatch for ${target.assetName}. Expected ${expectedSha}, got ${actualSha}.`
- );
- }
-
- fs.writeFileSync(targetPath, asset);
- fs.chmodSync(targetPath, 0o755);
-}
-
-main().catch((error) => {
- console.error(`deskctl install failed: ${error.message}`);
- process.exit(1);
-});
diff --git a/npm/deskctl/scripts/support.js b/npm/deskctl/scripts/support.js
deleted file mode 100644
index 1fd0d47..0000000
--- a/npm/deskctl/scripts/support.js
+++ /dev/null
@@ -1,120 +0,0 @@
-const crypto = require("node:crypto");
-const fs = require("node:fs");
-const path = require("node:path");
-const https = require("node:https");
-
-const PACKAGE_ROOT = path.resolve(__dirname, "..");
-const VENDOR_DIR = path.join(PACKAGE_ROOT, "vendor");
-const PACKAGE_JSON = path.join(PACKAGE_ROOT, "package.json");
-
-function readPackageJson() {
- return JSON.parse(fs.readFileSync(PACKAGE_JSON, "utf8"));
-}
-
-function releaseTag(pkg) {
- return process.env.DESKCTL_RELEASE_TAG || `v${pkg.version}`;
-}
-
-function supportedTarget(platform = process.platform, arch = process.arch) {
- if (platform === "linux" && arch === "x64") {
- return {
- platform,
- arch,
- assetName: "deskctl-linux-x86_64",
- binaryName: "deskctl-linux-x86_64"
- };
- }
-
- throw new Error(
- `deskctl currently supports linux-x64 only. Received ${platform}-${arch}.`
- );
-}
-
-function vendorBinaryPath(target) {
- return path.join(VENDOR_DIR, target.binaryName);
-}
-
-function releaseBaseUrl(tag) {
- return (
- process.env.DESKCTL_RELEASE_BASE_URL ||
- `https://github.com/harivansh-afk/deskctl/releases/download/${tag}`
- );
-}
-
-function releaseAssetUrl(tag, assetName) {
- return process.env.DESKCTL_DOWNLOAD_URL || `${releaseBaseUrl(tag)}/${assetName}`;
-}
-
-function checksumsUrl(tag) {
- return `${releaseBaseUrl(tag)}/checksums.txt`;
-}
-
-function ensureVendorDir() {
- fs.mkdirSync(VENDOR_DIR, { recursive: true });
-}
-
-function checksumForAsset(contents, assetName) {
- const line = contents
- .split("\n")
- .map((value) => value.trim())
- .find((value) => value.endsWith(` ${assetName}`) || value.endsWith(` *${assetName}`));
-
- if (!line) {
- throw new Error(`Could not find checksum entry for ${assetName}.`);
- }
-
- return line.split(/\s+/)[0];
-}
-
-function sha256(buffer) {
- return crypto.createHash("sha256").update(buffer).digest("hex");
-}
-
-function download(url) {
- return new Promise((resolve, reject) => {
- https
- .get(url, (response) => {
- if (
- response.statusCode &&
- response.statusCode >= 300 &&
- response.statusCode < 400 &&
- response.headers.location
- ) {
- response.resume();
- resolve(download(response.headers.location));
- return;
- }
-
- if (response.statusCode !== 200) {
- reject(new Error(`Download failed for ${url}: HTTP ${response.statusCode}`));
- return;
- }
-
- const chunks = [];
- response.on("data", (chunk) => chunks.push(chunk));
- response.on("end", () => resolve(Buffer.concat(chunks)));
- })
- .on("error", reject);
- });
-}
-
-function installLocalBinary(sourcePath, targetPath) {
- fs.copyFileSync(sourcePath, targetPath);
- fs.chmodSync(targetPath, 0o755);
-}
-
-module.exports = {
- PACKAGE_ROOT,
- VENDOR_DIR,
- checksumsUrl,
- checksumForAsset,
- download,
- ensureVendorDir,
- installLocalBinary,
- readPackageJson,
- releaseAssetUrl,
- releaseTag,
- sha256,
- supportedTarget,
- vendorBinaryPath
-};
diff --git a/npm/deskctl/scripts/validate-package.js b/npm/deskctl/scripts/validate-package.js
deleted file mode 100644
index 450fd6c..0000000
--- a/npm/deskctl/scripts/validate-package.js
+++ /dev/null
@@ -1,40 +0,0 @@
-const fs = require("node:fs");
-const path = require("node:path");
-
-const { readPackageJson, supportedTarget, vendorBinaryPath } = require("./support");
-
-function readCargoVersion() {
- const cargoToml = fs.readFileSync(
- path.resolve(__dirname, "..", "..", "..", "Cargo.toml"),
- "utf8"
- );
- const match = cargoToml.match(/^version = "([^"]+)"/m);
- if (!match) {
- throw new Error("Could not determine Cargo.toml version.");
- }
- return match[1];
-}
-
-function main() {
- const pkg = readPackageJson();
- const cargoVersion = readCargoVersion();
-
- if (pkg.version !== cargoVersion) {
- throw new Error(
- `Version mismatch: npm package is ${pkg.version}, Cargo.toml is ${cargoVersion}.`
- );
- }
-
- if (pkg.bin?.deskctl !== "bin/deskctl.js") {
- throw new Error("deskctl must expose the deskctl bin entrypoint.");
- }
-
- const target = supportedTarget("linux", "x64");
- const targetPath = vendorBinaryPath(target);
- const vendorDir = path.dirname(targetPath);
- if (!vendorDir.endsWith(path.join("deskctl", "vendor"))) {
- throw new Error("Vendor binary directory resolved unexpectedly.");
- }
-}
-
-main();
diff --git a/site/src/layouts/DocLayout.astro b/site/src/layouts/DocLayout.astro
index afc8648..f2608de 100644
--- a/site/src/layouts/DocLayout.astro
+++ b/site/src/layouts/DocLayout.astro
@@ -30,7 +30,7 @@ function formatTocText(text: string): string {
{
!isIndex && (
-