chore(project): initialize cargo workspace, CI workflows, and package config

This commit is contained in:
Stevan Freeborn
2026-07-23 11:57:40 -05:00
parent 18c99e9f88
commit a6481370fa
9 changed files with 8156 additions and 0 deletions
+49
View File
@@ -0,0 +1,49 @@
name: CI
on:
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- name: Install Node.js
uses: actions/setup-node@v6
with:
node-version: lts/*
- name: Cache Cargo dependencies
uses: actions/cache@v6
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Cache npm dependencies
uses: actions/cache@v6
with:
path: app/node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('app/package-lock.json') }}
- name: Install frontend dependencies
run: npm ci
working-directory: app
- name: Run Rust tests
run: cargo test --workspace
- name: Run Clippy
run: cargo clippy --workspace -- -D warnings
- name: Run Rust fmt check
run: cargo fmt --all -- --check
- name: Run frontend tests
run: npm test
working-directory: app
- name: Run ESLint
run: npm run lint
working-directory: app
- name: Run Prettier check
run: npm run format:check
working-directory: app
+54
View File
@@ -0,0 +1,54 @@
name: Release
on:
push:
tags: ['v*']
jobs:
build-server:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Cache Cargo dependencies
uses: actions/cache@v6
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-release-${{ hashFiles('**/Cargo.lock') }}
- name: Build server binary
run: cargo build --release -p clean-copy-server
- name: Create release artifact archive
run: |
tar -czf clean-copy-server-linux-x86_64.tar.gz -C target/release clean-copy-server
- name: Upload release artifacts
uses: actions/upload-artifact@v3
with:
name: server-release
path: clean-copy-server-linux-x86_64.tar.gz
build-tauri:
runs-on: windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Install Node.js
uses: actions/setup-node@v6
with:
node-version: "20"
- name: Install frontend dependencies
run: npm ci
working-directory: app
- name: Build Tauri app
run: npx tauri build
working-directory: app
- name: Upload release artifacts
uses: actions/upload-artifact@v4
with:
name: tauri-release
path: |
app/src-tauri/target/release/clean-copy.exe
app/src-tauri/target/release/bundle/msi/*.msi
+41
View File
@@ -0,0 +1,41 @@
# data
server/releases/
server/data/
# env
*.env*
!.env.example
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Licensing Keys
licensing/
# Rust build artifacts
target/
# Generated by Tauri
gen/schemas
Generated
+7673
View File
File diff suppressed because it is too large Load Diff
+24
View File
@@ -0,0 +1,24 @@
[workspace]
members = ["app/src-tauri", "shared", "server"]
resolver = "2"
[workspace.package]
edition = "2021"
authors = ["Stevan Freeborn"]
[workspace.dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
ed25519-dalek = "3.0.0"
base64 = "0.22.1"
sha2 = "0.11.0"
thiserror = "2"
url = "2.5.8"
log = "0.4"
[profile.release]
lto = true
codegen-units = 1
strip = "symbols"
opt-level = "s"
panic = "abort"
+49
View File
@@ -1 +1,50 @@
# CleanCopy # CleanCopy
CleanCopy is a Windows utility that sits in the system tray, automatically sanitizing your clipboard text on trigger. Designed with a focus on privacy, ergonomics, and performance (it is written in Rust btw).
## Repository Structure
```
clean-copy/
├── app/ # Tauri desktop app (React frontend + Rust backend)
├── shared/ # Shared Rust crate (types, licensing, cleansing logic)
├── server/ # Axum web server (landing page, activation API, updater)
├── licensing/ # Ed25519 key files (gitignored)
├── licensing_profiles.json # Profile-based licensing config (compiled into binary)
├── Cargo.toml # Workspace root
└── package.json # Thin npm wrapper (delegates to app/)
```
## Quick Start
### Prerequisites
- **Node.js** (v18+)
- **Rust & Cargo** (latest stable)
- **Windows C++ Build Tools** (for Tauri)
### Install & Run
```bash
npm install # from repo root, installs app/ dependencies
npm run tauri dev # starts Vite dev server + Tauri tray app
```
### Run Tests
```bash
cargo test --workspace # Rust tests (shared + app)
cd app && npm test # Frontend tests (Vitest)
```
### Lint & Format
```bash
cd app && npm run lint # ESLint
cd app && npm run format:check # Prettier
```
## Documentation
- **[app/README.md](app/README.md)** — Desktop app: features, architecture, licensing system, CLI tools
- **[server/README.md](server/README.md)** — Web server: routes, local dev, Docker deployment
+17
View File
@@ -0,0 +1,17 @@
{
"development": {
"activation_url": "http://localhost:3000/api/activate",
"public_key_hex": "5638e661da9d745dfe471c781ba91ee3ff25e56c566ff581b87255cf4c07f061",
"deprecated_public_keys": []
},
"testing": {
"activation_url": "http://localhost:3000/api/activate",
"public_key_hex": "b1dd4c2c035656de9cf1628567f3e81bdb72d4328359313c35196dc3c60641f0",
"deprecated_public_keys": []
},
"production": {
"activation_url": "https://cleancopy.stevanfreeborn.com/api/activate",
"public_key_hex": "dc72e7db51e5408fe3b7cb771c74e5633c48e30cf0950f16170e1789a067de4b",
"deprecated_public_keys": []
}
}
+16
View File
@@ -0,0 +1,16 @@
{
"name": "clean-copy",
"private": true,
"version": "0.1.0",
"scripts": {
"dev": "cd app && npm run dev",
"build": "cd app && npm run build",
"tauri": "cd app && npx tauri",
"test": "cd app && npm test",
"test:rust": "cargo test --workspace",
"lint": "cd app && npm run lint",
"lint:fix": "cd app && npm run lint:fix",
"format": "prettier --write .",
"format:check": "prettier --check ."
}
}
+233
View File
@@ -0,0 +1,233 @@
param(
[Parameter(Mandatory = $true)]
[string]$Version
)
$ErrorActionPreference = "Stop"
$VpsHost = "bit_bastion"
$VpsDeployDir = "/home/stevan/cleancopy.stevanfreeborn.com"
$ContainerName = "clean-copy"
$ImageName = "clean-copy-server"
if ($Version -notmatch '^\d+\.\d+\.\d+$') {
Write-Error "Invalid version format '$Version'. Expected semver (e.g. 0.2.0)"
exit 1
}
Write-Host "=== CleanCopy Release v$Version ===" -ForegroundColor Cyan
Write-Host ""
try {
docker info 2>&1 | Out-Null
if ($LASTEXITCODE -ne 0) {
throw
}
} catch {
Write-Error "Docker Desktop is not running. Start it and try again."
exit 1
}
Write-Host "[1/11] Checking SSH connectivity..." -ForegroundColor Yellow
try {
ssh $VpsHost "echo ok" 2>&1 | Out-Null
if ($LASTEXITCODE -ne 0) {
throw
}
} catch {
Write-Error "Cannot SSH into $VpsHost. Check your SSH config."
exit 1
}
Write-Host " OK" -ForegroundColor Green
Write-Host "[2/11] Running tests..." -ForegroundColor Yellow
cargo test --workspace
if ($LASTEXITCODE -ne 0) {
Write-Error "Rust tests failed";
exit 1
}
Push-Location app
npm test
if ($LASTEXITCODE -ne 0) { Pop-Location; Write-Error "Frontend tests failed"; exit 1 }
Pop-Location
Write-Host " All tests passed" -ForegroundColor Green
Write-Host "[3/11] Bumping versions to $Version..." -ForegroundColor Yellow
$cargoFiles = @(
"app/src-tauri/Cargo.toml",
"shared/Cargo.toml",
"server/Cargo.toml"
)
foreach ($file in $cargoFiles) {
$content = Get-Content $file -Raw
$content = $content -replace '(?m)^version = ".*"', "version = `"$Version`""
Set-Content $file $content -NoNewline
}
$pkg = Get-Content "app/package.json" -Raw
$pkg = $pkg -replace '"version":\s*"[^"]*"', "`"version`": `"$Version`""
Set-Content "app/package.json" $pkg -NoNewline
$tauri = Get-Content "app/src-tauri/tauri.conf.json" -Raw
$tauri = $tauri -replace '"version":\s*"[^"]*"', "`"version`": `"$Version`""
Set-Content "app/src-tauri/tauri.conf.json" $tauri -NoNewline
Write-Host " Updated: Cargo.toml (x3), package.json, tauri.conf.json" -ForegroundColor Green
Write-Host "[4/11] Creating git commit and tag..." -ForegroundColor Yellow
git add -A
git commit -m "release: v$Version"
if ($LASTEXITCODE -ne 0) {
Write-Error "Git commit failed";
exit 1
}
git tag "v$Version"
if ($LASTEXITCODE -ne 0) {
Write-Error "Git tag failed";
exit 1
}
Write-Host " Tagged v$Version" -ForegroundColor Green
Write-Host "[5/11] Building Tauri app..." -ForegroundColor Yellow
Push-Location app
npx tauri build
if ($LASTEXITCODE -ne 0) {
Pop-Location; Write-Error "Tauri build failed";
exit 1
}
Pop-Location
Write-Host " Built clean-copy.exe + MSI" -ForegroundColor Green
Write-Host "[6/11] Organizing release artifacts..." -ForegroundColor Yellow
$releaseDir = "server/releases/$Version/x86_64"
New-Item -ItemType Directory -Force -Path $releaseDir | Out-Null
$exeSrc = "app/src-tauri/target/release/clean-copy.exe"
$msiSrc = Get-ChildItem "app/src-tauri/target/release/bundle/msi/*.msi" | Select-Object -First 1
if (-not (Test-Path $exeSrc)) {
Write-Error "Expected $exeSrc not found"
exit 1
}
Copy-Item $exeSrc "$releaseDir/clean-copy-$Version.exe"
Write-Host " Copied clean-copy-$Version.exe" -ForegroundColor Green
if ($msiSrc) {
Copy-Item $msiSrc.FullName "$releaseDir/$($msiSrc.Name)"
Write-Host " Copied $($msiSrc.Name)" -ForegroundColor Green
} else {
Write-Host " Warning: No MSI found" -ForegroundColor Yellow
}
Write-Host "[7/11] Building Docker image for linux/arm64..." -ForegroundColor Yellow
docker buildx build --platform linux/arm64 -t "${ImageName}:${Version}" -t "${ImageName}:latest" -f server/Dockerfile . --load
if ($LASTEXITCODE -ne 0) {
Write-Error "Docker build failed";
exit 1
}
Write-Host " Built ${ImageName}:${Version}" -ForegroundColor Green
Write-Host "[8/11] Saving Docker image to tarball..." -ForegroundColor Yellow
$tarball = "${ImageName}-${Version}.tar"
docker save "${ImageName}:${Version}" -o $tarball
if ($LASTEXITCODE -ne 0) {
Write-Error "Docker save failed";
exit 1
}
$tarSize = [math]::Round((Get-Item $tarball).Length / 1MB, 1)
Write-Host " Saved $tarball ($tarSize MB)" -ForegroundColor Green
Write-Host "[9/11] Transferring files to VPS..." -ForegroundColor Yellow
scp $tarball "${VpsHost}:${VpsDeployDir}/"
if ($LASTEXITCODE -ne 0) {
Write-Error "Failed to transfer tarball"; exit 1
}
Write-Host " Transferred $tarball" -ForegroundColor Green
ssh $VpsHost "mkdir -p ${VpsDeployDir}/data/releases/${Version}/x86_64"
scp -r "${releaseDir}/*" "${VpsHost}:${VpsDeployDir}/data/releases/${Version}/x86_64/"
if ($LASTEXITCODE -ne 0) {
Write-Error "Failed to transfer release artifacts";
exit 1
}
Write-Host " Transferred release artifacts" -ForegroundColor Green
Write-Host "[10/11] Deploying on VPS..." -ForegroundColor Yellow
$deployScript = @"
cd ${VpsDeployDir} && \
docker load -i ${ImageName}-${Version}.tar && \
docker stop ${ContainerName} 2>/dev/null; docker rm ${ContainerName} 2>/dev/null; \
docker run -d \
--name ${ContainerName} \
-p 3000:3000 \
-v ${VpsDeployDir}/data:/data \
--env-file ${VpsDeployDir}/.env \
--restart unless-stopped \
${ImageName}:${Version} && \
rm ${ImageName}-${Version}.tar && \
echo "DEPLOY_OK"
"@
$result = ssh $VpsHost $deployScript
if ($result -notmatch "DEPLOY_OK") {
Write-Error "Deployment failed. Output: $result"
exit 1
}
Write-Host " Container $ContainerName started" -ForegroundColor Green
Write-Host "[11/11] Cleaning up local tarball..." -ForegroundColor Yellow
Remove-Item $tarball -Force
Write-Host " Removed $tarball" -ForegroundColor Green
Write-Host ""
Write-Host "=== Release Complete ===" -ForegroundColor Cyan
Write-Host " Version: v$Version"
Write-Host " Image: ${ImageName}:${Version}"
Write-Host " Container: $ContainerName (running)"
Write-Host " Deploy dir: ${VpsDeployDir}"
Write-Host ""
Write-Host " Next: git push && git push --tags" -ForegroundColor Yellow