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 -[![npm](https://img.shields.io/npm/v/deskctl?label=npm)](https://www.npmjs.com/package/deskctl) -[![skill](https://img.shields.io/badge/skills.sh-deskctl-111827)](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

-
- -
-
-
-
-
-
-
-
-
- - -
-
-
- Files ~/reports -
-
-
-
- 📝 - 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 -
-
-
- NVDA - $924.68 - +3.42% - 1Y -
-
- - - - - - - - - - - - - $950 - $800 - $650 - -
-
-
-
- - -
-
-
- Chrome - Google Docs -
-
-
-
-
-
-
-
-
-
-
-
-
-
- - - - - - - - - - - NVDA 1Y - -
-
-
-
- - -
@w1
-
@w2
-
@w3
- -
- -
- -
-
- - -
-
Files
-
Yahoo Finance
-
Google Docs
-
-
-
- -
-
-
-
-
- agent computer -
-
-
-
- -
-

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 && ( -