Compare commits
248 Commits
7e667a1708
...
v4.11.163
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4d4c73c2b | ||
| 73fbc1c77a | |||
|
|
3dc732521f | ||
| b76aa3fe4a | |||
|
|
c0d1e87b8d | ||
| 04d4026e78 | |||
|
|
cfbc59d6a9 | ||
| dfe42fa942 | |||
|
|
6a8397425e | ||
| 641ec26142 | |||
|
|
1eaf246806 | ||
| 0e97e4d3b6 | |||
|
|
1636a8d0c6 | ||
| ac236750aa | |||
|
|
7e747e6c36 | ||
| 1b2cae5b12 | |||
|
|
76921c4e79 | ||
| 688efff7c9 | |||
|
|
9bc377b62a | ||
| 836ccce7a3 | |||
|
|
34c8e6e38c | ||
| e69bda3aa6 | |||
|
|
5d55e5d826 | ||
| b5902b9fc3 | |||
|
|
f60cf0bd3c | ||
| 83dec0ef54 | |||
|
|
40a76f02cd | ||
| 3def3cc456 | |||
|
|
30bd67d43a | ||
| ffeccee680 | |||
|
|
facbeee024 | ||
| 09102c7903 | |||
|
|
fa33ec7ac8 | ||
| c44f5cf14a | |||
|
|
27793d7c4f | ||
| 32b2902ef7 | |||
|
|
5cdb318854 | ||
| a28f05bb88 | |||
|
|
cecfe73b2e | ||
| b458c7918c | |||
|
|
d8660f4822 | ||
| dad83773a6 | |||
|
|
d2d35de46b | ||
| c752ec0b38 | |||
|
|
28752eb56e | ||
| 4f243a0fcf | |||
|
|
9bce616f26 | ||
| f16ab73c9a | |||
|
|
c29a15e073 | ||
| f5fe61e3eb | |||
|
|
a1a752b0fb | ||
| 7769250875 | |||
|
|
b3af9f3185 | ||
| 7906326790 | |||
|
|
3b4babb9d7 | ||
| 4b284ec747 | |||
| e1b4cfb9c8 | |||
|
|
c0266903ec | ||
| 81964d6f14 | |||
|
|
47e025cf55 | ||
| b400070ad0 | |||
| 529694ce66 | |||
|
|
791c3ab4df | ||
| de8d9db795 | |||
|
|
ffaf9ff66a | ||
| f0891ba4cc | |||
| 627b117119 | |||
|
|
b6c6659a5a | ||
| d7a73afebc | |||
| e469d51ba4 | |||
|
|
6ba03277e5 | ||
| fdb2dca794 | |||
|
|
8797bb5a22 | ||
| 4d154fd4f3 | |||
| 42e8fb71ef | |||
|
|
ff2c284984 | ||
| 75be84e472 | |||
| a3c91b306a | |||
|
|
20edc293bb | ||
| 6803eac8fb | |||
| fa5592f1e4 | |||
|
|
78601be6f4 | ||
| c940bdc78c | |||
|
|
9471fe2f5d | ||
| 57f7fe7086 | |||
| a2364f2622 | |||
|
|
df2220581d | ||
| 198eb0fe04 | |||
| fff114abef | |||
|
|
7013f1c22d | ||
| 1dbc7d6cc4 | |||
| f93f109d4a | |||
|
|
f707aa98f5 | ||
| 8a0be09d5d | |||
| a1b86f00ba | |||
|
|
c9f569a2b2 | ||
| 7a736b3adf | |||
| b6ecd16148 | |||
|
|
30d6b8dc74 | ||
| b6bcd8d05b | |||
|
|
0b5fe5f04c | ||
| c629c07bf9 | |||
|
|
fcd7d3e7ea | ||
| db994a5d43 | |||
| fea9ebd501 | |||
|
|
9238d80adb | ||
| a28d3c26b5 | |||
|
|
161030927f | ||
| c431c1d547 | |||
| 739c863601 | |||
|
|
5b6c213101 | ||
| 5acf981bd4 | |||
|
|
14b27717a6 | ||
| bd3e1d5b9c | |||
| a668544c6e | |||
|
|
dd8f4eb47d | ||
| 54cb11e55b | |||
| dec48f6c1d | |||
|
|
8f776f4f7c | ||
| c6aff43467 | |||
| 9f95ef1ecd | |||
|
|
aafda0d437 | ||
| f97a8d4929 | |||
|
|
a8c742376f | ||
| 938ab53d4b | |||
|
|
62352412d3 | ||
| fd33b3f6b7 | |||
| 3c0b4cf108 | |||
|
|
4eb48726ae | ||
| ba17993001 | |||
| cf39f0acd9 | |||
|
|
bce2cbec94 | ||
| f401b5ea3d | |||
| 4a5e3a1691 | |||
|
|
dc50f74971 | ||
| a198eb6896 | |||
| dc9b12dd26 | |||
|
|
6ea0e7a4fd | ||
| 51b13e0fcd | |||
|
|
1d7a48c57e | ||
| 0e34ca2063 | |||
|
|
6248d5843a | ||
| 5af79606c2 | |||
| 3cc31d8e05 | |||
|
|
0f7dd5f55c | ||
| de4f6ebbc9 | |||
|
|
fd25143489 | ||
| 5265674a58 | |||
|
|
39376e0003 | ||
| cf1f7fcec8 | |||
|
|
153e55705a | ||
| 77cdd66027 | |||
| 80580391cb | |||
|
|
6f0988f873 | ||
| 99358803d2 | |||
| 6a347d977f | |||
|
|
c03a4d3f41 | ||
| 50818f1d76 | |||
|
|
0f8c9b83f4 | ||
| 76cae46005 | |||
| ca22449d54 | |||
|
|
dda1041268 | ||
| b4f8f13213 | |||
| 9ad9971a03 | |||
| c38dc7bbf6 | |||
|
|
aaed71e37b | ||
| ed85051480 | |||
| d0524c2bc7 | |||
|
|
8a0f8f5422 | ||
| 8a95d9e67d | |||
| b8a126fd69 | |||
|
|
6b18e5c8ee | ||
| 27c2ce021d | |||
|
|
de6620245c | ||
| 21e895efff | |||
| e9487c3043 | |||
|
|
686749ac55 | ||
| dd790d3336 | |||
|
|
c8ceeccc8c | ||
| a592766e0a | |||
| 7acf8ea877 | |||
|
|
2a87133d14 | ||
| c960bc5410 | |||
| fde412fafa | |||
|
|
476d0c71fc | ||
| df6fa0fa42 | |||
|
|
246521b7b3 | ||
| 8ccc3ecb78 | |||
|
|
19ce4b43ca | ||
| 8ea4c58a5b | |||
| 465fa6b57b | |||
|
|
12f0d7f8d3 | ||
| f11078bb5c | |||
| 4e6835c741 | |||
|
|
4584367041 | ||
| bf71bd9726 | |||
| 8cfc96688e | |||
|
|
eedd040203 | ||
| 8396834b08 | |||
| cb136002b9 | |||
|
|
9a6dc126db | ||
| c9352327eb | |||
| 9985b0931c | |||
|
|
5faf0056d7 | ||
| cf6bedf58a | |||
| 03ecdbb54f | |||
|
|
1222651d17 | ||
| 1f34f726bf | |||
| 5b1bd1cffa | |||
| 050352fd2b | |||
|
|
422b680b18 | ||
| 6510522b6b | |||
| 48a189ed6a | |||
|
|
a72758d9ef | ||
| 7ee178f6e9 | |||
| 50885a9da4 | |||
|
|
5d39692e41 | ||
| de87a55582 | |||
| a3c2510830 | |||
|
|
6b9a57e243 | ||
| 4fa0ce0d4a | |||
| e90b8d43cb | |||
|
|
379a957a40 | ||
| fc18b0b902 | |||
| a4429fe9c2 | |||
|
|
fc4229cea7 | ||
| 69fcfd12f3 | |||
| 843b00166e | |||
|
|
4dbc8c93d1 | ||
| a196d2706f | |||
| 643c8a7291 | |||
|
|
e503829d3a | ||
| 78fce9b5b0 | |||
| 42f21c274f | |||
|
|
d39d006296 | ||
| 8e8b5a732c | |||
| edd2c457ed | |||
|
|
097f45d8ca | ||
| 53422c8718 | |||
| 89ecc91acd | |||
|
|
abd22d4875 | ||
| ccb922fbe0 | |||
| b80128f029 | |||
|
|
c737edfa43 | ||
| 735511fad0 | |||
|
|
d650e34a8f | ||
| 1cb8fd6e49 | |||
|
|
7857e42c5a |
@@ -1,365 +0,0 @@
|
|||||||
name: Build Paarrot Windows
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ main, development ]
|
|
||||||
pull_request:
|
|
||||||
branches: [ main ]
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: build-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
start-vm:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 10
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Check if VM is already running
|
|
||||||
id: check-vm
|
|
||||||
run: |
|
|
||||||
if pgrep -f "qemu-system-x86_64.*windows.qcow2" > /dev/null; then
|
|
||||||
echo "VM is already running"
|
|
||||||
echo "vm_running=true" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "VM is not running"
|
|
||||||
echo "vm_running=false" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Start Windows VM with retry
|
|
||||||
run: |
|
|
||||||
MAX_RETRIES=3
|
|
||||||
RETRY_COUNT=0
|
|
||||||
BOOT_WAIT=90
|
|
||||||
RUNNER_CHECK_TIMEOUT=120
|
|
||||||
|
|
||||||
start_vm() {
|
|
||||||
echo "Starting Windows QEMU VM (attempt $((RETRY_COUNT + 1))/$MAX_RETRIES)..."
|
|
||||||
sudo -u litruv setsid bash -c 'DISPLAY=:0 XAUTHORITY=/home/litruv/.Xauthority qemu-system-x86_64 \
|
|
||||||
-m 18G \
|
|
||||||
-smp 4 \
|
|
||||||
-enable-kvm \
|
|
||||||
-cpu host \
|
|
||||||
-drive file=/home/litruv/windows.qcow2,format=qcow2 \
|
|
||||||
-boot d \
|
|
||||||
-vga virtio \
|
|
||||||
-display vnc=:0 \
|
|
||||||
> /tmp/qemu.log 2>&1' &
|
|
||||||
disown
|
|
||||||
}
|
|
||||||
|
|
||||||
kill_vm() {
|
|
||||||
echo "Killing VM..."
|
|
||||||
sudo pkill -9 qemu-system-x86 || true
|
|
||||||
sleep 5
|
|
||||||
}
|
|
||||||
|
|
||||||
check_runner() {
|
|
||||||
API_URL="${{ github.server_url }}/api/v1/repos/${{ github.repository }}/actions/runners"
|
|
||||||
RESPONSE=$(curl -s -H "Authorization: token ${{ secrets.CI_BOT_TOKEN }}" "$API_URL" 2>/dev/null)
|
|
||||||
|
|
||||||
if [ -z "$RESPONSE" ]; then
|
|
||||||
echo " Could not query runner API"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
ONLINE=$(echo "$RESPONSE" | jq -r '.runners[] | select(.labels[].name == "windows-vm") | select(.status == "online") | .name' 2>/dev/null)
|
|
||||||
|
|
||||||
if [ -n "$ONLINE" ]; then
|
|
||||||
echo " Runner found online!"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo " Windows runner (windows-vm) is NOT online"
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
wait_for_runner() {
|
|
||||||
local max_checks=$1
|
|
||||||
local required_successes=${2:-1}
|
|
||||||
local successes=0
|
|
||||||
echo "Waiting for runner to come online (need ${required_successes} successful checks, max ${max_checks} attempts)..."
|
|
||||||
for i in $(seq 1 $max_checks); do
|
|
||||||
if check_runner; then
|
|
||||||
successes=$((successes + 1))
|
|
||||||
echo "Runner online check $successes/$required_successes passed ($i/$max_checks)"
|
|
||||||
if [ $successes -ge $required_successes ]; then
|
|
||||||
echo "Runner confirmed online after $successes consecutive checks!"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
successes=0
|
|
||||||
echo "Waiting for runner... ($i/$max_checks)"
|
|
||||||
fi
|
|
||||||
sleep 5
|
|
||||||
done
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "Checking if runner is online..."
|
|
||||||
if wait_for_runner 12 12; then
|
|
||||||
echo "Runner verified online!"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "${{ steps.check-vm.outputs.vm_running }}" == "true" ]; then
|
|
||||||
echo "VM process exists but runner not responding, restarting VM..."
|
|
||||||
kill_vm
|
|
||||||
fi
|
|
||||||
|
|
||||||
while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
|
|
||||||
start_vm
|
|
||||||
echo "Waiting ${BOOT_WAIT}s for VM to boot..."
|
|
||||||
sleep $BOOT_WAIT
|
|
||||||
|
|
||||||
if ! pgrep -f "qemu-system-x86_64.*windows.qcow2" > /dev/null; then
|
|
||||||
echo "VM process not found, retrying..."
|
|
||||||
RETRY_COUNT=$((RETRY_COUNT + 1))
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
if wait_for_runner $((RUNNER_CHECK_TIMEOUT / 5)); then
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Runner did not come online, killing VM and retrying..."
|
|
||||||
kill_vm
|
|
||||||
RETRY_COUNT=$((RETRY_COUNT + 1))
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "ERROR: Failed to start VM after $MAX_RETRIES attempts"
|
|
||||||
exit 1
|
|
||||||
|
|
||||||
- name: VM Status
|
|
||||||
run: |
|
|
||||||
if [ "${{ steps.check-vm.outputs.vm_running }}" == "true" ]; then
|
|
||||||
echo "Using existing VM"
|
|
||||||
else
|
|
||||||
echo "Started new VM"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if pgrep -f "qemu-system-x86_64.*windows.qcow2" > /dev/null; then
|
|
||||||
echo "VM process confirmed running"
|
|
||||||
else
|
|
||||||
echo "ERROR: VM process not found!"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
build:
|
|
||||||
runs-on: windows-vm
|
|
||||||
needs: start-vm
|
|
||||||
timeout-minutes: 60
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Cancel pending shutdown
|
|
||||||
shell: powershell
|
|
||||||
run: |
|
|
||||||
shutdown /s /t 1000 2>$null
|
|
||||||
shutdown /a 2>$null
|
|
||||||
Write-Host "Cancelled any pending shutdown"
|
|
||||||
|
|
||||||
- name: Disable sleep and power saving
|
|
||||||
shell: powershell
|
|
||||||
run: |
|
|
||||||
powercfg /change standby-timeout-ac 0
|
|
||||||
powercfg /change standby-timeout-dc 0
|
|
||||||
powercfg /change hibernate-timeout-ac 0
|
|
||||||
powercfg /change hibernate-timeout-dc 0
|
|
||||||
powercfg /change monitor-timeout-ac 0
|
|
||||||
powercfg /change monitor-timeout-dc 0
|
|
||||||
powercfg /setactive 8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c
|
|
||||||
Write-Host "Power saving disabled for build"
|
|
||||||
|
|
||||||
- name: Fast checkout (persistent repo)
|
|
||||||
shell: powershell
|
|
||||||
run: |
|
|
||||||
$ErrorActionPreference = "Continue"
|
|
||||||
|
|
||||||
$repoDir = "C:\BuildCache\cinny-desktop\Repo"
|
|
||||||
$serverUrl = "${{ github.server_url }}"
|
|
||||||
$repo = "${{ github.repository }}"
|
|
||||||
$branch = "${{ github.ref_name }}"
|
|
||||||
$token = "${{ secrets.CI_BOT_TOKEN }}"
|
|
||||||
|
|
||||||
$uri = [System.Uri]$serverUrl
|
|
||||||
$scheme = $uri.Scheme
|
|
||||||
$authority = $uri.Authority
|
|
||||||
$repoUrl = "${scheme}://${authority}/${repo}"
|
|
||||||
|
|
||||||
Write-Host "Using server: ${scheme}://${authority}"
|
|
||||||
|
|
||||||
if (-not (Test-Path "C:\BuildCache\cinny-desktop")) {
|
|
||||||
New-Item -ItemType Directory -Path "C:\BuildCache\cinny-desktop" -Force
|
|
||||||
}
|
|
||||||
|
|
||||||
git config --global credential.helper ""
|
|
||||||
git config --global "http.${scheme}://${authority}/.extraheader" "Authorization: token $token"
|
|
||||||
|
|
||||||
function Remove-GitLocks {
|
|
||||||
$lockFiles = @(
|
|
||||||
"$repoDir\.git\shallow.lock",
|
|
||||||
"$repoDir\.git\index.lock",
|
|
||||||
"$repoDir\.git\HEAD.lock",
|
|
||||||
"$repoDir\.git\config.lock"
|
|
||||||
)
|
|
||||||
foreach ($lock in $lockFiles) {
|
|
||||||
if (Test-Path $lock) {
|
|
||||||
Write-Host "Removing stale lock: $lock"
|
|
||||||
Remove-Item -Force $lock
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Do-Clone {
|
|
||||||
Write-Host "Cloning repository fresh..."
|
|
||||||
if (Test-Path $repoDir) {
|
|
||||||
Remove-Item -Recurse -Force $repoDir
|
|
||||||
}
|
|
||||||
|
|
||||||
$cloneSuccess = $false
|
|
||||||
for ($i = 1; $i -le 3; $i++) {
|
|
||||||
Write-Host "Clone attempt $i/3..."
|
|
||||||
& git clone --depth=1 --branch $branch $repoUrl $repoDir 2>&1 | Out-Host
|
|
||||||
if ($LASTEXITCODE -eq 0) {
|
|
||||||
$cloneSuccess = $true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
Write-Host "Clone attempt $i failed, retrying in 5 seconds..."
|
|
||||||
if (Test-Path $repoDir) {
|
|
||||||
Remove-Item -Recurse -Force $repoDir
|
|
||||||
}
|
|
||||||
Start-Sleep -Seconds 5
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $cloneSuccess) {
|
|
||||||
throw "Clone failed after 3 attempts"
|
|
||||||
}
|
|
||||||
|
|
||||||
Push-Location $repoDir
|
|
||||||
& git submodule update --init --recursive --depth=1 2>&1 | Out-Host
|
|
||||||
Pop-Location
|
|
||||||
}
|
|
||||||
|
|
||||||
function Do-Update {
|
|
||||||
Write-Host "Updating existing repo..."
|
|
||||||
Push-Location $repoDir
|
|
||||||
|
|
||||||
Remove-GitLocks
|
|
||||||
|
|
||||||
git remote set-url origin $repoUrl 2>$null
|
|
||||||
Write-Host "Remote URL updated"
|
|
||||||
|
|
||||||
git reset --hard HEAD 2>$null
|
|
||||||
if ($LASTEXITCODE -ne 0) {
|
|
||||||
Write-Host "Reset failed, repo may be corrupted"
|
|
||||||
Pop-Location
|
|
||||||
return $false
|
|
||||||
}
|
|
||||||
|
|
||||||
git clean -fdx -e node_modules/ -e cinny/node_modules/ 2>$null
|
|
||||||
|
|
||||||
$fetchSuccess = $false
|
|
||||||
for ($i = 1; $i -le 3; $i++) {
|
|
||||||
Write-Host "Fetch attempt $i/3..."
|
|
||||||
& git fetch origin $branch --depth=1 2>&1 | Out-Host
|
|
||||||
if ($LASTEXITCODE -eq 0) {
|
|
||||||
$fetchSuccess = $true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
Write-Host "Fetch attempt $i failed, retrying in 5 seconds..."
|
|
||||||
Start-Sleep -Seconds 5
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $fetchSuccess) {
|
|
||||||
Write-Host "All fetch attempts failed"
|
|
||||||
Pop-Location
|
|
||||||
return $false
|
|
||||||
}
|
|
||||||
|
|
||||||
& git checkout -B $branch origin/$branch 2>&1 | Out-Host
|
|
||||||
if ($LASTEXITCODE -ne 0) {
|
|
||||||
Write-Host "Checkout failed"
|
|
||||||
Pop-Location
|
|
||||||
return $false
|
|
||||||
}
|
|
||||||
|
|
||||||
& git submodule update --init --recursive --depth=1 2>&1 | Out-Host
|
|
||||||
|
|
||||||
Pop-Location
|
|
||||||
return $true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Test-Path "$repoDir\.git") {
|
|
||||||
$success = Do-Update
|
|
||||||
if (-not $success) {
|
|
||||||
Write-Host "Update failed, will re-clone..."
|
|
||||||
Do-Clone
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Do-Clone
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (Test-Path "$repoDir\package.json")) {
|
|
||||||
throw "Checkout verification failed - package.json not found"
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host "Checkout complete at: $repoDir"
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
shell: powershell
|
|
||||||
run: |
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
$repoDir = "C:\BuildCache\cinny-desktop\Repo"
|
|
||||||
|
|
||||||
Push-Location $repoDir
|
|
||||||
|
|
||||||
Write-Host "Installing root dependencies..."
|
|
||||||
npm ci --prefer-offline --no-audit
|
|
||||||
|
|
||||||
Write-Host "Installing cinny dependencies..."
|
|
||||||
Push-Location cinny
|
|
||||||
npm ci --prefer-offline --no-audit
|
|
||||||
Pop-Location
|
|
||||||
|
|
||||||
Pop-Location
|
|
||||||
Write-Host "Dependencies installed"
|
|
||||||
|
|
||||||
- name: Build Paarrot
|
|
||||||
shell: powershell
|
|
||||||
run: |
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
$repoDir = "C:\BuildCache\cinny-desktop\Repo"
|
|
||||||
|
|
||||||
Push-Location $repoDir
|
|
||||||
|
|
||||||
Write-Host "Building Paarrot..."
|
|
||||||
npm run build:win
|
|
||||||
|
|
||||||
Pop-Location
|
|
||||||
Write-Host "Build completed successfully!"
|
|
||||||
|
|
||||||
- name: Upload artifacts
|
|
||||||
shell: powershell
|
|
||||||
run: |
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
$repoDir = "C:\BuildCache\cinny-desktop\Repo"
|
|
||||||
$artifactDir = "$repoDir\dist-electron"
|
|
||||||
|
|
||||||
if (-not (Test-Path $artifactDir)) {
|
|
||||||
Write-Error "Build artifacts not found at $artifactDir"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host "Build artifacts:"
|
|
||||||
Get-ChildItem $artifactDir -Recurse | Select-Object FullName
|
|
||||||
|
|
||||||
# TODO: Copy artifacts to artifact server or release location
|
|
||||||
Write-Host "Artifacts ready at: $artifactDir"
|
|
||||||
|
|
||||||
- name: Schedule delayed shutdown
|
|
||||||
if: always()
|
|
||||||
shell: powershell
|
|
||||||
run: |
|
|
||||||
shutdown /s /t 900 /c "Build complete - shutting down in 15 minutes"
|
|
||||||
Write-Host "VM will shutdown in 15 minutes unless another build starts"
|
|
||||||
@@ -11,10 +11,9 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
increment-version:
|
increment-version:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.ref == 'refs/heads/main' && !contains(github.event.head_commit.message, '[skip ci]')
|
|
||||||
outputs:
|
outputs:
|
||||||
version: ${{ steps.bump.outputs.VERSION }}
|
version: ${{ steps.finalize.outputs.VERSION }}
|
||||||
should_build: ${{ steps.bump.outputs.SHOULD_BUILD }}
|
should_build: ${{ steps.decide.outputs.SHOULD_BUILD }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
@@ -22,9 +21,35 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Decide version behavior
|
||||||
|
id: decide
|
||||||
|
run: |
|
||||||
|
CURRENT=$(grep -Po '"version":\s*"\K[^"]+' package.json | head -1)
|
||||||
|
COMMIT_MSG="${{ github.event.head_commit.message || '' }}"
|
||||||
|
|
||||||
|
SHOULD_BUMP=false
|
||||||
|
SHOULD_BUILD=true
|
||||||
|
|
||||||
|
if [ "${{ github.ref }}" = "refs/heads/main" ] && ! echo "$COMMIT_MSG" | grep -qi '\[skip ci\]'; then
|
||||||
|
SHOULD_BUMP=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if echo "$COMMIT_MSG" | grep -qi '\[skip ci\]'; then
|
||||||
|
SHOULD_BUILD=false
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "CURRENT_VERSION=$CURRENT" >> $GITHUB_OUTPUT
|
||||||
|
echo "SHOULD_BUMP=$SHOULD_BUMP" >> $GITHUB_OUTPUT
|
||||||
|
echo "SHOULD_BUILD=$SHOULD_BUILD" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
echo "Current version: $CURRENT"
|
||||||
|
echo "Should bump: $SHOULD_BUMP"
|
||||||
|
echo "Should build: $SHOULD_BUILD"
|
||||||
|
|
||||||
- name: Bump patch version
|
- name: Bump patch version
|
||||||
id: bump
|
id: bump
|
||||||
|
if: ${{ steps.decide.outputs.SHOULD_BUMP == 'true' }}
|
||||||
run: |
|
run: |
|
||||||
# Get current version from package.json
|
# Get current version from package.json
|
||||||
CURRENT=$(grep -Po '"version":\s*"\K[^"]+' package.json | head -1)
|
CURRENT=$(grep -Po '"version":\s*"\K[^"]+' package.json | head -1)
|
||||||
@@ -45,15 +70,40 @@ jobs:
|
|||||||
grep '"version"' package.json | head -1
|
grep '"version"' package.json | head -1
|
||||||
|
|
||||||
echo "VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT
|
echo "VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||||
echo "SHOULD_BUILD=true" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Commit and push version bump
|
- name: Commit and push version bump
|
||||||
|
if: ${{ steps.decide.outputs.SHOULD_BUMP == 'true' }}
|
||||||
run: |
|
run: |
|
||||||
git config user.name "GitHub Actions"
|
git config user.name "GitHub Actions"
|
||||||
git config user.email "actions@github.com"
|
git config user.email "actions@github.com"
|
||||||
git add package.json
|
git add package.json
|
||||||
git commit -m "chore: bump version to ${{ steps.bump.outputs.VERSION }} [skip ci]"
|
git commit -m "chore: bump version to ${{ steps.bump.outputs.VERSION }} [skip ci]"
|
||||||
git push
|
for attempt in 1 2 3; do
|
||||||
|
git fetch origin main
|
||||||
|
git rebase origin/main || {
|
||||||
|
git rebase --abort || true
|
||||||
|
echo "Failed to rebase version bump onto latest main"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if git push origin HEAD:main; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Push rejected on attempt ${attempt}, retrying against latest main"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Failed to push version bump after 3 attempts"
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
- name: Finalize version output
|
||||||
|
id: finalize
|
||||||
|
run: |
|
||||||
|
if [ -n "${{ steps.bump.outputs.VERSION }}" ]; then
|
||||||
|
echo "VERSION=${{ steps.bump.outputs.VERSION }}" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "VERSION=${{ steps.decide.outputs.CURRENT_VERSION }}" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
build-windows:
|
build-windows:
|
||||||
runs-on: windows
|
runs-on: windows
|
||||||
@@ -70,7 +120,19 @@ jobs:
|
|||||||
- name: Checkout submodules manually
|
- name: Checkout submodules manually
|
||||||
shell: powershell
|
shell: powershell
|
||||||
run: |
|
run: |
|
||||||
git submodule update --init --recursive
|
git submodule sync --recursive
|
||||||
|
|
||||||
|
if (git submodule update --init --recursive) {
|
||||||
|
Write-Host "Submodules checked out at pinned commits."
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Warning "Pinned submodule commit is unavailable on remote. Falling back to remote submodule HEAD."
|
||||||
|
git submodule deinit -f --all
|
||||||
|
git submodule update --init --recursive --remote
|
||||||
|
|
||||||
|
Write-Host "Resolved submodule commit:"
|
||||||
|
git -C cinny rev-parse HEAD
|
||||||
|
|
||||||
- name: Pull latest (after version bump)
|
- name: Pull latest (after version bump)
|
||||||
if: github.ref == 'refs/heads/main'
|
if: github.ref == 'refs/heads/main'
|
||||||
@@ -83,11 +145,11 @@ jobs:
|
|||||||
node-version: '20'
|
node-version: '20'
|
||||||
|
|
||||||
- name: Install dependencies (root)
|
- name: Install dependencies (root)
|
||||||
run: npm install --prefer-offline
|
run: npm ci --prefer-offline
|
||||||
|
|
||||||
- name: Install dependencies (cinny)
|
- name: Install dependencies (cinny)
|
||||||
working-directory: ./cinny
|
working-directory: ./cinny
|
||||||
run: npm install --prefer-offline
|
run: npm ci --prefer-offline
|
||||||
|
|
||||||
- name: Clean previous builds
|
- name: Clean previous builds
|
||||||
shell: powershell
|
shell: powershell
|
||||||
@@ -96,42 +158,26 @@ jobs:
|
|||||||
if (Test-Path dist-electron) { Remove-Item -Recurse -Force dist-electron }
|
if (Test-Path dist-electron) { Remove-Item -Recurse -Force dist-electron }
|
||||||
|
|
||||||
- name: Build Electron app
|
- name: Build Electron app
|
||||||
run: npm run build:win
|
run: npm run build:local -- --win
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GHTOKEN }}
|
||||||
|
|
||||||
- name: Upload Squirrel Setup (x64)
|
- name: Upload NSIS Installer (x64)
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: Paarrot-Setup-x64.exe
|
name: Paarrot-Setup-x64.exe
|
||||||
path: dist-electron/squirrel-windows/Paarrot-*-win-x64.exe
|
path: dist-electron/Paarrot-*-win-x64.exe
|
||||||
|
|
||||||
|
- name: Upload latest.yml
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: latest.yml
|
||||||
|
path: dist-electron/latest.yml
|
||||||
|
|
||||||
- name: List build output
|
- name: List build output
|
||||||
shell: powershell
|
shell: powershell
|
||||||
run: |
|
run: |
|
||||||
Get-ChildItem -Path dist-electron -Recurse | Select-Object FullName
|
Get-ChildItem -Path dist-electron -Recurse | Select-Object FullName
|
||||||
|
|
||||||
- name: Copy RELEASES file for upload
|
|
||||||
shell: powershell
|
|
||||||
run: |
|
|
||||||
if (Test-Path "dist-electron/squirrel-windows/RELEASES") {
|
|
||||||
Copy-Item "dist-electron/squirrel-windows/RELEASES" -Destination "dist-electron/RELEASES"
|
|
||||||
} else {
|
|
||||||
Write-Error "RELEASES file not found for x64"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
- name: Upload Squirrel RELEASES
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: RELEASES
|
|
||||||
path: dist-electron/RELEASES
|
|
||||||
|
|
||||||
- name: Upload Squirrel nupkg (x64)
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: Paarrot-x64.nupkg
|
|
||||||
path: dist-electron/squirrel-windows/*.nupkg
|
|
||||||
|
|
||||||
build-linux:
|
build-linux:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -142,8 +188,25 @@ jobs:
|
|||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: false
|
||||||
ref: ${{ github.ref_name }}
|
ref: ${{ github.ref_name }}
|
||||||
|
|
||||||
|
- name: Checkout submodules manually
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
git submodule sync --recursive
|
||||||
|
|
||||||
|
if git submodule update --init --recursive; then
|
||||||
|
echo "Submodules checked out at pinned commits."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Pinned submodule commit is unavailable on remote. Falling back to remote submodule HEAD."
|
||||||
|
git submodule deinit -f --all
|
||||||
|
git submodule update --init --recursive --remote
|
||||||
|
|
||||||
|
echo "Resolved submodule commit:"
|
||||||
|
git -C cinny rev-parse HEAD
|
||||||
|
|
||||||
- name: Pull latest (after version bump)
|
- name: Pull latest (after version bump)
|
||||||
if: github.ref == 'refs/heads/main'
|
if: github.ref == 'refs/heads/main'
|
||||||
@@ -166,15 +229,21 @@ jobs:
|
|||||||
rm -rf cinny/dist dist-electron
|
rm -rf cinny/dist dist-electron
|
||||||
|
|
||||||
- name: Build Electron app
|
- name: Build Electron app
|
||||||
run: npm run build:linux
|
run: npm run build:local -- --linux
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GHTOKEN }}
|
||||||
|
|
||||||
- name: Upload AppImage
|
- name: Upload AppImage
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: Paarrot-Linux-x64.AppImage
|
name: Paarrot-Linux-x64.AppImage
|
||||||
path: dist-electron/Paarrot-*.AppImage
|
path: dist-electron/Paarrot-*.AppImage
|
||||||
|
|
||||||
|
- name: Upload latest-linux.yml
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: latest-linux.yml
|
||||||
|
path: dist-electron/latest-linux.yml
|
||||||
|
|
||||||
create-release:
|
create-release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -232,7 +301,7 @@ jobs:
|
|||||||
- name: Prepare release files
|
- name: Prepare release files
|
||||||
run: |
|
run: |
|
||||||
mkdir -p release-files
|
mkdir -p release-files
|
||||||
find artifacts -type f \( -name "*.exe" -o -name "*.AppImage" -o -name "*.nupkg" -o -name "RELEASES*" \) -exec cp {} release-files/ \;
|
find artifacts -type f \( -name "*.exe" -o -name "*.AppImage" -o -name "*.yml" \) -exec cp {} release-files/ \;
|
||||||
ls -la release-files/
|
ls -la release-files/
|
||||||
|
|
||||||
- name: Create or Update Release
|
- name: Create or Update Release
|
||||||
@@ -244,6 +313,28 @@ jobs:
|
|||||||
API_BASE="http://synbox.ruv.wtf:8418/api/v1"
|
API_BASE="http://synbox.ruv.wtf:8418/api/v1"
|
||||||
REPO="litruv/cinny-desktop"
|
REPO="litruv/cinny-desktop"
|
||||||
|
|
||||||
|
# Delete old releases (keep only current)
|
||||||
|
echo "Cleaning up old releases..."
|
||||||
|
OLD_RELEASES=$(curl -s -H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
"${API_BASE}/repos/${REPO}/releases" | jq -r '.[] | select(.tag_name != "'${TAG_NAME}'") | .id')
|
||||||
|
|
||||||
|
for OLD_ID in $OLD_RELEASES; do
|
||||||
|
echo "Deleting old release ${OLD_ID}..."
|
||||||
|
OLD_TAG=$(curl -s -H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
"${API_BASE}/repos/${REPO}/releases/${OLD_ID}" | jq -r '.tag_name')
|
||||||
|
|
||||||
|
# Delete release
|
||||||
|
curl -s -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
"${API_BASE}/repos/${REPO}/releases/${OLD_ID}"
|
||||||
|
|
||||||
|
# Delete tag
|
||||||
|
if [ -n "$OLD_TAG" ]; then
|
||||||
|
echo "Deleting old tag ${OLD_TAG}..."
|
||||||
|
curl -s -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
"${API_BASE}/repos/${REPO}/tags/${OLD_TAG}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
# Check if release exists
|
# Check if release exists
|
||||||
RELEASE_ID=$(curl -s -H "Authorization: token ${GITEA_TOKEN}" \
|
RELEASE_ID=$(curl -s -H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
"${API_BASE}/repos/${REPO}/releases/tags/${TAG_NAME}" | jq -r '.id // empty')
|
"${API_BASE}/repos/${REPO}/releases/tags/${TAG_NAME}" | jq -r '.id // empty')
|
||||||
@@ -283,3 +374,67 @@ jobs:
|
|||||||
done
|
done
|
||||||
|
|
||||||
echo "Release complete!"
|
echo "Release complete!"
|
||||||
|
|
||||||
|
- name: Create GitHub Release
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GHTOKEN }}
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.version.outputs.VERSION }}"
|
||||||
|
TAG_NAME="v${VERSION}"
|
||||||
|
GITHUB_REPO="Paarrot/Paarrot-Desktop"
|
||||||
|
|
||||||
|
# Delete old GitHub releases (keep only current)
|
||||||
|
echo "Cleaning up old GitHub releases..."
|
||||||
|
OLD_RELEASES=$(curl -s -H "Authorization: token ${GH_TOKEN}" \
|
||||||
|
"https://api.github.com/repos/${GITHUB_REPO}/releases" | jq -r '.[] | select(.tag_name != "'${TAG_NAME}'") | .id')
|
||||||
|
|
||||||
|
for OLD_ID in $OLD_RELEASES; do
|
||||||
|
echo "Deleting old GitHub release ${OLD_ID}..."
|
||||||
|
OLD_TAG=$(curl -s -H "Authorization: token ${GH_TOKEN}" \
|
||||||
|
"https://api.github.com/repos/${GITHUB_REPO}/releases/${OLD_ID}" | jq -r '.tag_name')
|
||||||
|
|
||||||
|
# Delete release
|
||||||
|
curl -s -X DELETE -H "Authorization: token ${GH_TOKEN}" \
|
||||||
|
"https://api.github.com/repos/${GITHUB_REPO}/releases/${OLD_ID}"
|
||||||
|
|
||||||
|
# Delete tag
|
||||||
|
if [ -n "$OLD_TAG" ]; then
|
||||||
|
echo "Deleting old GitHub tag ${OLD_TAG}..."
|
||||||
|
curl -s -X DELETE -H "Authorization: token ${GH_TOKEN}" \
|
||||||
|
"https://api.github.com/repos/${GITHUB_REPO}/git/refs/tags/${OLD_TAG}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Check if release exists on GitHub
|
||||||
|
RELEASE_ID=$(curl -s -H "Authorization: token ${GH_TOKEN}" \
|
||||||
|
"https://api.github.com/repos/${GITHUB_REPO}/releases/tags/${TAG_NAME}" | jq -r '.id // empty')
|
||||||
|
|
||||||
|
if [ -z "$RELEASE_ID" ]; then
|
||||||
|
echo "Creating new GitHub release for ${TAG_NAME}..."
|
||||||
|
RELEASE_ID=$(curl -s -X POST \
|
||||||
|
-H "Authorization: token ${GH_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"tag_name\": \"${TAG_NAME}\", \"name\": \"Release ${TAG_NAME}\", \"body\": \"Release ${VERSION}\", \"draft\": false, \"prerelease\": false}" \
|
||||||
|
"https://api.github.com/repos/${GITHUB_REPO}/releases" | jq -r '.id')
|
||||||
|
echo "Created GitHub release with ID: ${RELEASE_ID}"
|
||||||
|
else
|
||||||
|
echo "GitHub release exists with ID: ${RELEASE_ID}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Upload assets to GitHub
|
||||||
|
echo "Uploading assets to GitHub..."
|
||||||
|
UPLOAD_URL=$(curl -s -H "Authorization: token ${GH_TOKEN}" \
|
||||||
|
"https://api.github.com/repos/${GITHUB_REPO}/releases/${RELEASE_ID}" | jq -r '.upload_url' | sed 's/{?name,label}//')
|
||||||
|
|
||||||
|
for FILE in release-files/*; do
|
||||||
|
FILENAME=$(basename "$FILE")
|
||||||
|
echo "Uploading ${FILENAME} to GitHub..."
|
||||||
|
curl -s -X POST \
|
||||||
|
-H "Authorization: token ${GH_TOKEN}" \
|
||||||
|
-H "Content-Type: application/octet-stream" \
|
||||||
|
--data-binary "@${FILE}" \
|
||||||
|
"${UPLOAD_URL}?name=${FILENAME}"
|
||||||
|
echo ""
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "GitHub release complete!"
|
||||||
|
|||||||
3
.github/FUNDING.yml
vendored
3
.github/FUNDING.yml
vendored
@@ -1,3 +0,0 @@
|
|||||||
github: ajbura
|
|
||||||
liberapay: ajbura
|
|
||||||
open_collective: cinny
|
|
||||||
58
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
58
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,58 +0,0 @@
|
|||||||
name: 🐞 Bug Report
|
|
||||||
description: Report a bug
|
|
||||||
labels: 'type: bug'
|
|
||||||
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
## First of all
|
|
||||||
1. Please search for [existing issues](https://github.com/cinnyapp/cinny-desktop/issues?q=is%3Aissue) about this problem first.
|
|
||||||
2. Make sure Cinny is up to date.
|
|
||||||
3. Make sure it's an issue with Cinny and not something else you are using.
|
|
||||||
4. Remember to be friendly.
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: description
|
|
||||||
attributes:
|
|
||||||
label: Describe the bug
|
|
||||||
description: A clear description of what the bug is. Include screenshots if applicable.
|
|
||||||
placeholder: Bug description
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: reproduction
|
|
||||||
attributes:
|
|
||||||
label: Reproduction
|
|
||||||
description: Steps to reproduce the behavior.
|
|
||||||
placeholder: |
|
|
||||||
1. Go to ...
|
|
||||||
2. Click on ...
|
|
||||||
3. See error
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: expected-behavior
|
|
||||||
attributes:
|
|
||||||
label: Expected behavior
|
|
||||||
description: A clear description of what you expected to happen.
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: info
|
|
||||||
attributes:
|
|
||||||
label: Platform and versions
|
|
||||||
description: "Provide OS, browser and Cinny version with your Homeserver."
|
|
||||||
placeholder: |
|
|
||||||
1. OS: [e.g. Windows 10, MacOS]
|
|
||||||
2. Cinny version: [e.g. 1.8.1]
|
|
||||||
3. Matrix homeserver: [e.g. matrix.org]
|
|
||||||
4. Downloaded from: [e.g. GitHub, Flatpak]
|
|
||||||
render: shell
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: context
|
|
||||||
attributes:
|
|
||||||
label: Additional context
|
|
||||||
description: Add any other context about the problem here.
|
|
||||||
4
.github/ISSUE_TEMPLATE/config.yml
vendored
4
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,4 +0,0 @@
|
|||||||
contact_links:
|
|
||||||
- name: 💬 Matrix Chat
|
|
||||||
url: https://matrix.to/#/#cinny:matrix.org
|
|
||||||
about: Ask questions and talk to other Cinny users and the maintainers
|
|
||||||
34
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
34
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -1,34 +0,0 @@
|
|||||||
name: 💡 Feature Request
|
|
||||||
description: Suggest an idea
|
|
||||||
labels: 'type: feature'
|
|
||||||
|
|
||||||
body:
|
|
||||||
- type: textarea
|
|
||||||
id: problem
|
|
||||||
attributes:
|
|
||||||
label: Describe the problem
|
|
||||||
description: A clear description of the problem this feature would solve
|
|
||||||
placeholder: "I'm always frustrated when..."
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: solution
|
|
||||||
attributes:
|
|
||||||
label: "Describe the solution you'd like"
|
|
||||||
description: A clear description of what change you would like
|
|
||||||
placeholder: "I would like to..."
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: alternatives
|
|
||||||
attributes:
|
|
||||||
label: Alternatives considered
|
|
||||||
description: "Any alternative solutions you've considered"
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: context
|
|
||||||
attributes:
|
|
||||||
label: Additional context
|
|
||||||
description: Add any other context about the problem here.
|
|
||||||
22
.github/PULL_REQUEST_TEMPLATE.md
vendored
22
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,22 +0,0 @@
|
|||||||
<!-- Please read https://github.com/cinnyapp/cinny/blob/dev/CONTRIBUTING.md before submitting your pull request -->
|
|
||||||
|
|
||||||
### Description
|
|
||||||
<!-- Please include a summary of the change. Please also include relevant motivation and context. List any dependencies that are required for this change. -->
|
|
||||||
|
|
||||||
|
|
||||||
Fixes #
|
|
||||||
|
|
||||||
#### Type of change
|
|
||||||
|
|
||||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
|
||||||
- [ ] New feature (non-breaking change which adds functionality)
|
|
||||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
|
||||||
- [ ] This change requires a documentation update
|
|
||||||
|
|
||||||
### Checklist:
|
|
||||||
|
|
||||||
- [ ] My code follows the style guidelines of this project
|
|
||||||
- [ ] I have performed a self-review of my own code
|
|
||||||
- [ ] I have commented my code, particularly in hard-to-understand areas
|
|
||||||
- [ ] I have made corresponding changes to the documentation
|
|
||||||
- [ ] My changes generate no new warnings
|
|
||||||
3
.github/SECURITY.md
vendored
3
.github/SECURITY.md
vendored
@@ -1,3 +0,0 @@
|
|||||||
# Reporting a Vulnerability
|
|
||||||
|
|
||||||
**If you've found a security vulnerability, please report it to cinnyapp@gmail.com**
|
|
||||||
30
.github/dependabot.yml
vendored
30
.github/dependabot.yml
vendored
@@ -1,30 +0,0 @@
|
|||||||
# Docs: <https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/customizing-dependency-updates>
|
|
||||||
|
|
||||||
version: 2
|
|
||||||
updates:
|
|
||||||
# - package-ecosystem: npm
|
|
||||||
# directory: /
|
|
||||||
# schedule:
|
|
||||||
# interval: weekly
|
|
||||||
# day: "tuesday"
|
|
||||||
# time: "01:00"
|
|
||||||
# timezone: "Asia/Kolkata"
|
|
||||||
# open-pull-requests-limit: 15
|
|
||||||
|
|
||||||
- package-ecosystem: github-actions
|
|
||||||
directory: /
|
|
||||||
schedule:
|
|
||||||
interval: weekly
|
|
||||||
day: "tuesday"
|
|
||||||
time: "01:00"
|
|
||||||
timezone: "Asia/Kolkata"
|
|
||||||
open-pull-requests-limit: 5
|
|
||||||
|
|
||||||
# - package-ecosystem: cargo
|
|
||||||
# directory: /src-tauri/
|
|
||||||
# schedule:
|
|
||||||
# interval: weekly
|
|
||||||
# day: "tuesday"
|
|
||||||
# time: "01:00"
|
|
||||||
# timezone: "Asia/Kolkata"
|
|
||||||
# open-pull-requests-limit: 5
|
|
||||||
21
.github/renovate.json
vendored
21
.github/renovate.json
vendored
@@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
|
||||||
"extends": [
|
|
||||||
"config:recommended",
|
|
||||||
":dependencyDashboardApproval"
|
|
||||||
],
|
|
||||||
"labels": [
|
|
||||||
"Dependencies"
|
|
||||||
],
|
|
||||||
"packageRules": [
|
|
||||||
{
|
|
||||||
"matchUpdateTypes": [
|
|
||||||
"lockFileMaintenance"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"lockFileMaintenance": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"dependencyDashboard": true
|
|
||||||
}
|
|
||||||
22
.github/workflows/archive.yml
vendored
22
.github/workflows/archive.yml
vendored
@@ -1,22 +0,0 @@
|
|||||||
name: "Upload zip-archive"
|
|
||||||
on:
|
|
||||||
release:
|
|
||||||
types: [published]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
zip-archive:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4.2.0
|
|
||||||
with:
|
|
||||||
submodules: true
|
|
||||||
- name: Create zip including submodules
|
|
||||||
run: |
|
|
||||||
cd ..
|
|
||||||
zip ${{ github.event.repository.name }}/${{ github.event.repository.name }}-${{ github.ref_name }}.zip ${{ github.event.repository.name }} -r
|
|
||||||
- name: Upload zip to release
|
|
||||||
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836
|
|
||||||
with:
|
|
||||||
files: |
|
|
||||||
${{ github.event.repository.name }}-${{ github.ref_name }}.zip
|
|
||||||
36
.github/workflows/cla.yml
vendored
36
.github/workflows/cla.yml
vendored
@@ -1,36 +0,0 @@
|
|||||||
name: 'CLA Assistant'
|
|
||||||
on:
|
|
||||||
issue_comment:
|
|
||||||
types: [created]
|
|
||||||
pull_request_target:
|
|
||||||
types: [opened, closed, synchronize]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
CLAssistant:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: 'CLA Assistant'
|
|
||||||
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
|
|
||||||
# Beta Release
|
|
||||||
uses: cla-assistant/github-action@v2.6.1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
# the below token should have repo scope and must be manually added by you in the repository's secret
|
|
||||||
PERSONAL_ACCESS_TOKEN: ${{ secrets.CLA_PAT }}
|
|
||||||
with:
|
|
||||||
path-to-signatures: 'signatures.json'
|
|
||||||
path-to-document: 'https://github.com/cinnyapp/cla/blob/main/cla.md' # e.g. a CLA or a DCO document
|
|
||||||
# branch should not be protected
|
|
||||||
branch: 'main'
|
|
||||||
allowlist: ajbura,bot*
|
|
||||||
|
|
||||||
#below are the optional inputs - If the optional inputs are not given, then default values will be taken
|
|
||||||
remote-organization-name: cinnyapp
|
|
||||||
remote-repository-name: cla
|
|
||||||
#create-file-commit-message: 'For example: Creating file for storing CLA Signatures'
|
|
||||||
#signed-commit-message: 'For example: $contributorName has signed the CLA in #$pullRequestNo'
|
|
||||||
#custom-notsigned-prcomment: 'pull request comment with Introductory message to ask new contributors to sign'
|
|
||||||
#custom-pr-sign-comment: 'The signature to be committed in order to sign the CLA'
|
|
||||||
#custom-allsigned-prcomment: 'pull request comment when all contributors has signed, defaults to **CLA Assistant Lite bot** All Contributors have signed the CLA.'
|
|
||||||
#lock-pullrequest-aftermerge: false - if you don't want this bot to automatically lock the pull request after merging (default - true)
|
|
||||||
#use-dco-flag: true - If you are using DCO instead of CLA
|
|
||||||
26
.github/workflows/lockfile.yml
vendored
26
.github/workflows/lockfile.yml
vendored
@@ -1,26 +0,0 @@
|
|||||||
name: NPM Lockfile Changes
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- 'package-lock.json'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
lockfile_changes:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
# Permission overwrite is required for Dependabot PRs, see "Common issues" below.
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pull-requests: write
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4.2.0
|
|
||||||
- name: NPM Lockfile Changes
|
|
||||||
uses: codepunkt/npm-lockfile-changes@b40543471c36394409466fdb277a73a0856d7891
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
# Optional inputs, can be deleted safely if you are happy with default values.
|
|
||||||
collapsibleThreshold: 25
|
|
||||||
failOnDowngrade: false
|
|
||||||
path: package-lock.json
|
|
||||||
updateComment: true
|
|
||||||
40
.github/workflows/test.yml
vendored
40
.github/workflows/test.yml
vendored
@@ -1,40 +0,0 @@
|
|||||||
name: "Build pull request"
|
|
||||||
on:
|
|
||||||
#pull_request:
|
|
||||||
#types: ['opened', 'synchronize']
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
publish-tauri:
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
platform: [macos-latest, ubuntu-20.04, windows-latest]
|
|
||||||
|
|
||||||
runs-on: ${{ matrix.platform }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4.2.0
|
|
||||||
with:
|
|
||||||
submodules: true
|
|
||||||
- name: Setup node
|
|
||||||
uses: actions/setup-node@v4.4.0
|
|
||||||
with:
|
|
||||||
node-version: 20.12.2
|
|
||||||
cache: 'npm'
|
|
||||||
- name: Install Rust stable
|
|
||||||
uses: actions-rs/toolchain@v1.0.7
|
|
||||||
with:
|
|
||||||
toolchain: stable
|
|
||||||
- name: Install dependencies (ubuntu only)
|
|
||||||
if: matrix.platform == 'ubuntu-20.04'
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf
|
|
||||||
- name: Install cinny dependencies
|
|
||||||
run: cd cinny && npm ci
|
|
||||||
- name: Install tauri dependencies
|
|
||||||
run: npm ci
|
|
||||||
- name: Build desktop app with Tauri
|
|
||||||
uses: tauri-apps/tauri-action@v0.5.14
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,3 +4,4 @@ node_modules
|
|||||||
devAssets
|
devAssets
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
dist-electron/
|
||||||
|
|||||||
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"liveServer.settings.port": 5501
|
||||||
|
}
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
# Paarrot API Quick Start
|
|
||||||
|
|
||||||
A local HTTP API server for controlling Paarrot from external devices like Stream Deck, scripts, and automation tools.
|
|
||||||
|
|
||||||
## 🚀 Getting Started
|
|
||||||
|
|
||||||
The API server starts automatically when you run Paarrot. It listens on `http://127.0.0.1:33384`.
|
|
||||||
|
|
||||||
### Test the API
|
|
||||||
|
|
||||||
**Option 1: Using Postman**
|
|
||||||
|
|
||||||
Import the Postman collection for easy testing:
|
|
||||||
1. Open Postman
|
|
||||||
2. Click **Import** → **File**
|
|
||||||
3. Select `paarrot-api.postman_collection.json`
|
|
||||||
4. All endpoints will be ready to use!
|
|
||||||
|
|
||||||
The collection is automatically updated on git push.
|
|
||||||
|
|
||||||
**Option 2: Using the test script**
|
|
||||||
```bash
|
|
||||||
node test-api.js
|
|
||||||
```
|
|
||||||
|
|
||||||
**Option 3: Using curl**
|
|
||||||
```bash
|
|
||||||
./test-api.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
**Option 4: Manual curl commands**
|
|
||||||
```bash
|
|
||||||
# Health check
|
|
||||||
curl http://127.0.0.1:33384/health
|
|
||||||
|
|
||||||
# Toggle mute
|
|
||||||
curl -X POST http://127.0.0.1:33384/mute/toggle
|
|
||||||
|
|
||||||
# Send message
|
|
||||||
curl -X POST http://127.0.0.1:33384/message/current \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"message": "Hello from API!"}'
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📝 Quick Reference
|
|
||||||
|
|
||||||
### Common Endpoints
|
|
||||||
|
|
||||||
| Endpoint | Method | Description |
|
|
||||||
|----------|--------|-------------|
|
|
||||||
| `/health` | GET | Check if API is running |
|
|
||||||
| `/status` | GET | Get app status (mute, deafen, etc.) |
|
|
||||||
| `/mute/toggle` | POST | Toggle microphone mute |
|
|
||||||
| `/deafen/toggle` | POST | Toggle deafen |
|
|
||||||
| `/channels` | GET | Get list of rooms |
|
|
||||||
| `/channel` | POST | Switch to a room |
|
|
||||||
| `/message/current` | POST | Send message to current room |
|
|
||||||
|
|
||||||
## 🎮 Stream Deck Integration
|
|
||||||
|
|
||||||
1. Install the "API Ninja" or "HTTP Request" plugin
|
|
||||||
2. Create buttons with these settings:
|
|
||||||
|
|
||||||
**Mute Toggle Button:**
|
|
||||||
- URL: `http://127.0.0.1:33384/mute/toggle`
|
|
||||||
- Method: POST
|
|
||||||
|
|
||||||
**Quick Message Button:**
|
|
||||||
- URL: `http://127.0.0.1:33384/message/current`
|
|
||||||
- Method: POST
|
|
||||||
- Body: `{"message": "BRB!"}`
|
|
||||||
|
|
||||||
## 🔧 Integration with Paarrot
|
|
||||||
|
|
||||||
To enable full functionality, you need to implement the action handlers in the Cinny frontend:
|
|
||||||
|
|
||||||
1. Import the API handler in your app initialization:
|
|
||||||
```typescript
|
|
||||||
import { initPaarrotAPI } from './app/paarrot-api';
|
|
||||||
|
|
||||||
// After Matrix client is initialized
|
|
||||||
initPaarrotAPI(matrixClient);
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Implement the TODO items in `cinny/src/app/paarrot-api.ts`:
|
|
||||||
- Mute/unmute logic (WebRTC audio)
|
|
||||||
- Deafen logic (WebRTC audio output)
|
|
||||||
- Navigation to rooms (router integration)
|
|
||||||
- Get current room from URL/state
|
|
||||||
|
|
||||||
## 📖 Full Documentation
|
|
||||||
|
|
||||||
See [API.md](API.md) for complete API documentation including:
|
|
||||||
- All available endpoints
|
|
||||||
- Request/response formats
|
|
||||||
- Error handling
|
|
||||||
- Code examples in multiple languages
|
|
||||||
- Detailed integration guide
|
|
||||||
|
|
||||||
## 🛠️ Configuration
|
|
||||||
|
|
||||||
Edit `electron/api-server.js` to customize:
|
|
||||||
- Port number (default: 33384)
|
|
||||||
- Timeout duration (default: 10s)
|
|
||||||
- CORS settings
|
|
||||||
|
|
||||||
## 🐛 Troubleshooting
|
|
||||||
|
|
||||||
**API not responding:**
|
|
||||||
1. Check if Paarrot is running
|
|
||||||
2. Look for "Paarrot API server listening" in console logs
|
|
||||||
3. Verify port 33384 is not in use: `lsof -i :33384`
|
|
||||||
|
|
||||||
**Actions not working:**
|
|
||||||
1. Check browser console for errors
|
|
||||||
2. Ensure `initPaarrotAPI()` is called in your app
|
|
||||||
3. Implement the TODO items in `paarrot-api.ts`
|
|
||||||
|
|
||||||
**Port in use:**
|
|
||||||
Edit `electron/api-server.js` and change the port number in the constructor.
|
|
||||||
|
|
||||||
## 📦 Files
|
|
||||||
|
|
||||||
- `electron/api-server.js` - API server implementation
|
|
||||||
- `cinny/src/app/paarrot-api.ts` - Client-side handler (needs implementation)
|
|
||||||
- `test-api.js` - Node.js test script
|
|
||||||
- `test-api.sh` - Bash test script
|
|
||||||
- `API.md` - Full documentation
|
|
||||||
|
|
||||||
## 🔐 Security
|
|
||||||
|
|
||||||
- API only listens on localhost (127.0.0.1)
|
|
||||||
- Not accessible from network
|
|
||||||
- No authentication required (local only)
|
|
||||||
- CORS enabled for all origins (safe since localhost only)
|
|
||||||
|
|
||||||
## 💡 Examples
|
|
||||||
|
|
||||||
**Python Script:**
|
|
||||||
```python
|
|
||||||
import requests
|
|
||||||
|
|
||||||
def toggle_mute():
|
|
||||||
r = requests.post('http://127.0.0.1:33384/mute/toggle')
|
|
||||||
print(r.json())
|
|
||||||
|
|
||||||
toggle_mute()
|
|
||||||
```
|
|
||||||
|
|
||||||
**JavaScript:**
|
|
||||||
```javascript
|
|
||||||
async function sendQuickMessage(msg) {
|
|
||||||
const res = await fetch('http://127.0.0.1:33384/message/current', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ message: msg })
|
|
||||||
});
|
|
||||||
return res.json();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🎯 Next Steps
|
|
||||||
|
|
||||||
1. Run Paarrot: `npm run dev`
|
|
||||||
2. Test the API: `node test-api.js`
|
|
||||||
3. Integrate the handler: Import and call `initPaarrotAPI()`
|
|
||||||
4. Implement the TODO items in `paarrot-api.ts`
|
|
||||||
5. Create Stream Deck buttons or automation scripts!
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
For more details, see [API.md](API.md)
|
|
||||||
537
API.md
537
API.md
@@ -1,537 +0,0 @@
|
|||||||
# Paarrot API Server Documentation
|
|
||||||
|
|
||||||
The Paarrot API server provides a local HTTP API for controlling the Paarrot desktop app from external applications like Stream Deck, keyboard macros, scripts, and other automation tools.
|
|
||||||
|
|
||||||
## Server Details
|
|
||||||
|
|
||||||
- **Base URL**: `http://127.0.0.1:33384`
|
|
||||||
- **Protocol**: HTTP
|
|
||||||
- **Content-Type**: `application/json`
|
|
||||||
- **CORS**: Enabled for all origins
|
|
||||||
|
|
||||||
## Authentication
|
|
||||||
|
|
||||||
Currently, no authentication is required. The API server only listens on localhost (127.0.0.1) for security.
|
|
||||||
|
|
||||||
## API Endpoints
|
|
||||||
|
|
||||||
### Health Check
|
|
||||||
|
|
||||||
Check if the API server is running.
|
|
||||||
|
|
||||||
**Endpoint**: `GET /health`
|
|
||||||
|
|
||||||
**Response**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "ok",
|
|
||||||
"app": "Paarrot API",
|
|
||||||
"version": "1.0.0"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Get Status
|
|
||||||
|
|
||||||
Get the current status of the app (mute, deafen, current room, etc.).
|
|
||||||
|
|
||||||
**Endpoint**: `GET /status`
|
|
||||||
|
|
||||||
**Response**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"data": {
|
|
||||||
"muted": false,
|
|
||||||
"deafened": false,
|
|
||||||
"currentRoom": "!abc123:matrix.org",
|
|
||||||
"connected": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Mute/Unmute
|
|
||||||
|
|
||||||
Set the mute status.
|
|
||||||
|
|
||||||
**Endpoint**: `POST /mute`
|
|
||||||
|
|
||||||
**Request Body**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"muted": true
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"data": {
|
|
||||||
"muted": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Toggle Mute
|
|
||||||
|
|
||||||
Toggle the current mute status.
|
|
||||||
|
|
||||||
**Endpoint**: `POST /mute/toggle`
|
|
||||||
|
|
||||||
**Response**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"data": {
|
|
||||||
"muted": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Deafen/Undeafen
|
|
||||||
|
|
||||||
Set the deafen status (deafening also mutes).
|
|
||||||
|
|
||||||
**Endpoint**: `POST /deafen`
|
|
||||||
|
|
||||||
**Request Body**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"deafened": true
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"data": {
|
|
||||||
"deafened": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Toggle Deafen
|
|
||||||
|
|
||||||
Toggle the current deafen status.
|
|
||||||
|
|
||||||
**Endpoint**: `POST /deafen/toggle`
|
|
||||||
|
|
||||||
**Response**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"data": {
|
|
||||||
"deafened": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Change Channel/Room
|
|
||||||
|
|
||||||
Switch to a different Matrix room.
|
|
||||||
|
|
||||||
**Endpoint**: `POST /channel`
|
|
||||||
|
|
||||||
**Request Body**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"roomId": "!abc123:matrix.org"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"data": {
|
|
||||||
"roomId": "!abc123:matrix.org",
|
|
||||||
"roomName": "General"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Get Channels/Rooms
|
|
||||||
|
|
||||||
Get a list of available rooms/channels.
|
|
||||||
|
|
||||||
**Endpoint**: `GET /channels`
|
|
||||||
|
|
||||||
**Response**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"data": [
|
|
||||||
{
|
|
||||||
"roomId": "!abc123:matrix.org",
|
|
||||||
"name": "General",
|
|
||||||
"server": "matrix.org",
|
|
||||||
"isDirect": false,
|
|
||||||
"avatar": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"roomId": "!def456:matrix.org",
|
|
||||||
"name": "Random",
|
|
||||||
"server": "matrix.org",
|
|
||||||
"isDirect": false,
|
|
||||||
"avatar": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"roomId": "!xyz789:example.com",
|
|
||||||
"name": "General",
|
|
||||||
"server": "example.com",
|
|
||||||
"isDirect": false,
|
|
||||||
"avatar": null
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Note**: The `server` field helps distinguish between rooms with the same name on different servers.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Send Message
|
|
||||||
|
|
||||||
Send a message to a specific room.
|
|
||||||
|
|
||||||
**Endpoint**: `POST /message`
|
|
||||||
|
|
||||||
**Request Body**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"roomId": "!abc123:matrix.org",
|
|
||||||
"message": "Hello from the API!"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"data": {
|
|
||||||
"eventId": "$abc123",
|
|
||||||
"roomId": "!abc123:matrix.org"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Send Message to Current Room
|
|
||||||
|
|
||||||
Send a message to the currently active room.
|
|
||||||
|
|
||||||
**Endpoint**: `POST /message/current`
|
|
||||||
|
|
||||||
**Request Body**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"message": "Hello from the API!"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"data": {
|
|
||||||
"eventId": "$abc123",
|
|
||||||
"roomId": "!abc123:matrix.org"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Get Current Room
|
|
||||||
|
|
||||||
Get information about the currently active room.
|
|
||||||
|
|
||||||
**Endpoint**: `GET /room/current`
|
|
||||||
|
|
||||||
**Response**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"data": {
|
|
||||||
"roomId": "!abc123:matrix.org",
|
|
||||||
"name": "General",
|
|
||||||
"server": "matrix.org",
|
|
||||||
"avatar": "mxc://matrix.org/abc123",
|
|
||||||
"isDirect": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Error Responses
|
|
||||||
|
|
||||||
All endpoints return error responses in the following format:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"error": "Error message here"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Common HTTP status codes:
|
|
||||||
- `200 OK` - Request successful
|
|
||||||
- `400 Bad Request` - Missing or invalid parameters
|
|
||||||
- `404 Not Found` - Endpoint not found
|
|
||||||
- `500 Internal Server Error` - Server error
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Example Usage
|
|
||||||
|
|
||||||
### cURL
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Toggle mute
|
|
||||||
curl -X POST http://127.0.0.1:33384/mute/toggle
|
|
||||||
|
|
||||||
# Send message to current room
|
|
||||||
curl -X POST http://127.0.0.1:33384/message/current \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"message": "Hello!"}'
|
|
||||||
|
|
||||||
# Change channel
|
|
||||||
curl -X POST http://127.0.0.1:33384/channel \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"roomId": "!abc123:matrix.org"}'
|
|
||||||
```
|
|
||||||
|
|
||||||
### JavaScript/Node.js
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const fetch = require('node-fetch');
|
|
||||||
|
|
||||||
// Toggle mute
|
|
||||||
async function toggleMute() {
|
|
||||||
const response = await fetch('http://127.0.0.1:33384/mute/toggle', {
|
|
||||||
method: 'POST'
|
|
||||||
});
|
|
||||||
const data = await response.json();
|
|
||||||
console.log('Muted:', data.data.muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send message
|
|
||||||
async function sendMessage(roomId, message) {
|
|
||||||
const response = await fetch('http://127.0.0.1:33384/message', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ roomId, message })
|
|
||||||
});
|
|
||||||
const data = await response.json();
|
|
||||||
console.log('Message sent:', data.data.eventId);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Python
|
|
||||||
|
|
||||||
```python
|
|
||||||
import requests
|
|
||||||
|
|
||||||
# Toggle mute
|
|
||||||
def toggle_mute():
|
|
||||||
response = requests.post('http://127.0.0.1:33384/mute/toggle')
|
|
||||||
data = response.json()
|
|
||||||
print('Muted:', data['data']['muted'])
|
|
||||||
|
|
||||||
# Send message
|
|
||||||
def send_message(room_id, message):
|
|
||||||
response = requests.post('http://127.0.0.1:33384/message', json={
|
|
||||||
'roomId': room_id,
|
|
||||||
'message': message
|
|
||||||
})
|
|
||||||
data = response.json()
|
|
||||||
print('Message sent:', data['data']['eventId'])
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Stream Deck Integration
|
|
||||||
|
|
||||||
To integrate with Stream Deck:
|
|
||||||
|
|
||||||
1. Install the "API Ninja" or "HTTP Request" plugin for Stream Deck
|
|
||||||
2. Create a button with the following configuration:
|
|
||||||
- **URL**: `http://127.0.0.1:33384/mute/toggle`
|
|
||||||
- **Method**: POST
|
|
||||||
- **Headers**: `Content-Type: application/json`
|
|
||||||
|
|
||||||
Example configurations:
|
|
||||||
|
|
||||||
**Mute Toggle Button**:
|
|
||||||
- URL: `http://127.0.0.1:33384/mute/toggle`
|
|
||||||
- Method: POST
|
|
||||||
|
|
||||||
**Deafen Toggle Button**:
|
|
||||||
- URL: `http://127.0.0.1:33384/deafen/toggle`
|
|
||||||
- Method: POST
|
|
||||||
|
|
||||||
**Send Quick Message**:
|
|
||||||
- URL: `http://127.0.0.1:33384/message/current`
|
|
||||||
- Method: POST
|
|
||||||
- Body: `{"message": "BRB!"}`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Client-Side Implementation
|
|
||||||
|
|
||||||
To enable the API functionality in the Paarrot app, you need to implement the action handlers in your Matrix client code. The API server sends actions via IPC to the renderer process, which should handle them and respond.
|
|
||||||
|
|
||||||
### Example Handler (TypeScript/React)
|
|
||||||
|
|
||||||
Add this to your app initialization:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Listen for API actions from the Electron API server
|
|
||||||
if (window.electron?.api?.onAction) {
|
|
||||||
window.electron.api.onAction(async (action: {
|
|
||||||
action: string;
|
|
||||||
params: any;
|
|
||||||
responseChannel: string;
|
|
||||||
}) => {
|
|
||||||
try {
|
|
||||||
let result;
|
|
||||||
|
|
||||||
switch (action.action) {
|
|
||||||
case 'toggle-mute':
|
|
||||||
// Toggle microphone mute
|
|
||||||
result = await toggleMicrophone();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'set-mute':
|
|
||||||
// Set microphone mute state
|
|
||||||
result = await setMicrophone(action.params.muted);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'toggle-deafen':
|
|
||||||
// Toggle deafen (mute speakers)
|
|
||||||
result = await toggleDeafen();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'set-deafen':
|
|
||||||
// Set deafen state
|
|
||||||
result = await setDeafen(action.params.deafened);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'change-channel':
|
|
||||||
// Navigate to a different room
|
|
||||||
result = await changeRoom(action.params.roomId);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'get-channels':
|
|
||||||
// Get list of rooms
|
|
||||||
result = await getRoomList();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'send-message':
|
|
||||||
// Send message to specific room
|
|
||||||
result = await sendMessage(action.params.roomId, action.params.message);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'send-message-current':
|
|
||||||
// Send message to current room
|
|
||||||
result = await sendMessageToCurrent(action.params.message);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'get-current-room':
|
|
||||||
// Get current room info
|
|
||||||
result = await getCurrentRoomInfo();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'get-status':
|
|
||||||
// Get current app status
|
|
||||||
result = await getAppStatus();
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new Error(`Unknown action: ${action.action}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send success response back
|
|
||||||
window.electron.api.sendResponse(action.responseChannel, {
|
|
||||||
success: true,
|
|
||||||
data: result
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
// Send error response back
|
|
||||||
window.electron.api.sendResponse(action.responseChannel, {
|
|
||||||
success: false,
|
|
||||||
error: error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
The API server can be configured by modifying `electron/api-server.js`:
|
|
||||||
|
|
||||||
- **Default Port**: 33384 (can be changed in the constructor)
|
|
||||||
- **Bind Address**: 127.0.0.1 (localhost only for security)
|
|
||||||
- **Timeout**: 10 seconds for actions
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Security Notes
|
|
||||||
|
|
||||||
1. The API server only listens on localhost (127.0.0.1) and is not accessible from the network
|
|
||||||
2. No authentication is implemented - anyone with local access can control the app
|
|
||||||
3. Consider adding API key authentication if needed for your use case
|
|
||||||
4. CORS is enabled for all origins since it's localhost only
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
**API server not starting**:
|
|
||||||
- Check if port 33384 is already in use
|
|
||||||
- Check console logs for errors
|
|
||||||
- Verify Express is installed: `npm install express cors body-parser`
|
|
||||||
|
|
||||||
**Actions not working**:
|
|
||||||
- Ensure the client-side handler is implemented
|
|
||||||
- Check browser console for errors
|
|
||||||
- Verify the action names match between API server and client
|
|
||||||
|
|
||||||
**Timeout errors**:
|
|
||||||
- Actions must respond within 10 seconds
|
|
||||||
- Check if the Matrix client is initialized
|
|
||||||
- Verify the action handler is registered
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Future Enhancements
|
|
||||||
|
|
||||||
Potential features to add:
|
|
||||||
- WebSocket support for real-time events
|
|
||||||
- Authentication with API keys
|
|
||||||
- Voice channel controls (join/leave)
|
|
||||||
- User status updates
|
|
||||||
- Notification controls
|
|
||||||
- Screen sharing controls
|
|
||||||
51
README.md
51
README.md
@@ -1,4 +1,4 @@
|
|||||||
# Paarrot
|
# Paarrot
|
||||||
|
|
||||||
Paarrot is a Matrix client focusing primarily on simple, elegant and secure interface. The desktop app is built with Electron and based on Cinny.
|
Paarrot is a Matrix client focusing primarily on simple, elegant and secure interface. The desktop app is built with Electron and based on Cinny.
|
||||||
|
|
||||||
@@ -22,10 +22,57 @@ To setup development locally run the following commands:
|
|||||||
* `cd cinny-desktop/cinny`
|
* `cd cinny-desktop/cinny`
|
||||||
* `npm ci`
|
* `npm ci`
|
||||||
* `cd ..`
|
* `cd ..`
|
||||||
* `npm ci`
|
* `npm ci`
|
||||||
|
|
||||||
To build the app locally, run:
|
To build the app locally, run:
|
||||||
* `npm run build`
|
* `npm run build`
|
||||||
|
|
||||||
To start local dev server, run:
|
To start local dev server, run:
|
||||||
* `npm run dev`
|
* `npm run dev`
|
||||||
|
|
||||||
|
## Plugin System
|
||||||
|
|
||||||
|
Paarrot includes a plugin system for extending and customising the app with JavaScript modules.
|
||||||
|
|
||||||
|
### Installing Plugins
|
||||||
|
|
||||||
|
1. Drop a plugin folder into your plugins directory:
|
||||||
|
- **Windows**: `%APPDATA%\paarrot\plugins\<plugin-name>\`
|
||||||
|
- **Linux**: `~/.config/Paarrot/plugins/<plugin-name>/`
|
||||||
|
- **macOS**: `~/Library/Application Support/Paarrot/plugins/<plugin-name>/`
|
||||||
|
2. Enable it in **Settings → Plugins → Installed**
|
||||||
|
|
||||||
|
### What Plugins Can Do
|
||||||
|
|
||||||
|
- Register custom slash commands
|
||||||
|
- Intercept and modify messages
|
||||||
|
- Inject buttons into 11 UI locations (nav lists, toolbars, headers, sidebar, menus)
|
||||||
|
- Register custom themes
|
||||||
|
- Hook into raw Matrix events
|
||||||
|
- Run background tasks
|
||||||
|
- Show system notifications
|
||||||
|
|
||||||
|
### Plugin Locations (UI Buttons)
|
||||||
|
|
||||||
|
Plugins can inject buttons as **nav list rows** or **icon buttons** across the app:
|
||||||
|
|
||||||
|
| Location | Style | Where |
|
||||||
|
|---|---|---|
|
||||||
|
| `channel-list` | Nav row | Space channel list |
|
||||||
|
| `home-section` | Nav row | Home panel, above room list |
|
||||||
|
| `direct-messages` | Nav row | DMs panel, below "Create Chat" |
|
||||||
|
| `sidebar-actions` | Icon | Left sidebar — above Explore and above Search |
|
||||||
|
| `text-composer-toolbar` | Icon | Message composer toolbar |
|
||||||
|
| `composer-actions` | Icon | Beside the `+` attach button |
|
||||||
|
| `room-header` | Icon | Room header bar |
|
||||||
|
| `room-menu` | Icon | Room ⋮ dropdown |
|
||||||
|
| `message-actions` | Icon | Message hover bar |
|
||||||
|
| `user-menu` | Icon | Right-click on user avatar |
|
||||||
|
| `search-notification-section` | Icon | Notifications page header |
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- [Plugin System Overview](PLUGINS.md)
|
||||||
|
- [Full Plugin API Reference](docs/PLUGIN_API.md)
|
||||||
|
- [Button Registration API](docs/PLUGIN_BUTTON_API.md)
|
||||||
|
- [Example Plugins](plugins/)
|
||||||
|
|||||||
10
build-and-publish.sh
Normal file
10
build-and-publish.sh
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Set GitHub token for publishing
|
||||||
|
export GH_TOKEN="ghtoken"
|
||||||
|
|
||||||
|
# Run the build and publish
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Exit with the build's exit code
|
||||||
|
exit $?
|
||||||
2
cinny
2
cinny
Submodule cinny updated: fe193bb062...f62200ecd1
19
config.json
19
config.json
@@ -9,30 +9,13 @@
|
|||||||
"xmr.se"
|
"xmr.se"
|
||||||
],
|
],
|
||||||
"allowCustomHomeservers": true,
|
"allowCustomHomeservers": true,
|
||||||
|
|
||||||
"calling": {
|
|
||||||
"livekitServiceUrl": "https://b.ruv.wtf/matrix-rtc/livekit/jwt"
|
|
||||||
},
|
|
||||||
|
|
||||||
"featuredCommunities": {
|
"featuredCommunities": {
|
||||||
"openAsDefault": false,
|
"openAsDefault": false,
|
||||||
"spaces": [
|
"spaces": [
|
||||||
"#cinny-space:matrix.org",
|
|
||||||
"#community:matrix.org",
|
|
||||||
"#space:envs.net",
|
|
||||||
"#science-space:matrix.org",
|
|
||||||
"#libregaming-games:tchncs.de",
|
|
||||||
"#mathematics-on:matrix.org"
|
|
||||||
],
|
],
|
||||||
"rooms": [
|
"rooms": [
|
||||||
"#cinny:matrix.org",
|
|
||||||
"#freesoftware:matrix.org",
|
|
||||||
"#pcapdroid:matrix.org",
|
|
||||||
"#gentoo:matrix.org",
|
|
||||||
"#PrivSec.dev:arcticfoxes.net",
|
|
||||||
"#disroot:aria-net.org"
|
|
||||||
],
|
],
|
||||||
"servers": ["envs.net", "matrix.org", "monero.social", "mozilla.org"]
|
"servers": []
|
||||||
},
|
},
|
||||||
|
|
||||||
"hashRouter": {
|
"hashRouter": {
|
||||||
|
|||||||
4
dev-app-update.yml
Normal file
4
dev-app-update.yml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
provider: github
|
||||||
|
owner: Paarrot
|
||||||
|
repo: Paarrot-Desktop
|
||||||
|
updaterCacheDirName: paarrot-updater
|
||||||
BIN
dist-electron/win-unpacked/resources/app.asar
Normal file
BIN
dist-electron/win-unpacked/resources/app.asar
Normal file
Binary file not shown.
@@ -10,6 +10,7 @@
|
|||||||
"files": [
|
"files": [
|
||||||
"electron/**/*",
|
"electron/**/*",
|
||||||
"cinny/dist/**/*",
|
"cinny/dist/**/*",
|
||||||
|
"dev-app-update.yml",
|
||||||
"!**/*.map",
|
"!**/*.map",
|
||||||
"!**/node_modules/*/{CHANGELOG.md,README.md,README,readme.md,readme}",
|
"!**/node_modules/*/{CHANGELOG.md,README.md,README,readme.md,readme}",
|
||||||
"!**/node_modules/*/{test,__tests__,tests,powered-test,example,examples}",
|
"!**/node_modules/*/{test,__tests__,tests,powered-test,example,examples}",
|
||||||
@@ -28,6 +29,17 @@
|
|||||||
"from": "icons",
|
"from": "icons",
|
||||||
"to": "icons",
|
"to": "icons",
|
||||||
"filter": ["**/*"]
|
"filter": ["**/*"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "cinny/public/sound",
|
||||||
|
"to": "sound",
|
||||||
|
"filter": ["**/*"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"protocols": [
|
||||||
|
{
|
||||||
|
"name": "Paarrot URL",
|
||||||
|
"schemes": ["paarrot"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"linux": {
|
"linux": {
|
||||||
@@ -45,6 +57,7 @@
|
|||||||
"Name": "Paarrot",
|
"Name": "Paarrot",
|
||||||
"GenericName": "Matrix Client",
|
"GenericName": "Matrix Client",
|
||||||
"Comment": "A Matrix client built with Cinny",
|
"Comment": "A Matrix client built with Cinny",
|
||||||
|
"MimeType": "x-scheme-handler/paarrot;",
|
||||||
"Categories": "Network;InstantMessaging;",
|
"Categories": "Network;InstantMessaging;",
|
||||||
"Keywords": "matrix;chat;messaging;",
|
"Keywords": "matrix;chat;messaging;",
|
||||||
"StartupWMClass": "paarrot"
|
"StartupWMClass": "paarrot"
|
||||||
@@ -72,23 +85,30 @@
|
|||||||
"win": {
|
"win": {
|
||||||
"target": [
|
"target": [
|
||||||
{
|
{
|
||||||
"target": "squirrel",
|
"target": "nsis",
|
||||||
"arch": ["x64"]
|
"arch": ["x64"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "icons/icon.ico",
|
"icon": "icons/icon.ico",
|
||||||
"artifactName": "${productName}-${version}-${os}-${arch}.${ext}"
|
"artifactName": "${productName}-${version}-${os}-${arch}.${ext}"
|
||||||
},
|
},
|
||||||
"squirrelWindows": {
|
"nsis": {
|
||||||
"name": "Paarrot",
|
"oneClick": true,
|
||||||
"loadingGif": "icons/icon.png",
|
"perMachine": false,
|
||||||
"iconUrl": "http://synbox.ruv.wtf:8418/litruv/cinny-desktop/raw/branch/main/icons/icon.ico",
|
"allowToChangeInstallationDirectory": false,
|
||||||
"remoteReleases": false,
|
"deleteAppDataOnUninstall": false,
|
||||||
"useAppIdAsId": true
|
"createDesktopShortcut": true,
|
||||||
|
"createStartMenuShortcut": true,
|
||||||
|
"shortcutName": "Paarrot",
|
||||||
|
"differentialPackage": false
|
||||||
},
|
},
|
||||||
"publish": {
|
"compression": "normal",
|
||||||
"provider": "generic",
|
"publish": [
|
||||||
"url": "http://synbox.ruv.wtf:8418/litruv/cinny-desktop/releases/download/v${version}",
|
{
|
||||||
"channel": "latest"
|
"provider": "github",
|
||||||
}
|
"owner": "Paarrot",
|
||||||
|
"repo": "Paarrot-Desktop",
|
||||||
|
"releaseType": "release"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
1438
electron/main.js
1438
electron/main.js
File diff suppressed because it is too large
Load Diff
@@ -18,7 +18,8 @@ const commandMap = {
|
|||||||
'check_for_updates': 'check-for-updates',
|
'check_for_updates': 'check-for-updates',
|
||||||
'download_update': 'download-update',
|
'download_update': 'download-update',
|
||||||
'install_update': 'install-update',
|
'install_update': 'install-update',
|
||||||
'get_desktop_sources': 'get-desktop-sources'
|
'get_desktop_sources': 'get-desktop-sources',
|
||||||
|
'get_protocol_status': 'protocol:get-status'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Shared invoke function for legacy API compatibility
|
// Shared invoke function for legacy API compatibility
|
||||||
@@ -105,7 +106,8 @@ contextBridge.exposeInMainWorld('electron', {
|
|||||||
},
|
},
|
||||||
// Audio
|
// Audio
|
||||||
audio: {
|
audio: {
|
||||||
playNotificationSound: (soundType = 'message') => ipcRenderer.invoke('play-notification-sound', soundType)
|
playNotificationSound: (soundType = 'message') => ipcRenderer.invoke('play-notification-sound', soundType),
|
||||||
|
getSoundBaseUrl: () => ipcRenderer.invoke('get-sound-base-url'),
|
||||||
},
|
},
|
||||||
// External URLs
|
// External URLs
|
||||||
shell: {
|
shell: {
|
||||||
@@ -128,6 +130,7 @@ contextBridge.exposeInMainWorld('electron', {
|
|||||||
updater: {
|
updater: {
|
||||||
checkForUpdates: () => ipcRenderer.invoke('check-for-updates'),
|
checkForUpdates: () => ipcRenderer.invoke('check-for-updates'),
|
||||||
downloadAndInstallUpdate: () => ipcRenderer.invoke('download-and-install-update'),
|
downloadAndInstallUpdate: () => ipcRenderer.invoke('download-and-install-update'),
|
||||||
|
isMock: () => ipcRenderer.invoke('updater-is-mock'),
|
||||||
onUpdateAvailable: (callback) => {
|
onUpdateAvailable: (callback) => {
|
||||||
ipcRenderer.on('update-available', (_, info) => callback(info));
|
ipcRenderer.on('update-available', (_, info) => callback(info));
|
||||||
},
|
},
|
||||||
@@ -137,6 +140,9 @@ contextBridge.exposeInMainWorld('electron', {
|
|||||||
onUpdateDownloaded: (callback) => {
|
onUpdateDownloaded: (callback) => {
|
||||||
ipcRenderer.on('update-downloaded', (_, info) => callback(info));
|
ipcRenderer.on('update-downloaded', (_, info) => callback(info));
|
||||||
},
|
},
|
||||||
|
onUpdateNotAvailable: (callback) => {
|
||||||
|
ipcRenderer.on('update-not-available', (_, info) => callback(info));
|
||||||
|
},
|
||||||
downloadUpdate: () => ipcRenderer.invoke('download-update'),
|
downloadUpdate: () => ipcRenderer.invoke('download-update'),
|
||||||
installUpdate: () => ipcRenderer.invoke('install-update'),
|
installUpdate: () => ipcRenderer.invoke('install-update'),
|
||||||
},
|
},
|
||||||
@@ -166,6 +172,21 @@ contextBridge.exposeInMainWorld('electron', {
|
|||||||
ipcRenderer.on('notification:navigate', handler);
|
ipcRenderer.on('notification:navigate', handler);
|
||||||
return () => ipcRenderer.removeListener('notification:navigate', handler);
|
return () => ipcRenderer.removeListener('notification:navigate', handler);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Plugin management
|
||||||
|
plugins: {
|
||||||
|
getPath: () => ipcRenderer.invoke('plugin:get-path'),
|
||||||
|
download: (pluginId, downloadUrl, name) => ipcRenderer.invoke('plugin:download', { pluginId, downloadUrl, name }),
|
||||||
|
list: () => ipcRenderer.invoke('plugin:list'),
|
||||||
|
uninstall: (pluginId) => ipcRenderer.invoke('plugin:uninstall', pluginId),
|
||||||
|
readPluginCode: (pluginId) => ipcRenderer.invoke('plugin:read-code', pluginId)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Desktop protocol diagnostics
|
||||||
|
protocol: {
|
||||||
|
getStatus: () => ipcRenderer.invoke('protocol:get-status'),
|
||||||
|
repair: () => ipcRenderer.invoke('protocol:repair')
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
161
example-plugin/index.js
Normal file
161
example-plugin/index.js
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
// Example plugin for Paarrot - Demonstrates all plugin API features
|
||||||
|
// This plugin showcases commands, message interceptors, settings, logging, and more
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name: "Example Plugin",
|
||||||
|
version: "2.0.0",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the plugin is loaded
|
||||||
|
* @param {PluginContext} ctx - The plugin context with access to all APIs
|
||||||
|
*/
|
||||||
|
onLoad: async (ctx) => {
|
||||||
|
ctx.log('🎉 Example plugin is loading...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Register commands with args
|
||||||
|
ctx.commands.register({
|
||||||
|
name: "shrug",
|
||||||
|
description: "Send a shrug emoji",
|
||||||
|
run: () => {
|
||||||
|
ctx.log('Shrug command executed');
|
||||||
|
return "¯\\_(ツ)_/¯";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.commands.register({
|
||||||
|
name: "echo",
|
||||||
|
args: ["text"],
|
||||||
|
description: "Echo back your text",
|
||||||
|
run: ({ text }) => {
|
||||||
|
ctx.log('Echo command:', text);
|
||||||
|
return text || "Nothing to echo!";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.commands.register({
|
||||||
|
name: "wave",
|
||||||
|
description: "Wave at someone",
|
||||||
|
args: ["name"],
|
||||||
|
run: ({ name }) => {
|
||||||
|
return `👋 Hey ${name || 'everyone'}!`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Message interceptors
|
||||||
|
ctx.messages.onBeforeSend((msg) => {
|
||||||
|
// Auto-expand abbreviations
|
||||||
|
if (msg.content === "brb") {
|
||||||
|
msg.content = "be right back!";
|
||||||
|
ctx.log('Expanded brb to full phrase');
|
||||||
|
}
|
||||||
|
if (msg.content === "omw") {
|
||||||
|
msg.content = "on my way!";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.messages.onReceive((msg) => {
|
||||||
|
// Log all received messages (for debugging)
|
||||||
|
ctx.log(`Received message in ${msg.roomId}:`, msg.content.substring(0, 50));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Define settings
|
||||||
|
ctx.settings.define({
|
||||||
|
autoShrug: {
|
||||||
|
type: "boolean",
|
||||||
|
label: "Auto Shrug",
|
||||||
|
description: "Automatically append ¯\\_(ツ)_/¯ to messages ending with '?'",
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
type: "select",
|
||||||
|
label: "Plugin Theme",
|
||||||
|
description: "Choose your preferred theme",
|
||||||
|
options: [
|
||||||
|
{ value: "dark", label: "Dark" },
|
||||||
|
{ value: "light", label: "Light" },
|
||||||
|
{ value: "purple", label: "Purple" }
|
||||||
|
],
|
||||||
|
default: "dark"
|
||||||
|
},
|
||||||
|
customPrefix: {
|
||||||
|
type: "string",
|
||||||
|
label: "Custom Prefix",
|
||||||
|
description: "Prefix for plugin commands",
|
||||||
|
default: "[Plugin]"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Use settings
|
||||||
|
const autoShrug = ctx.settings.get('autoShrug');
|
||||||
|
if (autoShrug) {
|
||||||
|
ctx.log('Auto shrug is enabled!');
|
||||||
|
ctx.messages.onBeforeSend((msg) => {
|
||||||
|
if (msg.content.endsWith('?')) {
|
||||||
|
msg.content += ' ¯\\_(ツ)_/¯';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Listen to raw Matrix events
|
||||||
|
ctx.matrix.on('Room.timeline', (event) => {
|
||||||
|
if (event.getType() === 'm.room.message') {
|
||||||
|
const content = event.getContent();
|
||||||
|
if (content.body?.includes('hello')) {
|
||||||
|
ctx.log('Someone said hello!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 5. Background tasks
|
||||||
|
ctx.timers.setInterval(() => {
|
||||||
|
ctx.log('Background task running... (every 30s)');
|
||||||
|
// You could check for notifications, clean up data, etc.
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
|
// 6. Notifications
|
||||||
|
ctx.notify({
|
||||||
|
title: "Example Plugin",
|
||||||
|
body: "Plugin loaded successfully!",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
|
||||||
|
// 7. Custom UI renderer (conceptual - would need UI integration)
|
||||||
|
ctx.ui.registerRenderer("custom-message", (msg) => {
|
||||||
|
ctx.log('Rendering custom message:', msg);
|
||||||
|
// Return custom React component
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 8. Export API for other plugins
|
||||||
|
this.exports = {
|
||||||
|
greet: (name) => {
|
||||||
|
return `Hello, ${name}!`;
|
||||||
|
},
|
||||||
|
version: "2.0.0"
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.log('✅ Example plugin loaded successfully!');
|
||||||
|
ctx.log('Registered commands: /shrug, /echo, /wave');
|
||||||
|
ctx.log('Settings:', {
|
||||||
|
autoShrug: ctx.settings.get('autoShrug'),
|
||||||
|
theme: ctx.settings.get('theme'),
|
||||||
|
prefix: ctx.settings.get('customPrefix')
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
ctx.error('Failed to load example plugin:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the plugin is unloaded or disabled
|
||||||
|
*/
|
||||||
|
onUnload: async () => {
|
||||||
|
console.log('[ExamplePlugin] Plugin is unloading...');
|
||||||
|
console.log('[ExamplePlugin] Cleaned up successfully!');
|
||||||
|
},
|
||||||
|
|
||||||
|
// Exports for other plugins to use via ctx.require()
|
||||||
|
exports: null // Will be set during onLoad
|
||||||
|
};
|
||||||
9
example-plugin/plugin-metadata.json
Normal file
9
example-plugin/plugin-metadata.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "Example Plugin",
|
||||||
|
"version": "2.0.0",
|
||||||
|
"description": "A comprehensive example plugin demonstrating all plugin API features including commands, interceptors, settings, and more",
|
||||||
|
"author": "Paarrot Team",
|
||||||
|
"homepage": "https://github.com/Paarrot/cinny-desktop",
|
||||||
|
"thumbnail": "https://raw.githubusercontent.com/Paarrot/cinny-desktop/main/icons/icon.png",
|
||||||
|
"tags": ["example", "demo", "utility", "commands", "interceptors"]
|
||||||
|
}
|
||||||
3922
package-lock.json
generated
3922
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
39
package.json
39
package.json
@@ -1,17 +1,22 @@
|
|||||||
{
|
{
|
||||||
"name": "paarrot",
|
"name": "paarrot",
|
||||||
"version": "4.11.64",
|
"version": "4.11.163",
|
||||||
"description": "Paarrot - A Matrix client based on Cinny",
|
"description": "Paarrot - A Matrix client based on Cinny",
|
||||||
"homepage": "http://synbox.ruv.wtf:8418/litruv/cinny-desktop",
|
"homepage": "https://github.com/Paarrot/Paarrot-Desktop",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/Paarrot/Paarrot-Desktop.git"
|
||||||
|
},
|
||||||
"main": "electron/main.js",
|
"main": "electron/main.js",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "concurrently \"npm run dev:vite\" \"npm run dev:electron\"",
|
"dev": "concurrently \"npm run dev:vite\" \"npm run dev:electron\"",
|
||||||
"dev:vite": "cd cinny && npm start",
|
"dev:vite": "cross-env BROWSER=none sh -c 'cd cinny && npm start'",
|
||||||
"dev:electron": "wait-on http://localhost:38347 && cross-env NODE_ENV=development electron .",
|
"dev:electron": "wait-on http://localhost:38347 && cross-env NODE_ENV=development electron .",
|
||||||
"build": "node -e \"require('fs').copyFileSync('config.json', 'cinny/config.json')\" && cd cinny && npm run build && cd .. && electron-builder --publish never",
|
"build": "node -e \"require('fs').copyFileSync('config.json', 'cinny/config.json')\" && cd cinny && npm run build && cd .. && electron-builder --publish always",
|
||||||
|
"build:local": "node -e \"require('fs').copyFileSync('config.json', 'cinny/config.json')\" && cd cinny && npm run build && cd .. && electron-builder",
|
||||||
"build:linux": "npm run build -- --linux",
|
"build:linux": "npm run build -- --linux",
|
||||||
"build:win": "npm run build -- --win",
|
"build:win": "npm run build -- --win",
|
||||||
"postman:generate": "node scripts/generate-postman-collection.js"
|
"postman:generate": "node scripts/generate-postman-collection.js"
|
||||||
@@ -23,24 +28,28 @@
|
|||||||
},
|
},
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"body-parser": "2.2.2",
|
"adm-zip": "^0.5.18",
|
||||||
|
"body-parser": "2.3.0",
|
||||||
"cors": "2.8.6",
|
"cors": "2.8.6",
|
||||||
"electron-squirrel-startup": "^1.0.1",
|
"electron-store": "^11.0.2",
|
||||||
"electron-store": "^8.2.0",
|
"electron-updater": "^6.8.9",
|
||||||
"electron-updater": "^6.3.9",
|
|
||||||
"express": "5.2.1",
|
"express": "5.2.1",
|
||||||
"open": "11.0.0"
|
"open": "11.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@actions/github": "6.0.0",
|
"@actions/github": "9.1.1",
|
||||||
"@electron/rebuild": "4.0.3",
|
"@electron/rebuild": "4.2.0",
|
||||||
"concurrently": "9.2.1",
|
"concurrently": "10.0.3",
|
||||||
"cross-env": "10.1.0",
|
"cross-env": "10.1.0",
|
||||||
"electron": "40.6.0",
|
"electron": "43.1.0",
|
||||||
"electron-builder": "26.8.1",
|
"electron-builder": "26.15.3",
|
||||||
"node-fetch": "3.3.2",
|
"node-fetch": "3.3.2",
|
||||||
"png2icons": "2.0.1",
|
"png2icons": "2.0.1",
|
||||||
"sharp": "0.34.5",
|
"sharp": "0.35.3",
|
||||||
"wait-on": "9.0.4"
|
"wait-on": "9.0.10"
|
||||||
|
},
|
||||||
|
"allowScripts": {
|
||||||
|
"electron-winstaller@5.4.0": true,
|
||||||
|
"electron@43.1.0": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
104
scripts/migrate-icons.mjs
Normal file
104
scripts/migrate-icons.mjs
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const srcRoot = path.resolve(__dirname, '../cinny/src');
|
||||||
|
const iconsModule = path.resolve(srcRoot, 'app/components/icons');
|
||||||
|
|
||||||
|
const ICON_SYMBOLS = new Set(['Icon', 'Icons', 'IconName', 'IconSrc']);
|
||||||
|
|
||||||
|
function walk(dir, files = []) {
|
||||||
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||||
|
const fullPath = path.join(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
walk(fullPath, files);
|
||||||
|
} else if (/\.(tsx?)$/.test(entry.name)) {
|
||||||
|
files.push(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIconsImportPath(filePath) {
|
||||||
|
const rel = path.relative(path.dirname(filePath), iconsModule).replace(/\\/g, '/');
|
||||||
|
return rel.startsWith('.') ? rel : `./${rel}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitFoldsImport(importClause) {
|
||||||
|
const match = importClause.match(/^\{([^}]+)\}(?:\s+as\s+\w+)?$/);
|
||||||
|
if (!match) return null;
|
||||||
|
|
||||||
|
const specifiers = match[1]
|
||||||
|
.split(',')
|
||||||
|
.map((part) => part.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((part) => {
|
||||||
|
const [local, imported] = part.split(/\s+as\s+/).map((s) => s.trim());
|
||||||
|
return { local: local ?? imported, imported: imported ?? local };
|
||||||
|
});
|
||||||
|
|
||||||
|
const iconSpecs = specifiers.filter((s) => ICON_SYMBOLS.has(s.imported));
|
||||||
|
const foldsSpecs = specifiers.filter((s) => !ICON_SYMBOLS.has(s.imported));
|
||||||
|
|
||||||
|
if (iconSpecs.length === 0) return null;
|
||||||
|
|
||||||
|
return { iconSpecs, foldsSpecs };
|
||||||
|
}
|
||||||
|
|
||||||
|
function migrateFile(filePath) {
|
||||||
|
let content = fs.readFileSync(filePath, 'utf8');
|
||||||
|
const importRegex = /import\s+(\{[^}]+\})\s+from\s+['"]folds['"];?/g;
|
||||||
|
let changed = false;
|
||||||
|
const iconImports = new Map();
|
||||||
|
|
||||||
|
content = content.replace(importRegex, (fullMatch, importClause) => {
|
||||||
|
const split = splitFoldsImport(importClause);
|
||||||
|
if (!split) return fullMatch;
|
||||||
|
|
||||||
|
changed = true;
|
||||||
|
for (const spec of split.iconSpecs) {
|
||||||
|
iconImports.set(spec.local, spec.imported);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (split.foldsSpecs.length === 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const foldsImport = split.foldsSpecs.map((s) => (s.local === s.imported ? s.local : `${s.imported} as ${s.local}`)).join(', ');
|
||||||
|
return `import { ${foldsImport} } from 'folds';`;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!changed) return false;
|
||||||
|
|
||||||
|
const iconsPath = getIconsImportPath(filePath);
|
||||||
|
const iconExportList = [...iconImports.entries()]
|
||||||
|
.map(([local, imported]) => (local === imported ? imported : `${imported} as ${local}`))
|
||||||
|
.join(', ');
|
||||||
|
|
||||||
|
const iconsImportLine = `import { ${iconExportList} } from '${iconsPath}';`;
|
||||||
|
|
||||||
|
const foldsImportMatch = content.match(/^import\s+\{[^}]+\}\s+from\s+['"]folds['"];?\s*$/m);
|
||||||
|
if (foldsImportMatch) {
|
||||||
|
const insertAt = content.indexOf(foldsImportMatch[0]) + foldsImportMatch[0].length;
|
||||||
|
content = `${content.slice(0, insertAt)}\n${iconsImportLine}${content.slice(insertAt)}`;
|
||||||
|
} else {
|
||||||
|
content = `${iconsImportLine}\n${content}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
content = content.replace(/\n{3,}/g, '\n\n');
|
||||||
|
fs.writeFileSync(filePath, content);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = walk(srcRoot);
|
||||||
|
let migrated = 0;
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
if (file.includes(`${path.sep}components${path.sep}icons${path.sep}`)) continue;
|
||||||
|
if (migrateFile(file)) {
|
||||||
|
migrated += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Migrated ${migrated} files.`);
|
||||||
Reference in New Issue
Block a user