Compare commits
231 Commits
4fa0ce0d4a
...
v4.11.168
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8b57ba217 | ||
| 8d8809d0c7 | |||
|
|
55a0f4f566 | ||
| f488f32d88 | |||
|
|
9154929de9 | ||
| c2cc0d5ed5 | |||
|
|
aa38348fdd | ||
| 02ab544a37 | |||
|
|
3707a6d38a | ||
| fe1f4abf2d | |||
| 147389724e | |||
|
|
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 |
@@ -8,13 +8,15 @@ on:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
# Large AppImage/exe uploads time out on Gitea's actions artifact pipeline.
|
||||
# Build jobs publish assets straight to Gitea/GitHub releases via the REST API.
|
||||
|
||||
jobs:
|
||||
increment-version:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref == 'refs/heads/main' && !contains(github.event.head_commit.message, '[skip ci]')
|
||||
outputs:
|
||||
version: ${{ steps.bump.outputs.VERSION }}
|
||||
should_build: ${{ steps.bump.outputs.SHOULD_BUILD }}
|
||||
version: ${{ steps.finalize.outputs.VERSION }}
|
||||
should_build: ${{ steps.decide.outputs.SHOULD_BUILD }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -22,9 +24,35 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
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
|
||||
id: bump
|
||||
if: ${{ steps.decide.outputs.SHOULD_BUMP == 'true' }}
|
||||
run: |
|
||||
# Get current version from package.json
|
||||
CURRENT=$(grep -Po '"version":\s*"\K[^"]+' package.json | head -1)
|
||||
@@ -45,20 +73,136 @@ jobs:
|
||||
grep '"version"' package.json | head -1
|
||||
|
||||
echo "VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "SHOULD_BUILD=true" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Commit and push version bump
|
||||
if: ${{ steps.decide.outputs.SHOULD_BUMP == 'true' }}
|
||||
run: |
|
||||
git config user.name "GitHub Actions"
|
||||
git config user.email "actions@github.com"
|
||||
git add package.json
|
||||
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
|
||||
|
||||
prepare-release:
|
||||
runs-on: ubuntu-latest
|
||||
needs: increment-version
|
||||
if: always() && needs.increment-version.outputs.should_build == 'true'
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.VERSION }}
|
||||
tag: ${{ steps.version.outputs.TAG_NAME }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.ref_name }}
|
||||
|
||||
- name: Pull latest (after version bump)
|
||||
if: github.ref == 'refs/heads/main'
|
||||
run: git pull origin main
|
||||
|
||||
- name: Resolve version
|
||||
id: version
|
||||
run: |
|
||||
if [ -n "${{ needs.increment-version.outputs.version }}" ]; then
|
||||
VERSION="${{ needs.increment-version.outputs.version }}"
|
||||
else
|
||||
VERSION=$(grep -Po '"version":\s*"\K[^"]+' package.json | head -1)
|
||||
fi
|
||||
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "TAG_NAME=v$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Version: $VERSION"
|
||||
|
||||
- name: Create and push tag
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.VERSION }}"
|
||||
TAG_NAME="v${VERSION}"
|
||||
if git rev-parse "$TAG_NAME" >/dev/null 2>&1; then
|
||||
echo "Tag $TAG_NAME already exists"
|
||||
else
|
||||
git config user.name "GitHub Actions"
|
||||
git config user.email "actions@github.com"
|
||||
git tag -a "$TAG_NAME" -m "Release $TAG_NAME"
|
||||
git push origin "$TAG_NAME"
|
||||
fi
|
||||
|
||||
- name: Ensure Gitea release
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
VERSION="${{ steps.version.outputs.VERSION }}"
|
||||
TAG_NAME="v${VERSION}"
|
||||
API_BASE="${{ github.server_url }}/api/v1"
|
||||
REPO="${{ github.repository }}"
|
||||
|
||||
RELEASE_ID=$(curl -fsS -H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${API_BASE}/repos/${REPO}/releases/tags/${TAG_NAME}" | jq -r '.id // empty' || true)
|
||||
|
||||
if [ -z "$RELEASE_ID" ]; then
|
||||
echo "Creating Gitea release ${TAG_NAME}..."
|
||||
RELEASE_ID=$(curl -fsS -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"tag_name\": \"${TAG_NAME}\", \"name\": \"Release ${TAG_NAME}\", \"body\": \"Release ${VERSION}\", \"draft\": false, \"prerelease\": false}" \
|
||||
"${API_BASE}/repos/${REPO}/releases" | jq -r '.id')
|
||||
fi
|
||||
|
||||
echo "Gitea release id: ${RELEASE_ID}"
|
||||
|
||||
- name: Ensure GitHub release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GHTOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
VERSION="${{ steps.version.outputs.VERSION }}"
|
||||
TAG_NAME="v${VERSION}"
|
||||
GITHUB_REPO="Paarrot/Paarrot-Desktop"
|
||||
|
||||
RELEASE_ID=$(curl -fsS -H "Authorization: token ${GH_TOKEN}" \
|
||||
"https://api.github.com/repos/${GITHUB_REPO}/releases/tags/${TAG_NAME}" | jq -r '.id // empty' || true)
|
||||
|
||||
if [ -z "$RELEASE_ID" ]; then
|
||||
echo "Creating GitHub release ${TAG_NAME}..."
|
||||
RELEASE_ID=$(curl -fsS -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')
|
||||
fi
|
||||
|
||||
echo "GitHub release id: ${RELEASE_ID}"
|
||||
|
||||
build-windows:
|
||||
runs-on: windows
|
||||
needs: increment-version
|
||||
if: always() && (needs.increment-version.outputs.should_build == 'true' || needs.increment-version.result == 'skipped')
|
||||
needs: [increment-version, prepare-release]
|
||||
if: always() && needs.prepare-release.result == 'success'
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -70,7 +214,32 @@ jobs:
|
||||
- name: Checkout submodules manually
|
||||
shell: powershell
|
||||
run: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
# Use the same host/scheme the runner already used for origin (not public HTTPS).
|
||||
$origin = (git remote get-url origin).Trim()
|
||||
$origin = $origin.TrimEnd('/')
|
||||
if ($origin.EndsWith('.git')) { $origin = $origin.Substring(0, $origin.Length - 4) }
|
||||
$base = $origin.Substring(0, $origin.LastIndexOf('/'))
|
||||
$cinnyUrl = "$base/cinny.git"
|
||||
Write-Host "Rewriting submodule URL to $cinnyUrl"
|
||||
git config submodule.cinny.url $cinnyUrl
|
||||
git submodule sync --recursive
|
||||
|
||||
git submodule update --init --recursive
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
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 config submodule.cinny.url $cinnyUrl
|
||||
git submodule sync --recursive
|
||||
git submodule update --init --recursive --remote
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
|
||||
Write-Host "Resolved submodule commit:"
|
||||
git -C cinny rev-parse HEAD
|
||||
|
||||
- name: Pull latest (after version bump)
|
||||
if: github.ref == 'refs/heads/main'
|
||||
@@ -83,11 +252,11 @@ jobs:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install dependencies (root)
|
||||
run: npm install --prefer-offline
|
||||
run: npm ci --prefer-offline
|
||||
|
||||
- name: Install dependencies (cinny)
|
||||
working-directory: ./cinny
|
||||
run: npm install --prefer-offline
|
||||
run: npm ci --prefer-offline
|
||||
|
||||
- name: Clean previous builds
|
||||
shell: powershell
|
||||
@@ -96,54 +265,141 @@ jobs:
|
||||
if (Test-Path dist-electron) { Remove-Item -Recurse -Force dist-electron }
|
||||
|
||||
- name: Build Electron app
|
||||
run: npm run build:win
|
||||
run: npm run build:local -- --win
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GHTOKEN }}
|
||||
|
||||
- name: Upload Squirrel Setup (x64)
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Paarrot-Setup-x64.exe
|
||||
path: dist-electron/squirrel-windows/Paarrot-*-win-x64.exe
|
||||
- name: Upload Windows assets to releases
|
||||
shell: powershell
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_TOKEN: ${{ secrets.GHTOKEN }}
|
||||
run: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
$TAG_NAME = "${{ needs.prepare-release.outputs.tag }}"
|
||||
$API_BASE = "${{ github.server_url }}/api/v1"
|
||||
$REPO = "${{ github.repository }}"
|
||||
$GITHUB_REPO = "Paarrot/Paarrot-Desktop"
|
||||
|
||||
function Invoke-JsonCurl([string[]]$CurlArgs) {
|
||||
$raw = & curl.exe @CurlArgs
|
||||
if ($LASTEXITCODE -ne 0) { throw "curl failed: $($CurlArgs -join ' ')" }
|
||||
return ($raw | ConvertFrom-Json)
|
||||
}
|
||||
|
||||
$giteaRelease = Invoke-JsonCurl @(
|
||||
"-fsS", "-H", "Authorization: token $env:GITEA_TOKEN",
|
||||
"$API_BASE/repos/$REPO/releases/tags/$TAG_NAME"
|
||||
)
|
||||
$ghRelease = Invoke-JsonCurl @(
|
||||
"-fsS", "-H", "Authorization: token $env:GH_TOKEN",
|
||||
"https://api.github.com/repos/$GITHUB_REPO/releases/tags/$TAG_NAME"
|
||||
)
|
||||
# GitHub returns upload_url like .../assets{?name,label}
|
||||
$ghUploadUrl = [string]$ghRelease.upload_url
|
||||
if ([string]::IsNullOrWhiteSpace($ghUploadUrl)) {
|
||||
throw "GitHub release $($ghRelease.id) has no upload_url"
|
||||
}
|
||||
$ghUploadUrl = $ghUploadUrl -replace '\{\?[^}]*\}', ''
|
||||
Write-Host "GitHub upload base: $ghUploadUrl"
|
||||
|
||||
function Upload-One([string]$File) {
|
||||
$filename = [System.IO.Path]::GetFileName($File)
|
||||
$sizeMb = [math]::Round((Get-Item $File).Length / 1MB, 1)
|
||||
Write-Host "Uploading $filename (${sizeMb} MB)..."
|
||||
|
||||
$giteaAssets = Invoke-JsonCurl @(
|
||||
"-fsS", "-H", "Authorization: token $env:GITEA_TOKEN",
|
||||
"$API_BASE/repos/$REPO/releases/$($giteaRelease.id)/assets"
|
||||
)
|
||||
foreach ($asset in @($giteaAssets)) {
|
||||
if ($asset.name -eq $filename) {
|
||||
& curl.exe -fsS -X DELETE -H "Authorization: token $env:GITEA_TOKEN" `
|
||||
"$API_BASE/repos/$REPO/releases/$($giteaRelease.id)/assets/$($asset.id)" | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
& curl.exe -fsS --max-time 0 --retry 5 --retry-delay 10 `
|
||||
-X POST `
|
||||
-H "Authorization: token $env:GITEA_TOKEN" `
|
||||
-H "Content-Type: application/octet-stream" `
|
||||
--data-binary "@$File" `
|
||||
"$API_BASE/repos/$REPO/releases/$($giteaRelease.id)/assets?name=$filename" | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { throw "Gitea upload failed for $filename" }
|
||||
Write-Host " Gitea: ok"
|
||||
|
||||
$ghAssets = Invoke-JsonCurl @(
|
||||
"-fsS", "-H", "Authorization: token $env:GH_TOKEN",
|
||||
"https://api.github.com/repos/$GITHUB_REPO/releases/$($ghRelease.id)/assets"
|
||||
)
|
||||
foreach ($asset in @($ghAssets)) {
|
||||
if ($asset.name -eq $filename) {
|
||||
& curl.exe -fsS -X DELETE -H "Authorization: token $env:GH_TOKEN" `
|
||||
"https://api.github.com/repos/$GITHUB_REPO/releases/assets/$($asset.id)" | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
# Brace the variable — `$url?name=` is parsed as PowerShell's null-conditional.
|
||||
$ghAssetUrl = "${ghUploadUrl}?name=$([uri]::EscapeDataString($filename))"
|
||||
Write-Host " GitHub URL: $ghAssetUrl"
|
||||
& curl.exe -fsS --max-time 0 --retry 5 --retry-delay 10 `
|
||||
-X POST `
|
||||
-H "Authorization: token $env:GH_TOKEN" `
|
||||
-H "Content-Type: application/octet-stream" `
|
||||
--data-binary "@$File" `
|
||||
$ghAssetUrl | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { throw "GitHub upload failed for $filename" }
|
||||
Write-Host " GitHub: ok"
|
||||
}
|
||||
|
||||
Get-ChildItem -Path dist-electron -File |
|
||||
Where-Object { $_.Name -like 'Paarrot-*-win-x64.exe' -or $_.Name -eq 'latest.yml' } |
|
||||
ForEach-Object { Upload-One $_.FullName }
|
||||
|
||||
- name: List build output
|
||||
shell: powershell
|
||||
run: |
|
||||
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:
|
||||
runs-on: ubuntu-latest
|
||||
needs: increment-version
|
||||
if: always() && (needs.increment-version.outputs.should_build == 'true' || needs.increment-version.result == 'skipped')
|
||||
needs: [increment-version, prepare-release]
|
||||
if: always() && needs.prepare-release.result == 'success'
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
submodules: false
|
||||
ref: ${{ github.ref_name }}
|
||||
|
||||
- name: Checkout submodules manually
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Use the same host/scheme the runner already used for origin (not public HTTPS).
|
||||
ORIGIN_URL=$(git remote get-url origin)
|
||||
ORIGIN_URL=${ORIGIN_URL%/}
|
||||
ORIGIN_URL=${ORIGIN_URL%.git}
|
||||
BASE_URL=${ORIGIN_URL%/*}
|
||||
CINNY_URL="${BASE_URL}/cinny.git"
|
||||
echo "Rewriting submodule URL to ${CINNY_URL}"
|
||||
git config submodule.cinny.url "${CINNY_URL}"
|
||||
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 || true
|
||||
rm -rf cinny .git/modules/cinny
|
||||
git config submodule.cinny.url "${CINNY_URL}"
|
||||
git submodule sync --recursive
|
||||
git submodule update --init --recursive --remote
|
||||
|
||||
echo "Resolved submodule commit:"
|
||||
git -C cinny rev-parse HEAD
|
||||
|
||||
- name: Pull latest (after version bump)
|
||||
if: github.ref == 'refs/heads/main'
|
||||
@@ -166,162 +422,130 @@ jobs:
|
||||
rm -rf cinny/dist dist-electron
|
||||
|
||||
- name: Build Electron app
|
||||
run: npm run build:linux
|
||||
run: npm run build:local -- --linux
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GHTOKEN }}
|
||||
|
||||
- name: Upload AppImage
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Paarrot-Linux-x64.AppImage
|
||||
path: dist-electron/Paarrot-*.AppImage
|
||||
- name: Upload Linux assets to releases
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_TOKEN: ${{ secrets.GHTOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
VERSION="${{ needs.prepare-release.outputs.version }}"
|
||||
TAG_NAME="${{ needs.prepare-release.outputs.tag }}"
|
||||
API_BASE="${{ github.server_url }}/api/v1"
|
||||
REPO="${{ github.repository }}"
|
||||
GITHUB_REPO="Paarrot/Paarrot-Desktop"
|
||||
|
||||
create-release:
|
||||
GITEA_RELEASE_ID=$(curl -fsS -H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${API_BASE}/repos/${REPO}/releases/tags/${TAG_NAME}" | jq -r '.id')
|
||||
GH_RELEASE_ID=$(curl -fsS -H "Authorization: token ${GH_TOKEN}" \
|
||||
"https://api.github.com/repos/${GITHUB_REPO}/releases/tags/${TAG_NAME}" | jq -r '.id')
|
||||
GH_UPLOAD_URL=$(curl -fsS -H "Authorization: token ${GH_TOKEN}" \
|
||||
"https://api.github.com/repos/${GITHUB_REPO}/releases/${GH_RELEASE_ID}" | jq -r '.upload_url' | sed 's/{?name,label}//')
|
||||
|
||||
upload_one() {
|
||||
local FILE="$1"
|
||||
local FILENAME
|
||||
FILENAME=$(basename "$FILE")
|
||||
echo "Uploading ${FILENAME} ($(du -h "$FILE" | cut -f1))..."
|
||||
|
||||
OLD_ID=$(curl -fsS -H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${API_BASE}/repos/${REPO}/releases/${GITEA_RELEASE_ID}/assets" \
|
||||
| jq -r --arg n "$FILENAME" '.[] | select(.name == $n) | .id' || true)
|
||||
if [ -n "${OLD_ID:-}" ]; then
|
||||
curl -fsS -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${API_BASE}/repos/${REPO}/releases/${GITEA_RELEASE_ID}/assets/${OLD_ID}" >/dev/null
|
||||
fi
|
||||
|
||||
curl -fsS --max-time 0 --retry 5 --retry-delay 10 \
|
||||
-X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
--data-binary @"${FILE}" \
|
||||
"${API_BASE}/repos/${REPO}/releases/${GITEA_RELEASE_ID}/assets?name=${FILENAME}" \
|
||||
>/dev/null
|
||||
echo " Gitea: ok"
|
||||
|
||||
OLD_GH=$(curl -fsS -H "Authorization: token ${GH_TOKEN}" \
|
||||
"https://api.github.com/repos/${GITHUB_REPO}/releases/${GH_RELEASE_ID}/assets" \
|
||||
| jq -r --arg n "$FILENAME" '.[] | select(.name == $n) | .id' || true)
|
||||
if [ -n "${OLD_GH:-}" ]; then
|
||||
curl -fsS -X DELETE -H "Authorization: token ${GH_TOKEN}" \
|
||||
"https://api.github.com/repos/${GITHUB_REPO}/releases/assets/${OLD_GH}" >/dev/null
|
||||
fi
|
||||
|
||||
curl -fsS --max-time 0 --retry 5 --retry-delay 10 \
|
||||
-X POST \
|
||||
-H "Authorization: token ${GH_TOKEN}" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
--data-binary @"${FILE}" \
|
||||
"${GH_UPLOAD_URL}?name=${FILENAME}" \
|
||||
>/dev/null
|
||||
echo " GitHub: ok"
|
||||
}
|
||||
|
||||
shopt -s nullglob
|
||||
for FILE in dist-electron/Paarrot-*.AppImage dist-electron/latest-linux.yml; do
|
||||
upload_one "$FILE"
|
||||
done
|
||||
|
||||
finalize-release:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [increment-version, build-windows, build-linux]
|
||||
if: always() && (needs.build-windows.result == 'success' || needs.build-linux.result == 'success')
|
||||
needs: [increment-version, prepare-release, build-windows, build-linux]
|
||||
if: always() && needs.prepare-release.result == 'success' && (needs.build-windows.result == 'success' || needs.build-linux.result == 'success')
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.ref_name }}
|
||||
|
||||
- name: Pull latest (after version bump)
|
||||
if: github.ref == 'refs/heads/main'
|
||||
run: git pull origin main
|
||||
|
||||
- name: Get version
|
||||
id: version
|
||||
run: |
|
||||
# Use version from increment-version job if available, otherwise read from file
|
||||
if [ -n "${{ needs.increment-version.outputs.version }}" ]; then
|
||||
VERSION="${{ needs.increment-version.outputs.version }}"
|
||||
else
|
||||
VERSION=$(grep -Po '"version":\s*"\K[^"]+' package.json | head -1)
|
||||
fi
|
||||
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Version: $VERSION"
|
||||
|
||||
- name: Check if tag exists
|
||||
id: tag_check
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.VERSION }}"
|
||||
if git rev-parse "v$VERSION" >/dev/null 2>&1; then
|
||||
echo "Tag v$VERSION already exists"
|
||||
echo "exists=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "Tag v$VERSION does not exist"
|
||||
echo "exists=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Create and push tag
|
||||
if: steps.tag_check.outputs.exists == 'false'
|
||||
run: |
|
||||
git config user.name "GitHub Actions"
|
||||
git config user.email "actions@github.com"
|
||||
git tag -a "v${{ steps.version.outputs.VERSION }}" -m "Release v${{ steps.version.outputs.VERSION }}"
|
||||
git push origin "v${{ steps.version.outputs.VERSION }}"
|
||||
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
- name: Prepare release files
|
||||
run: |
|
||||
mkdir -p release-files
|
||||
find artifacts -type f \( -name "*.exe" -o -name "*.AppImage" -o -name "*.nupkg" -o -name "RELEASES*" \) -exec cp {} release-files/ \;
|
||||
ls -la release-files/
|
||||
|
||||
- name: Create or Update Release
|
||||
- name: Cleanup old Gitea releases
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.VERSION }}"
|
||||
TAG_NAME="v${VERSION}"
|
||||
API_BASE="http://synbox.ruv.wtf:8418/api/v1"
|
||||
REPO="litruv/cinny-desktop"
|
||||
|
||||
# Check if release exists
|
||||
RELEASE_ID=$(curl -s -H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${API_BASE}/repos/${REPO}/releases/tags/${TAG_NAME}" | jq -r '.id // empty')
|
||||
|
||||
if [ -z "$RELEASE_ID" ]; then
|
||||
echo "Creating new release for ${TAG_NAME}..."
|
||||
RELEASE_ID=$(curl -s -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"tag_name\": \"${TAG_NAME}\", \"name\": \"Release ${TAG_NAME}\", \"body\": \"Release ${VERSION}\", \"draft\": false, \"prerelease\": false}" \
|
||||
"${API_BASE}/repos/${REPO}/releases" | jq -r '.id')
|
||||
echo "Created release with ID: ${RELEASE_ID}"
|
||||
else
|
||||
echo "Release exists with ID: ${RELEASE_ID}"
|
||||
# Delete existing assets
|
||||
echo "Deleting existing assets..."
|
||||
ASSETS=$(curl -s -H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${API_BASE}/repos/${REPO}/releases/${RELEASE_ID}/assets" | jq -r '.[].id')
|
||||
for ASSET_ID in $ASSETS; do
|
||||
echo "Deleting asset ${ASSET_ID}..."
|
||||
curl -s -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${API_BASE}/repos/${REPO}/releases/${RELEASE_ID}/assets/${ASSET_ID}"
|
||||
done
|
||||
fi
|
||||
|
||||
# Upload new assets
|
||||
echo "Uploading assets..."
|
||||
for FILE in release-files/*; do
|
||||
FILENAME=$(basename "$FILE")
|
||||
echo "Uploading ${FILENAME}..."
|
||||
curl -s -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
--data-binary "@${FILE}" \
|
||||
"${API_BASE}/repos/${REPO}/releases/${RELEASE_ID}/assets?name=${FILENAME}"
|
||||
echo ""
|
||||
set -euo pipefail
|
||||
TAG_NAME="${{ needs.prepare-release.outputs.tag }}"
|
||||
API_BASE="${{ github.server_url }}/api/v1"
|
||||
REPO="${{ github.repository }}"
|
||||
|
||||
echo "Cleaning up old Gitea releases (keeping ${TAG_NAME})..."
|
||||
OLD_RELEASES=$(curl -fsS -H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${API_BASE}/repos/${REPO}/releases" | jq -r --arg t "$TAG_NAME" '.[] | select(.tag_name != $t) | .id')
|
||||
|
||||
for OLD_ID in $OLD_RELEASES; do
|
||||
[ -n "$OLD_ID" ] || continue
|
||||
OLD_TAG=$(curl -fsS -H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${API_BASE}/repos/${REPO}/releases/${OLD_ID}" | jq -r '.tag_name')
|
||||
echo "Deleting Gitea release ${OLD_ID} (${OLD_TAG})..."
|
||||
curl -fsS -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${API_BASE}/repos/${REPO}/releases/${OLD_ID}" >/dev/null || true
|
||||
if [ -n "$OLD_TAG" ] && [ "$OLD_TAG" != "null" ]; then
|
||||
curl -fsS -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${API_BASE}/repos/${REPO}/tags/${OLD_TAG}" >/dev/null || true
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Release complete!"
|
||||
|
||||
- name: Create GitHub Release
|
||||
- name: Cleanup old GitHub releases
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GHTOKEN }}
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.VERSION }}"
|
||||
TAG_NAME="v${VERSION}"
|
||||
set -euo pipefail
|
||||
TAG_NAME="${{ needs.prepare-release.outputs.tag }}"
|
||||
GITHUB_REPO="Paarrot/Paarrot-Desktop"
|
||||
|
||||
# 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 ""
|
||||
|
||||
echo "Cleaning up old GitHub releases (keeping ${TAG_NAME})..."
|
||||
OLD_RELEASES=$(curl -fsS -H "Authorization: token ${GH_TOKEN}" \
|
||||
"https://api.github.com/repos/${GITHUB_REPO}/releases" | jq -r --arg t "$TAG_NAME" '.[] | select(.tag_name != $t) | .id')
|
||||
|
||||
for OLD_ID in $OLD_RELEASES; do
|
||||
[ -n "$OLD_ID" ] || continue
|
||||
OLD_TAG=$(curl -fsS -H "Authorization: token ${GH_TOKEN}" \
|
||||
"https://api.github.com/repos/${GITHUB_REPO}/releases/${OLD_ID}" | jq -r '.tag_name')
|
||||
echo "Deleting GitHub release ${OLD_ID} (${OLD_TAG})..."
|
||||
curl -fsS -X DELETE -H "Authorization: token ${GH_TOKEN}" \
|
||||
"https://api.github.com/repos/${GITHUB_REPO}/releases/${OLD_ID}" >/dev/null || true
|
||||
if [ -n "$OLD_TAG" ] && [ "$OLD_TAG" != "null" ]; then
|
||||
curl -fsS -X DELETE -H "Authorization: token ${GH_TOKEN}" \
|
||||
"https://api.github.com/repos/${GITHUB_REPO}/git/refs/tags/${OLD_TAG}" >/dev/null || true
|
||||
fi
|
||||
done
|
||||
|
||||
echo "GitHub release complete!"
|
||||
|
||||
echo "Release finalize complete for ${TAG_NAME}"
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,3 +4,4 @@ node_modules
|
||||
devAssets
|
||||
|
||||
.DS_Store
|
||||
dist-electron/
|
||||
|
||||
2
.gitmodules
vendored
2
.gitmodules
vendored
@@ -1,3 +1,3 @@
|
||||
[submodule "cinny"]
|
||||
path = cinny
|
||||
url = http://synbox.ruv.wtf:8418/litruv/cinny.git
|
||||
url = ../cinny.git
|
||||
|
||||
@@ -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
|
||||
156
PLUGINS.md
156
PLUGINS.md
@@ -1,156 +0,0 @@
|
||||
# Paarrot Plugin System
|
||||
|
||||
The Paarrot desktop client includes a powerful plugin system that allows you to extend and customize your Matrix experience.
|
||||
|
||||
## What are Plugins?
|
||||
|
||||
Plugins are small JavaScript modules that run inside Paarrot and can:
|
||||
- Add custom commands
|
||||
- Modify the UI
|
||||
- React to Matrix events
|
||||
- Add new settings sections
|
||||
- Enhance functionality
|
||||
|
||||
## Installing Plugins
|
||||
|
||||
### From the Plugin Marketplace
|
||||
|
||||
1. Open **Settings** → **Plugins**
|
||||
2. Browse the **Marketplace** tab
|
||||
3. Click **Install** on any plugin you want to add
|
||||
4. The plugin will be downloaded and enabled automatically
|
||||
|
||||
### Manual Installation
|
||||
|
||||
1. Download a plugin ZIP file
|
||||
2. Extract it to your plugins directory:
|
||||
- **Windows**: `%APPDATA%\paarrot\plugins\`
|
||||
- **Linux**: `~/.config/Paarrot/plugins/`
|
||||
- **macOS**: `~/Library/Application Support/Paarrot/plugins/`
|
||||
3. Restart Paarrot
|
||||
4. Enable the plugin in **Settings** → **Plugins** → **Installed**
|
||||
|
||||
## Managing Plugins
|
||||
|
||||
### Viewing Installed Plugins
|
||||
|
||||
Go to **Settings** → **Plugins** → **Installed** to see all installed plugins.
|
||||
|
||||
### Enabling/Disabling Plugins
|
||||
|
||||
Click the toggle switch next to any plugin to enable or disable it. Disabled plugins won't run or use resources.
|
||||
|
||||
### Uninstalling Plugins
|
||||
|
||||
Click the **Uninstall** button next to any plugin to remove it completely.
|
||||
|
||||
### Searching Plugins
|
||||
|
||||
Use the search bar to filter plugins by name, description, author, or tags.
|
||||
|
||||
## Plugin Security
|
||||
|
||||
⚠️ **Important Security Information**
|
||||
|
||||
Plugins run with access to your Matrix client and can:
|
||||
- Read your messages
|
||||
- Send messages on your behalf
|
||||
- Access your room list
|
||||
- Modify app behavior
|
||||
|
||||
**Only install plugins from trusted sources!**
|
||||
|
||||
### Safety Tips
|
||||
|
||||
1. **Check the author**: Install plugins from known developers
|
||||
2. **Read reviews**: Look for community feedback
|
||||
3. **Review permissions**: Understand what the plugin can do
|
||||
4. **Keep updated**: Update plugins regularly for security fixes
|
||||
5. **Report issues**: If a plugin behaves suspiciously, report it immediately
|
||||
|
||||
## Developing Plugins
|
||||
|
||||
Want to create your own plugin? Check out the [Plugin Development Guide](./cinny/src/app/features/settings/plugins/PLUGIN_DEVELOPMENT.md).
|
||||
|
||||
### Quick Start
|
||||
|
||||
1. Create a plugin directory with:
|
||||
- `index.js` - Your plugin code
|
||||
- `plugin-metadata.json` - Plugin information
|
||||
|
||||
2. Implement the plugin interface:
|
||||
```javascript
|
||||
module.exports = {
|
||||
onLoad: async (context) => {
|
||||
// Your plugin initialization
|
||||
console.log('Plugin loaded!');
|
||||
},
|
||||
onUnload: async () => {
|
||||
// Cleanup when disabled
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
3. Test locally by copying to your plugins directory
|
||||
|
||||
4. Publish to the [Plugin Directory](https://github.com/Paarrot/Plugin-Directory)
|
||||
|
||||
## Plugin API
|
||||
|
||||
Plugins have access to:
|
||||
- **Matrix Client**: Full matrix-js-sdk client instance
|
||||
- **React**: For building UI components
|
||||
- **Commands**: Register custom slash commands
|
||||
- **UI Hooks**: Inject UI elements
|
||||
- **Settings**: Add custom settings sections
|
||||
- **Utilities**: Notifications and helpers
|
||||
|
||||
See the [Plugin Development Guide](./cinny/src/app/features/settings/plugins/PLUGIN_DEVELOPMENT.md) for complete API documentation.
|
||||
|
||||
## Example Plugins
|
||||
|
||||
Check out the `example-plugin/` directory for a simple example demonstrating:
|
||||
- Matrix client access
|
||||
- Event listeners
|
||||
- Custom commands
|
||||
- Notifications
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Plugin Won't Load
|
||||
|
||||
1. Check the browser console for errors (F12 → Console)
|
||||
2. Look for `[PluginLoader]` messages
|
||||
3. Verify `index.js` exists in the plugin directory
|
||||
4. Ensure `plugin-metadata.json` is valid JSON
|
||||
|
||||
### Plugin Crashes
|
||||
|
||||
1. Disable the plugin in Settings → Plugins
|
||||
2. Check console for error stack traces
|
||||
3. Report the issue to the plugin author
|
||||
4. Try reinstalling the plugin
|
||||
|
||||
### Performance Issues
|
||||
|
||||
If Paarrot becomes slow:
|
||||
1. Disable plugins one by one to identify the culprit
|
||||
2. Check for memory leaks in the console
|
||||
3. Contact the plugin author with details
|
||||
|
||||
## Plugin Directory
|
||||
|
||||
The official Paarrot Plugin Directory is maintained at:
|
||||
https://github.com/Paarrot/Plugin-Directory
|
||||
|
||||
Submit your plugins there to make them available in the marketplace!
|
||||
|
||||
## Support
|
||||
|
||||
- **Documentation**: [Plugin Development Guide](./cinny/src/app/features/settings/plugins/PLUGIN_DEVELOPMENT.md)
|
||||
- **Issues**: https://github.com/Paarrot/cinny-desktop/issues
|
||||
- **Community**: Join our Matrix room for plugin development help
|
||||
|
||||
## License
|
||||
|
||||
Plugins are independent works and have their own licenses. Check each plugin's metadata for license information.
|
||||
1188
PLUGIN_API.md
1188
PLUGIN_API.md
File diff suppressed because it is too large
Load Diff
@@ -1,199 +0,0 @@
|
||||
# Plugin System Wishlist - Implementation Summary
|
||||
|
||||
All requested features have been implemented! 🎉
|
||||
|
||||
## ✅ Implemented Features
|
||||
|
||||
### 1. ✅ Enhanced Slash Commands
|
||||
- **Simple commands**: `ctx.commands.register({ name: "shrug", run: () => "¯\\_(ツ)_/¯" })`
|
||||
- **Commands with args**: `ctx.commands.register({ name: "echo", args: ["text"], run: ({ text }) => text })`
|
||||
- Automatic argument parsing
|
||||
- Execution API: `ctx.commands.execute(name, args)`
|
||||
|
||||
### 2. ✅ Message Interceptors
|
||||
- **Before send**: `ctx.messages.onBeforeSend((msg) => { msg.content = "modified" })`
|
||||
- **On receive**: `ctx.messages.onReceive((msg) => { /* process */ })`
|
||||
- Full MessageContext with content, roomId, metadata
|
||||
- Async support for interceptors
|
||||
|
||||
### 3. ✅ Custom Renderers
|
||||
- **Register**: `ctx.ui.registerRenderer("message", (msg, defaultRenderer) => { /* render */ })`
|
||||
- **Unregister**: `ctx.ui.unregisterRenderer("message")`
|
||||
- Fallback to default renderer support
|
||||
- Discord-mod-tier customization enabled
|
||||
|
||||
### 4. ✅ Settings UI Per Plugin
|
||||
- **Define schema**: `ctx.settings.define({ theme: { type: "select", options: [...] } })`
|
||||
- **Supported types**: string, number, boolean, select, color
|
||||
- **Get/Set**: `ctx.settings.get(key)` / `ctx.settings.set(key, value)`
|
||||
- Auto-persisted to localStorage
|
||||
- No JSON editing needed!
|
||||
|
||||
### 5. ✅ Hot Reload
|
||||
- **API**: `pluginRegistry.reloadPlugin(pluginId, newPlugin, newContext)`
|
||||
- Auto-cleanup of old plugin resources
|
||||
- Maintains enabled/disabled state
|
||||
- Dev-friendly workflow
|
||||
|
||||
### 6. ✅ Dependency System
|
||||
- **Declare**: `dependencies: { "paarrot.core-utils": "^1.0.0" }`
|
||||
- **Require**: `const utils = ctx.require("paarrot.utils")`
|
||||
- Error handling for missing dependencies
|
||||
- Plugin exports system
|
||||
|
||||
### 7. ✅ Raw Matrix Events Hook
|
||||
- **Full SDK access**: `ctx.matrix.on("m.room.message", (ev) => { })`
|
||||
- **Any event type**: Room.timeline, sync, RoomState.events, etc.
|
||||
- Direct Matrix.js SDK integration
|
||||
- Power users will go nuts!
|
||||
|
||||
### 8. ✅ Background Tasks / Intervals
|
||||
- **Interval**: `ctx.timers.setInterval(() => { /* cursed shit */ }, 10000)`
|
||||
- **Timeout**: `ctx.timers.setTimeout(() => { }, 5000)`
|
||||
- **Clear**: `ctx.timers.clearInterval(id)` / `ctx.timers.clearTimeout(id)`
|
||||
- Auto-cleanup on plugin unload
|
||||
- Perfect for bots, reminders, auto-cleanup
|
||||
|
||||
### 9. ✅ Notifications API
|
||||
- **Simple**: `ctx.notify("Oi")`
|
||||
- **Advanced**: `ctx.notify({ title: "Oi", body: "New message from Steve", type: "info" })`
|
||||
- Action buttons support
|
||||
- Custom duration
|
||||
|
||||
### 10. ✅ Plugin Logs Panel
|
||||
- **Logging**: `ctx.log()`, `ctx.warn()`, `ctx.error()`
|
||||
- Per-plugin log storage
|
||||
- Visible in `pluginRegistry.getLogs(pluginId)`
|
||||
- Cleared on plugin unload
|
||||
- Max 1000 logs retained
|
||||
|
||||
## 🎯 Bonus Features
|
||||
|
||||
- **Plugin exports**: Share functionality between plugins
|
||||
- **Automatic cleanup**: Timers, handlers, resources auto-cleaned on unload
|
||||
- **Error isolation**: Plugin errors don't crash the app
|
||||
- **Type safety**: Full TypeScript definitions
|
||||
- **Sandboxed execution**: Plugins run in controlled environment
|
||||
- **Rich context**: Full Matrix client, React, all APIs
|
||||
|
||||
## 📁 Files Changed
|
||||
|
||||
### Core System
|
||||
- [`PluginAPI.ts`](cinny/src/app/features/settings/plugins/PluginAPI.ts) - Enhanced with all APIs
|
||||
- [`PluginLoader.tsx`](cinny/src/app/features/settings/plugins/PluginLoader.tsx) - Complete rewrite with context creation
|
||||
- [`index.ts`](cinny/src/app/features/settings/plugins/index.ts) - Exports updated
|
||||
|
||||
### Documentation
|
||||
- [`PLUGIN_DEVELOPMENT.md`](cinny/src/app/features/settings/plugins/PLUGIN_DEVELOPMENT.md) - Comprehensive guide
|
||||
- [`PLUGINS.md`](PLUGINS.md) - User guide
|
||||
- [`example-plugin/index.js`](example-plugin/index.js) - Full-featured example
|
||||
|
||||
### Integration
|
||||
- [`ClientRoot.tsx`](cinny/src/app/pages/client/ClientRoot.tsx) - PluginLoader integrated
|
||||
- [`main.js`](electron/main.js) - IPC handler for reading plugin code
|
||||
- [`preload.js`](electron/preload.js) - readPluginCode API exposed
|
||||
- [`ext.d.ts`](cinny/src/ext.d.ts) - TypeScript definitions
|
||||
|
||||
## 🚀 Usage Examples
|
||||
|
||||
### Command with Args
|
||||
```javascript
|
||||
ctx.commands.register({
|
||||
name: "echo",
|
||||
args: ["text"],
|
||||
run: ({ text }) => text
|
||||
});
|
||||
```
|
||||
|
||||
### Message Interceptor
|
||||
```javascript
|
||||
ctx.messages.onBeforeSend((msg) => {
|
||||
if (msg.content === "brb") {
|
||||
msg.content = "be right back ya legend";
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Custom Settings
|
||||
```javascript
|
||||
ctx.settings.define({
|
||||
theme: { type: "select", options: ["dark", "light"] },
|
||||
spamFilter: { type: "boolean", default: true }
|
||||
});
|
||||
```
|
||||
|
||||
### Background Tasks
|
||||
```javascript
|
||||
ctx.timers.setInterval(() => {
|
||||
// do cursed shit every 10s
|
||||
}, 10000);
|
||||
```
|
||||
|
||||
### Raw Matrix Events
|
||||
```javascript
|
||||
ctx.matrix.on("m.room.message", (ev) => {
|
||||
ctx.log('Message:', ev.getContent().body);
|
||||
});
|
||||
```
|
||||
|
||||
### Plugin Dependencies
|
||||
```javascript
|
||||
const utils = ctx.require("paarrot.utils");
|
||||
utils.doSomething();
|
||||
```
|
||||
|
||||
## 🎨 Plugin API Summary
|
||||
|
||||
```typescript
|
||||
interface PluginContext {
|
||||
pluginId: string;
|
||||
matrixClient: MatrixClient;
|
||||
React: typeof import('react');
|
||||
|
||||
commands: {
|
||||
register: (command: PluginCommand) => void;
|
||||
unregister: (name: string) => void;
|
||||
execute: (name: string, args: Record<string, any>) => Promise<string | void>;
|
||||
};
|
||||
|
||||
messages: {
|
||||
onBeforeSend: (interceptor: MessageInterceptor) => void;
|
||||
onReceive: (interceptor: MessageInterceptor) => void;
|
||||
};
|
||||
|
||||
ui: {
|
||||
registerRenderer: (type: string, renderer: CustomRenderer) => void;
|
||||
unregisterRenderer: (type: string) => void;
|
||||
};
|
||||
|
||||
settings: {
|
||||
define: (schema: SettingsSchema) => void;
|
||||
get: <T = any>(key: string) => T | undefined;
|
||||
set: (key: string, value: any) => void;
|
||||
};
|
||||
|
||||
matrix: {
|
||||
on: (eventType: string, handler: (event: MatrixEvent) => void) => void;
|
||||
off: (eventType: string, handler: (event: MatrixEvent) => void) => void;
|
||||
};
|
||||
|
||||
timers: {
|
||||
setInterval: (callback: () => void, ms: number) => number;
|
||||
setTimeout: (callback: () => void, ms: number) => number;
|
||||
clearInterval: (id: number) => void;
|
||||
clearTimeout: (id: number) => void;
|
||||
};
|
||||
|
||||
notify: (options: NotificationOptions | string) => void;
|
||||
log: (...args: any[]) => void;
|
||||
warn: (...args: any[]) => void;
|
||||
error: (...args: any[]) => void;
|
||||
require: (pluginId: string) => any;
|
||||
}
|
||||
```
|
||||
|
||||
## 🔥 The Result
|
||||
|
||||
A **production-ready plugin system** that rivals Discord, VSCode, and other extensible apps. Plugin developers will have everything they need to build powerful extensions without fighting the framework.
|
||||
|
||||
**No compromises. All features implemented. Ready to ship.**
|
||||
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.
|
||||
|
||||
@@ -22,10 +22,57 @@ To setup development locally run the following commands:
|
||||
* `cd cinny-desktop/cinny`
|
||||
* `npm ci`
|
||||
* `cd ..`
|
||||
* `npm ci`
|
||||
* `npm ci`
|
||||
|
||||
To build the app locally, run:
|
||||
* `npm run build`
|
||||
|
||||
To start local dev server, run:
|
||||
* `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/)
|
||||
|
||||
2
cinny
2
cinny
Submodule cinny updated: 52e9cc360f...99a294791e
19
config.json
19
config.json
@@ -9,30 +9,13 @@
|
||||
"xmr.se"
|
||||
],
|
||||
"allowCustomHomeservers": true,
|
||||
|
||||
"calling": {
|
||||
"livekitServiceUrl": "https://b.ruv.wtf/matrix-rtc/livekit/jwt"
|
||||
},
|
||||
|
||||
"featuredCommunities": {
|
||||
"openAsDefault": false,
|
||||
"spaces": [
|
||||
"#cinny-space:matrix.org",
|
||||
"#community:matrix.org",
|
||||
"#space:envs.net",
|
||||
"#science-space:matrix.org",
|
||||
"#libregaming-games:tchncs.de",
|
||||
"#mathematics-on:matrix.org"
|
||||
],
|
||||
"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": {
|
||||
|
||||
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": [
|
||||
"electron/**/*",
|
||||
"cinny/dist/**/*",
|
||||
"dev-app-update.yml",
|
||||
"!**/*.map",
|
||||
"!**/node_modules/*/{CHANGELOG.md,README.md,README,readme.md,readme}",
|
||||
"!**/node_modules/*/{test,__tests__,tests,powered-test,example,examples}",
|
||||
@@ -35,6 +36,12 @@
|
||||
"filter": ["**/*"]
|
||||
}
|
||||
],
|
||||
"protocols": [
|
||||
{
|
||||
"name": "Paarrot URL",
|
||||
"schemes": ["paarrot"]
|
||||
}
|
||||
],
|
||||
"linux": {
|
||||
"target": [
|
||||
{
|
||||
@@ -50,6 +57,7 @@
|
||||
"Name": "Paarrot",
|
||||
"GenericName": "Matrix Client",
|
||||
"Comment": "A Matrix client built with Cinny",
|
||||
"MimeType": "x-scheme-handler/paarrot;",
|
||||
"Categories": "Network;InstantMessaging;",
|
||||
"Keywords": "matrix;chat;messaging;",
|
||||
"StartupWMClass": "paarrot"
|
||||
@@ -77,30 +85,29 @@
|
||||
"win": {
|
||||
"target": [
|
||||
{
|
||||
"target": "squirrel",
|
||||
"target": "nsis",
|
||||
"arch": ["x64"]
|
||||
}
|
||||
],
|
||||
"icon": "icons/icon.ico",
|
||||
"artifactName": "${productName}-${version}-${os}-${arch}.${ext}"
|
||||
},
|
||||
"squirrelWindows": {
|
||||
"name": "Paarrot",
|
||||
"loadingGif": "icons/icon.png",
|
||||
"iconUrl": "http://synbox.ruv.wtf:8418/litruv/cinny-desktop/raw/branch/main/icons/icon.ico",
|
||||
"remoteReleases": false
|
||||
"nsis": {
|
||||
"oneClick": true,
|
||||
"perMachine": false,
|
||||
"allowToChangeInstallationDirectory": false,
|
||||
"deleteAppDataOnUninstall": false,
|
||||
"createDesktopShortcut": true,
|
||||
"createStartMenuShortcut": true,
|
||||
"shortcutName": "Paarrot",
|
||||
"differentialPackage": false
|
||||
},
|
||||
"compression": "normal",
|
||||
"publish": [
|
||||
{
|
||||
"provider": "generic",
|
||||
"url": "http://synbox.ruv.wtf:8418/litruv/cinny-desktop/releases/download/v${version}",
|
||||
"channel": "latest"
|
||||
},
|
||||
{
|
||||
"provider": "github",
|
||||
"owner": "Paarrot",
|
||||
"repo": "Paarrot-Desktop",
|
||||
"token": "${env.GH_TOKEN}",
|
||||
"releaseType": "release"
|
||||
}
|
||||
]
|
||||
|
||||
1208
electron/main.js
1208
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',
|
||||
'download_update': 'download-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
|
||||
@@ -129,6 +130,7 @@ contextBridge.exposeInMainWorld('electron', {
|
||||
updater: {
|
||||
checkForUpdates: () => ipcRenderer.invoke('check-for-updates'),
|
||||
downloadAndInstallUpdate: () => ipcRenderer.invoke('download-and-install-update'),
|
||||
isMock: () => ipcRenderer.invoke('updater-is-mock'),
|
||||
onUpdateAvailable: (callback) => {
|
||||
ipcRenderer.on('update-available', (_, info) => callback(info));
|
||||
},
|
||||
@@ -138,6 +140,9 @@ contextBridge.exposeInMainWorld('electron', {
|
||||
onUpdateDownloaded: (callback) => {
|
||||
ipcRenderer.on('update-downloaded', (_, info) => callback(info));
|
||||
},
|
||||
onUpdateNotAvailable: (callback) => {
|
||||
ipcRenderer.on('update-not-available', (_, info) => callback(info));
|
||||
},
|
||||
downloadUpdate: () => ipcRenderer.invoke('download-update'),
|
||||
installUpdate: () => ipcRenderer.invoke('install-update'),
|
||||
},
|
||||
@@ -176,6 +181,12 @@ contextBridge.exposeInMainWorld('electron', {
|
||||
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')
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
3548
package-lock.json
generated
3548
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
38
package.json
38
package.json
@@ -1,17 +1,22 @@
|
||||
{
|
||||
"name": "paarrot",
|
||||
"version": "4.11.74",
|
||||
"version": "4.11.168",
|
||||
"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",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"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 .",
|
||||
"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:win": "npm run build -- --win",
|
||||
"postman:generate": "node scripts/generate-postman-collection.js"
|
||||
@@ -23,25 +28,28 @@
|
||||
},
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
"adm-zip": "^0.5.16",
|
||||
"body-parser": "2.2.2",
|
||||
"adm-zip": "^0.5.18",
|
||||
"body-parser": "2.3.0",
|
||||
"cors": "2.8.6",
|
||||
"electron-squirrel-startup": "^1.0.1",
|
||||
"electron-store": "^8.2.0",
|
||||
"electron-updater": "^6.3.9",
|
||||
"electron-store": "^11.0.2",
|
||||
"electron-updater": "^6.8.9",
|
||||
"express": "5.2.1",
|
||||
"open": "11.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@actions/github": "6.0.0",
|
||||
"@electron/rebuild": "4.0.3",
|
||||
"concurrently": "9.2.1",
|
||||
"@actions/github": "9.1.1",
|
||||
"@electron/rebuild": "4.2.0",
|
||||
"concurrently": "10.0.3",
|
||||
"cross-env": "10.1.0",
|
||||
"electron": "40.6.0",
|
||||
"electron-builder": "26.8.1",
|
||||
"electron": "43.1.0",
|
||||
"electron-builder": "26.15.3",
|
||||
"node-fetch": "3.3.2",
|
||||
"png2icons": "2.0.1",
|
||||
"sharp": "0.34.5",
|
||||
"wait-on": "9.0.4"
|
||||
"sharp": "0.35.3",
|
||||
"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