mirror of
https://github.com/harivansh-afk/deskctl.git
synced 2026-04-15 01:00:29 +00:00
parent
425a71095a
commit
714e34ba19
16 changed files with 849 additions and 66 deletions
130
.github/workflows/ci.yml
vendored
130
.github/workflows/ci.yml
vendored
|
|
@ -13,7 +13,6 @@ on:
|
|||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
|
|
@ -37,7 +36,11 @@ jobs:
|
|||
- 'tests/**'
|
||||
- 'Cargo.toml'
|
||||
- 'Cargo.lock'
|
||||
- 'npm/**'
|
||||
- 'flake.nix'
|
||||
- 'flake.lock'
|
||||
- 'docker/**'
|
||||
- '.github/workflows/**'
|
||||
- 'Makefile'
|
||||
|
||||
- name: Set outputs
|
||||
|
|
@ -137,72 +140,36 @@ jobs:
|
|||
- name: Xvfb integration tests
|
||||
run: make test-integration
|
||||
|
||||
build:
|
||||
name: Build (${{ matrix.target }})
|
||||
needs: [changes, validate, integration]
|
||||
if: github.event_name != 'pull_request' && needs.changes.outputs.rust == 'true'
|
||||
distribution:
|
||||
name: Distribution Validate
|
||||
needs: changes
|
||||
if: 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
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- uses: cachix/install-nix-action@v30
|
||||
with:
|
||||
extra_nix_config: |
|
||||
experimental-features = nix-command flakes
|
||||
|
||||
- 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:
|
||||
name: deskctl-linux-x86_64
|
||||
path: target/release/deskctl
|
||||
retention-days: 7
|
||||
|
||||
# --- Docker steps ---
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
if: matrix.target == 'docker'
|
||||
|
||||
- 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
|
||||
- name: Distribution validation
|
||||
run: make dist-validate
|
||||
|
||||
update-manifests:
|
||||
name: Update Manifests
|
||||
needs: [changes, build]
|
||||
needs: [changes, validate, integration, distribution]
|
||||
if: github.event_name != 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
|
@ -212,12 +179,17 @@ jobs:
|
|||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Update version in Cargo.toml
|
||||
run: |
|
||||
CURRENT=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/')
|
||||
NEW="${{ needs.changes.outputs.version }}"
|
||||
if [ "$CURRENT" != "$NEW" ]; then
|
||||
sed -i "0,/^version = \"${CURRENT}\"/s//version = \"${NEW}\"/" Cargo.toml
|
||||
node -e 'const fs=require("node:fs"); const path="npm/deskctl-cli/package.json"; const pkg=JSON.parse(fs.readFileSync(path,"utf8")); pkg.version=process.argv[1]; fs.writeFileSync(path, JSON.stringify(pkg, null, 2)+"\n");' "$NEW"
|
||||
cargo generate-lockfile
|
||||
fi
|
||||
|
||||
|
|
@ -227,7 +199,7 @@ jobs:
|
|||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
if ! git diff --quiet; then
|
||||
git add Cargo.toml Cargo.lock
|
||||
git add Cargo.toml Cargo.lock npm/deskctl-cli/package.json
|
||||
git commit -m "release: ${{ needs.changes.outputs.tag }} [skip ci]"
|
||||
fi
|
||||
|
||||
|
|
@ -236,6 +208,38 @@ jobs:
|
|||
fi
|
||||
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 }}
|
||||
fetch-depth: 0
|
||||
|
||||
- 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: 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
|
||||
|
||||
release:
|
||||
name: Release
|
||||
needs: [changes, build, update-manifests]
|
||||
|
|
@ -256,9 +260,15 @@ jobs:
|
|||
chmod +x artifacts/deskctl
|
||||
mv artifacts/deskctl artifacts/deskctl-linux-x86_64
|
||||
cd artifacts && sha256sum deskctl-linux-x86_64 > checksums.txt && cd ..
|
||||
|
||||
gh release create "${{ needs.changes.outputs.tag }}" \
|
||||
--title "${{ needs.changes.outputs.tag }}" \
|
||||
--generate-notes \
|
||||
artifacts/deskctl-linux-x86_64 \
|
||||
artifacts/checksums.txt
|
||||
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
|
||||
|
|
|
|||
102
.github/workflows/publish.yml
vendored
Normal file
102
.github/workflows/publish.yml
vendored
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
name: Publish Registries
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: Release tag to publish (for example v0.1.5)
|
||||
required: true
|
||||
type: string
|
||||
publish_npm:
|
||||
description: Publish deskctl-cli to npm
|
||||
required: true
|
||||
type: boolean
|
||||
default: false
|
||||
publish_crates:
|
||||
description: Publish deskctl to crates.io
|
||||
required: true
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.tag }}
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Install system dependencies
|
||||
run: sudo apt-get update && sudo apt-get install -y libx11-dev libxtst-dev
|
||||
|
||||
- name: Verify release exists and contains canonical assets
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh release view "${{ inputs.tag }}" --json assets --jq '.assets[].name' > /tmp/release-assets.txt
|
||||
grep -Fx "deskctl-linux-x86_64" /tmp/release-assets.txt >/dev/null
|
||||
grep -Fx "checksums.txt" /tmp/release-assets.txt >/dev/null
|
||||
|
||||
- name: Verify versions align with tag
|
||||
run: |
|
||||
TAG="${{ inputs.tag }}"
|
||||
VERSION="${TAG#v}"
|
||||
CARGO_VERSION=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/')
|
||||
NPM_VERSION=$(node -p 'require("./npm/deskctl-cli/package.json").version')
|
||||
|
||||
test "$VERSION" = "$CARGO_VERSION"
|
||||
test "$VERSION" = "$NPM_VERSION"
|
||||
|
||||
- name: Check current published state
|
||||
id: published
|
||||
run: |
|
||||
VERSION="${{ inputs.tag }}"
|
||||
VERSION="${VERSION#v}"
|
||||
|
||||
if npm view "deskctl-cli@${VERSION}" version >/dev/null 2>&1; then
|
||||
echo "npm=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "npm=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
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: Validate npm package
|
||||
run: |
|
||||
mkdir -p ./tmp/npm-pack
|
||||
node npm/deskctl-cli/scripts/validate-package.js
|
||||
npm pack ./npm/deskctl-cli --pack-destination ./tmp/npm-pack >/dev/null
|
||||
|
||||
- name: Validate crate publish path
|
||||
run: cargo publish --dry-run --locked
|
||||
|
||||
- name: Publish npm
|
||||
if: inputs.publish_npm && steps.published.outputs.npm != 'true'
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: npm publish ./npm/deskctl-cli --access public
|
||||
|
||||
- name: Publish crates.io
|
||||
if: inputs.publish_crates && steps.published.outputs.crates != 'true'
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
run: cargo publish --locked
|
||||
|
||||
- name: Summary
|
||||
run: |
|
||||
echo "tag=${{ inputs.tag }}"
|
||||
echo "npm_already_published=${{ steps.published.outputs.npm }}"
|
||||
echo "crates_already_published=${{ steps.published.outputs.crates }}"
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -5,3 +5,5 @@ secret/
|
|||
.claude/
|
||||
.codex/
|
||||
openspec/
|
||||
npm/deskctl-cli/vendor/
|
||||
npm/deskctl-cli/*.tgz
|
||||
|
|
|
|||
|
|
@ -35,10 +35,15 @@ 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
|
||||
|
||||
|
|
@ -60,6 +65,19 @@ 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-cli`
|
||||
- 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:
|
||||
|
|
|
|||
13
Cargo.toml
13
Cargo.toml
|
|
@ -5,6 +5,19 @@ 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"] }
|
||||
|
|
|
|||
32
Makefile
32
Makefile
|
|
@ -1,4 +1,4 @@
|
|||
.PHONY: fmt fmt-check lint test-unit test-integration site-format-check validate
|
||||
.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
|
||||
|
||||
fmt:
|
||||
cargo fmt --all
|
||||
|
|
@ -30,4 +30,34 @@ 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-cli/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-cli --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
|
||||
|
|
|
|||
53
README.md
53
README.md
|
|
@ -4,11 +4,45 @@ Desktop control CLI for AI agents on Linux X11.
|
|||
|
||||
## Install
|
||||
|
||||
### Cargo
|
||||
|
||||
```bash
|
||||
cargo install deskctl
|
||||
```
|
||||
|
||||
Build a Linux binary with Docker:
|
||||
Source builds on Linux require:
|
||||
|
||||
- Rust 1.75+
|
||||
- `pkg-config`
|
||||
- X11 development libraries for input and windowing, typically `libx11-dev` and `libxtst-dev` on Debian/Ubuntu
|
||||
|
||||
### npm
|
||||
|
||||
```bash
|
||||
npm install -g deskctl-cli
|
||||
deskctl --help
|
||||
```
|
||||
|
||||
One-shot execution is also supported:
|
||||
|
||||
```bash
|
||||
npx deskctl-cli --help
|
||||
```
|
||||
|
||||
`deskctl-cli` currently supports `linux-x64` and installs the `deskctl` command by downloading the matching GitHub Release asset.
|
||||
|
||||
### Nix
|
||||
|
||||
```bash
|
||||
nix run github:harivansh-afk/deskctl -- --help
|
||||
nix profile install github:harivansh-afk/deskctl
|
||||
```
|
||||
|
||||
The repo flake is the supported Nix install surface in this phase.
|
||||
|
||||
### Docker Convenience
|
||||
|
||||
Build a Linux binary locally with Docker:
|
||||
|
||||
```bash
|
||||
docker compose -f docker/docker-compose.yml run --rm build
|
||||
|
|
@ -28,13 +62,12 @@ Run it on an X11 session:
|
|||
DISPLAY=:1 XDG_SESSION_TYPE=x11 ~/deskctl --json snapshot --annotate
|
||||
```
|
||||
|
||||
Local source build requirements:
|
||||
### Local Source Build
|
||||
|
||||
```bash
|
||||
cargo build
|
||||
```
|
||||
|
||||
At the moment there are no extra native build dependencies beyond a Rust toolchain.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
|
|
@ -78,7 +111,7 @@ Source layout:
|
|||
## Runtime Requirements
|
||||
|
||||
- Linux with X11 session
|
||||
- Rust 1.75+ (for build)
|
||||
- Rust 1.75+ plus the source-build dependencies above when building from source
|
||||
|
||||
The binary itself only links the standard glibc runtime on Linux (`libc`, `libm`, `libgcc_s`).
|
||||
|
||||
|
|
@ -158,6 +191,16 @@ Text mode is compact and follow-up-oriented, but JSON is the parsing contract.
|
|||
|
||||
See [docs/runtime-output.md](docs/runtime-output.md) for the exact stable-vs-best-effort breakdown.
|
||||
|
||||
## Distribution
|
||||
|
||||
- GitHub Releases are the canonical binary source
|
||||
- crates.io package: `deskctl`
|
||||
- npm package: `deskctl-cli`
|
||||
- installed command on every channel: `deskctl`
|
||||
- repo-owned Nix install path: `flake.nix`
|
||||
|
||||
For maintainer publishing and release steps, see [docs/releasing.md](docs/releasing.md).
|
||||
|
||||
## Selector Contract
|
||||
|
||||
Explicit selector modes:
|
||||
|
|
|
|||
110
docs/releasing.md
Normal file
110
docs/releasing.md
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
# 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-cli`
|
||||
- installed command: `deskctl`
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before the first live publish on each registry:
|
||||
|
||||
- npm ownership for `deskctl-cli`
|
||||
- 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 workflow:
|
||||
|
||||
- targets an existing release tag
|
||||
- checks that Cargo, npm, and the requested tag all agree on version
|
||||
- checks whether that version is already published on npm and crates.io
|
||||
- only publishes the channels explicitly requested
|
||||
|
||||
## 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
|
||||
61
flake.lock
generated
Normal file
61
flake.lock
generated
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
77
flake.nix
Normal file
77
flake.nix
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
{
|
||||
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
|
||||
];
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
36
npm/deskctl-cli/README.md
Normal file
36
npm/deskctl-cli/README.md
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# deskctl-cli
|
||||
|
||||
`deskctl-cli` installs the `deskctl` command for Linux X11 systems.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
npm install -g deskctl-cli
|
||||
```
|
||||
|
||||
After install, run:
|
||||
|
||||
```bash
|
||||
deskctl --help
|
||||
```
|
||||
|
||||
One-shot usage is also supported:
|
||||
|
||||
```bash
|
||||
npx deskctl-cli --help
|
||||
```
|
||||
|
||||
## Runtime Support
|
||||
|
||||
- Linux
|
||||
- X11 session
|
||||
- currently packaged release asset: `linux-x64`
|
||||
|
||||
`deskctl-cli` 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
|
||||
```
|
||||
36
npm/deskctl-cli/bin/deskctl.js
Normal file
36
npm/deskctl-cli/bin/deskctl.js
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
#!/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-cli 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();
|
||||
36
npm/deskctl-cli/package.json
Normal file
36
npm/deskctl-cli/package.json
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"name": "deskctl-cli",
|
||||
"version": "0.1.5",
|
||||
"description": "Installable deskctl CLI 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"
|
||||
]
|
||||
}
|
||||
49
npm/deskctl-cli/scripts/postinstall.js
Normal file
49
npm/deskctl-cli/scripts/postinstall.js
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
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-cli install failed: ${error.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
120
npm/deskctl-cli/scripts/support.js
Normal file
120
npm/deskctl-cli/scripts/support.js
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
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-cli 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
|
||||
};
|
||||
40
npm/deskctl-cli/scripts/validate-package.js
Normal file
40
npm/deskctl-cli/scripts/validate-package.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
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-cli 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-cli", "vendor"))) {
|
||||
throw new Error("Vendor binary directory resolved unexpectedly.");
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
Loading…
Add table
Add a link
Reference in a new issue