Compare commits

...

10 Commits

Author SHA1 Message Date
bigjakk 2cf5623186 chore: bump version to 0.7.9
Build and Release / build-and-release (push) Failing after 7m37s
2026-04-16 10:21:26 -07:00
bigjakk 5e7786d66e fix: resolve transitive deps in build-asar, use Move-Item in swap script 2026-04-16 10:21:14 -07:00
bigjakk 9a52d6b8fc chore: bump version to 0.7.8
Build and Release / build-and-release (push) Successful in 12m59s
2026-04-16 09:38:00 -07:00
bigjakk 255befd1b8 fix: use original-fs to bypass Electron asar interception 2026-04-16 09:38:00 -07:00
bigjakk 691b363f88 ci: remove sudo from workflow (Gitea runner is root)
Build and Release / build-and-release (push) Successful in 9m20s
2026-04-16 08:56:19 -07:00
bigjakk 8972c3e363 chore: bump version to 0.7.7
Build and Release / build-and-release (push) Has been cancelled
2026-04-16 08:31:27 -07:00
bigjakk 5a8d77c494 ci: auto-detect patch vs full, control asar upload accordingly
Build and Release / build-and-release (push) Successful in 8s
2026-04-16 08:26:26 -07:00
bigjakk 2240ca38bd ci: auto-detect patch vs full release in workflow
Build and Release / build-and-release (push) Successful in 37s
2026-04-16 08:10:14 -07:00
bigjakk d2696a510f feat: add 2-stage update system (asar swap + full installer) 2026-04-16 08:10:06 -07:00
bigjakk 4aecb402d6 perf: freeze background tabs via CDP Page Lifecycle API
Build and Release / build-and-release (push) Failing after 32m2s
2026-04-16 06:52:44 -07:00
8 changed files with 1099 additions and 360 deletions
+173 -28
View File
@@ -4,6 +4,17 @@ on:
push:
branches:
- main
workflow_dispatch:
inputs:
release_type:
description: 'Override release type (auto-detected by default)'
required: false
default: 'auto'
type: choice
options:
- auto
- patch
- full
permissions:
contents: write
@@ -14,18 +25,24 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check if version already released
id: version-check
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITEA_TOKEN: ${{ secrets.DEST_GITEA_TOKEN }}
run: |
VERSION=$(grep '"version"' package.json | head -1 | sed 's/.*"version": *"\([^"]*\)".*/\1/')
TAG="v$VERSION"
echo "VERSION=$VERSION" >> "$GITHUB_OUTPUT"
echo "TAG=$TAG" >> "$GITHUB_OUTPUT"
if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
"https://gitea.crjlab.net/api/v1/repos/bigjakk/Krunker-Civilian-Client-Test/releases/tags/$TAG" \
-H "Authorization: token $GITEA_TOKEN")
if [ "$STATUS" = "200" ]; then
echo "Release $TAG already exists, skipping build"
echo "SKIP=true" >> "$GITHUB_OUTPUT"
else
@@ -33,6 +50,55 @@ jobs:
echo "SKIP=false" >> "$GITHUB_OUTPUT"
fi
- name: Detect release type
if: steps.version-check.outputs.SKIP == 'false'
id: release-type
run: |
OVERRIDE="${{ github.event.inputs.release_type || 'auto' }}"
if [ "$OVERRIDE" != "auto" ]; then
echo "Release type overridden to: $OVERRIDE"
echo "TYPE=$OVERRIDE" >> "$GITHUB_OUTPUT"
exit 0
fi
# Auto-detect: find what changed since the last release tag
PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || true)
if [ -z "$PREV_TAG" ]; then
echo "No previous tag found — defaulting to full build"
echo "TYPE=full" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "Comparing changes since $PREV_TAG"
CHANGED=$(git diff --name-only "$PREV_TAG"..HEAD)
echo "$CHANGED"
# Files that require a full (installer-only) release
FULL_TRIGGERS="electron-builder.yml|scripts/download-electron.js"
if echo "$CHANGED" | grep -qE "^($FULL_TRIGGERS)$"; then
echo "Full build trigger detected in changed files"
echo "TYPE=full" >> "$GITHUB_OUTPUT"
exit 0
fi
# Check if package.json deps changed (not just version field)
if echo "$CHANGED" | grep -q "^package.json$"; then
DEP_DIFF=$(git diff "$PREV_TAG"..HEAD -- package.json \
| grep -E '^\+' | grep -vE '^\+\+\+' \
| grep -E '"(dependencies|devDependencies|electron|electron-nightly)"' || true)
if [ -n "$DEP_DIFF" ]; then
echo "Dependency changes detected in package.json"
echo "TYPE=full" >> "$GITHUB_OUTPUT"
exit 0
fi
fi
echo "Only source changes detected — patch release"
echo "TYPE=patch" >> "$GITHUB_OUTPUT"
- name: Setup Node.js
if: steps.version-check.outputs.SKIP == 'false'
uses: actions/setup-node@v4
@@ -42,9 +108,9 @@ jobs:
- name: Install system dependencies
if: steps.version-check.outputs.SKIP == 'false'
run: |
sudo dpkg --add-architecture i386
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends \
dpkg --add-architecture i386
apt-get update -qq
apt-get install -y --no-install-recommends \
wine wine32 wine64 \
libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2t64 \
libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 \
@@ -70,46 +136,125 @@ jobs:
if: steps.version-check.outputs.SKIP == 'false'
run: npx electron-builder --linux -c.electronDist=node_modules/electron/dist-linux --publish never
- name: Build asar for patch updates
if: steps.version-check.outputs.SKIP == 'false' && steps.release-type.outputs.TYPE == 'patch'
run: npm run build:asar
- name: Report build sizes
if: steps.version-check.outputs.SKIP == 'false'
run: |
echo "=== Build output sizes ==="
echo "=== Release type: ${{ steps.release-type.outputs.TYPE }} ==="
ls -lh out/*.exe out/*.AppImage out/*.deb 2>/dev/null || true
ls -lh out/asar/* 2>/dev/null || true
- name: Generate release notes
if: steps.version-check.outputs.SKIP == 'false'
env:
GITEA_TOKEN: ${{ secrets.DEST_GITEA_TOKEN }}
run: |
git fetch --unshallow 2>/dev/null || true
chmod +x scripts/generate-release-notes.sh
scripts/generate-release-notes.sh "${{ steps.version-check.outputs.TAG }}" > /tmp/release-notes.md
PREV_SHA=$(curl -s "https://gitea.crjlab.net/api/v1/repos/bigjakk/Krunker-Civilian-Client-Test/releases?limit=1" \
-H "Authorization: token $GITEA_TOKEN" \
| jq -r '.[0].target_commitish // empty' 2>/dev/null || true)
echo "Previous release SHA: ${PREV_SHA:-none}"
scripts/generate-release-notes.sh "${{ steps.version-check.outputs.TAG }}" "$PREV_SHA" > /tmp/release-notes.md
echo "--- Generated release notes ---"
cat /tmp/release-notes.md
- name: Create GitHub release and upload assets
- name: Create release and upload assets
if: steps.version-check.outputs.SKIP == 'false'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITEA_TOKEN: ${{ secrets.DEST_GITEA_TOKEN }}
TAG: ${{ steps.version-check.outputs.TAG }}
RELEASE_TYPE: ${{ steps.release-type.outputs.TYPE }}
run: |
# Collect built artifacts
ASSETS=()
for file in out/*.exe out/*.AppImage out/*.deb; do
[ -f "$file" ] || continue
ASSETS+=("$file")
done
GITEA_BASE="https://gitea.crjlab.net"
REPO="bigjakk/Krunker-Civilian-Client-Test"
if [ ${#ASSETS[@]} -eq 0 ]; then
echo "ERROR: No build artifacts found in out/"
ls -la out/ 2>/dev/null || echo "out/ directory does not exist"
# Create tag
curl -s -X POST "$GITEA_BASE/api/v1/repos/$REPO/tags" \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"tag_name\": \"$TAG\", \"message\": \"$TAG\", \"target\": \"$GITHUB_SHA\"}"
# Read release notes
NOTES=$(cat /tmp/release-notes.md)
BODY=$(echo "$NOTES" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read()))')
# Create release
RESPONSE=$(curl -s -X POST "$GITEA_BASE/api/v1/repos/$REPO/releases" \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"tag_name\": \"$TAG\",
\"name\": \"$TAG\",
\"body\": $BODY,
\"draft\": false,
\"prerelease\": false
}")
RELEASE_ID=$(echo "$RESPONSE" | jq -r '.id')
echo "Created release ID: $RELEASE_ID"
if [ "$RELEASE_ID" = "null" ] || [ -z "$RELEASE_ID" ]; then
echo "Failed to create release:"
echo "$RESPONSE"
exit 1
fi
echo "Uploading ${#ASSETS[@]} assets:"
printf ' %s\n' "${ASSETS[@]}"
# Upload asar + checksums (patch releases only — signals client to use fast path)
if [ "$RELEASE_TYPE" = "patch" ]; then
for file in out/asar/app.asar out/asar/checksums.sha256; do
[ -f "$file" ] || continue
FILENAME=$(basename "$file")
echo "Uploading: $FILENAME ($(du -h "$file" | cut -f1))"
curl -s -X POST \
"$GITEA_BASE/api/v1/repos/$REPO/releases/$RELEASE_ID/assets?name=$FILENAME" \
-H "Authorization: token $GITEA_TOKEN" \
-F "attachment=@$file" \
| jq -r '" -> \(.name) (\(.size) bytes)"'
done
else
echo "Full release — skipping asar upload (forces installer update)"
fi
gh release create "${{ steps.version-check.outputs.TAG }}" \
--repo "$GITHUB_REPOSITORY" \
--title "${{ steps.version-check.outputs.TAG }}" \
--notes-file /tmp/release-notes.md \
--draft=false \
--latest \
"${ASSETS[@]}"
# Upload installers (always — for new users and major updates)
for file in out/*.exe out/*.AppImage out/*.deb; do
[ -f "$file" ] || continue
FILENAME=$(basename "$file")
SAFE_NAME=$(echo "$FILENAME" | tr ' ' '_')
echo "Uploading: $SAFE_NAME ($(du -h "$file" | cut -f1))"
curl -s -X POST \
"$GITEA_BASE/api/v1/repos/$REPO/releases/$RELEASE_ID/assets?name=$SAFE_NAME" \
-H "Authorization: token $GITEA_TOKEN" \
-F "attachment=@$file" \
| jq -r '" -> \(.name) (\(.size) bytes)"'
done
echo "All assets uploaded"
- name: Prune old releases
if: steps.version-check.outputs.SKIP == 'false'
env:
GITEA_TOKEN: ${{ secrets.DEST_GITEA_TOKEN }}
KEEP: 5
run: |
GITEA_BASE="https://gitea.crjlab.net"
REPO="bigjakk/Krunker-Civilian-Client-Test"
echo "=== Pruning $REPO (keeping last $KEEP) ==="
RELEASES=$(curl -s "$GITEA_BASE/api/v1/repos/$REPO/releases?limit=50" \
-H "Authorization: token $GITEA_TOKEN")
DELETE_IDS=$(echo "$RELEASES" | jq -r "sort_by(.created_at) | reverse | .[$KEEP:][] | .id")
for ID in $DELETE_IDS; do
TAG_NAME=$(echo "$RELEASES" | jq -r ".[] | select(.id == $ID) | .tag_name")
echo "Deleting release $TAG_NAME (id=$ID)"
curl -s -X DELETE "$GITEA_BASE/api/v1/repos/$REPO/releases/$ID" \
-H "Authorization: token $GITEA_TOKEN"
curl -s -X DELETE "$GITEA_BASE/api/v1/repos/$REPO/tags/$TAG_NAME" \
-H "Authorization: token $GITEA_TOKEN"
done
echo "Prune complete"
+191 -21
View File
@@ -1,18 +1,19 @@
{
"name": "krunker-civilian-client",
"version": "0.7.1",
"version": "0.7.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "krunker-civilian-client",
"version": "0.7.1",
"version": "0.7.5",
"hasInstallScript": true,
"license": "GPL-3.0",
"dependencies": {
"electron-store": "^8.2.0"
},
"devDependencies": {
"@electron/asar": "^4.2.0",
"@eslint/js": "^10.0.1",
"@types/node": "^22.0.0",
"electron": "npm:electron-nightly@42.0.0-nightly.20260227",
@@ -45,34 +46,66 @@
}
},
"node_modules/@electron/asar": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz",
"integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@electron/asar/-/asar-4.2.0.tgz",
"integrity": "sha512-npW1NW5yy8EB9XY/vEw9sUdgmq0sJEhmSBb6bqyFOAw1CSkrhvAvO6QWlW8CdIMo8VN1lkdF345l/MeW0LrY0Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"commander": "^5.0.0",
"glob": "^7.1.6",
"minimatch": "^3.0.4"
"commander": "^13.1.0",
"glob": "^13.0.2",
"minimatch": "^10.0.1"
},
"bin": {
"asar": "bin/asar.js"
"asar": "bin/asar.mjs"
},
"engines": {
"node": ">=10.12.0"
"node": ">=22.12.0"
}
},
"node_modules/@electron/asar/node_modules/minimatch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"node_modules/@electron/asar/node_modules/glob": {
"version": "13.0.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",
"integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==",
"dev": true,
"license": "ISC",
"license": "BlueOak-1.0.0",
"dependencies": {
"brace-expansion": "^1.1.7"
"minimatch": "^10.2.2",
"minipass": "^7.1.3",
"path-scurry": "^2.0.2"
},
"engines": {
"node": "*"
"node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@electron/asar/node_modules/lru-cache": {
"version": "11.3.5",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz",
"integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==",
"dev": true,
"license": "BlueOak-1.0.0",
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@electron/asar/node_modules/path-scurry": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
"integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"lru-cache": "^11.0.0",
"minipass": "^7.1.2"
},
"engines": {
"node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@electron/fuses": {
@@ -338,6 +371,48 @@
"node": ">=16.4"
}
},
"node_modules/@electron/universal/node_modules/@electron/asar": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz",
"integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==",
"dev": true,
"license": "MIT",
"dependencies": {
"commander": "^5.0.0",
"glob": "^7.1.6",
"minimatch": "^3.0.4"
},
"bin": {
"asar": "bin/asar.js"
},
"engines": {
"node": ">=10.12.0"
}
},
"node_modules/@electron/universal/node_modules/@electron/asar/node_modules/brace-expansion": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/@electron/universal/node_modules/@electron/asar/node_modules/minimatch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/@electron/universal/node_modules/brace-expansion": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
@@ -348,6 +423,16 @@
"balanced-match": "^1.0.0"
}
},
"node_modules/@electron/universal/node_modules/commander": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz",
"integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
}
},
"node_modules/@electron/universal/node_modules/fs-extra": {
"version": "11.3.3",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz",
@@ -2359,6 +2444,37 @@
"electron-builder-squirrel-windows": "26.8.1"
}
},
"node_modules/app-builder-lib/node_modules/@electron/asar": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz",
"integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==",
"dev": true,
"license": "MIT",
"dependencies": {
"commander": "^5.0.0",
"glob": "^7.1.6",
"minimatch": "^3.0.4"
},
"bin": {
"asar": "bin/asar.js"
},
"engines": {
"node": ">=10.12.0"
}
},
"node_modules/app-builder-lib/node_modules/@electron/asar/node_modules/minimatch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/app-builder-lib/node_modules/@electron/get": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@electron/get/-/get-3.1.0.tgz",
@@ -2422,6 +2538,16 @@
"node": ">=8"
}
},
"node_modules/app-builder-lib/node_modules/commander": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz",
"integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
}
},
"node_modules/app-builder-lib/node_modules/fs-extra": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
@@ -3032,13 +3158,13 @@
}
},
"node_modules/commander": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz",
"integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==",
"version": "13.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz",
"integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
"node": ">=18"
}
},
"node_modules/compare-version": {
@@ -3715,6 +3841,36 @@
"@electron/windows-sign": "^1.1.2"
}
},
"node_modules/electron-winstaller/node_modules/@electron/asar": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz",
"integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"commander": "^5.0.0",
"glob": "^7.1.6",
"minimatch": "^3.0.4"
},
"bin": {
"asar": "bin/asar.js"
},
"engines": {
"node": ">=10.12.0"
}
},
"node_modules/electron-winstaller/node_modules/commander": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz",
"integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 6"
}
},
"node_modules/electron-winstaller/node_modules/fs-extra": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz",
@@ -3731,6 +3887,20 @@
"node": ">=6 <7 || >=8"
}
},
"node_modules/electron-winstaller/node_modules/minimatch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"peer": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/electron/node_modules/@types/node": {
"version": "24.11.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.11.0.tgz",
+3 -1
View File
@@ -1,6 +1,6 @@
{
"name": "krunker-civilian-client",
"version": "0.7.5",
"version": "0.7.9",
"description": "Cross-platform Krunker game client",
"main": "dist/main/index.js",
"homepage": "https://github.com/bigjakk/Krunker-Civilian-Client",
@@ -17,6 +17,7 @@
"dist:win": "npm run build && electron-builder --win",
"dist:linux": "npm run build && electron-builder --linux",
"dist:all": "npm run build && electron-builder --win --linux",
"build:asar": "node scripts/build-asar.js",
"clean": "rimraf dist out",
"lint": "eslint src/",
"prepare": "husky"
@@ -28,6 +29,7 @@
"electron-store": "^8.2.0"
},
"devDependencies": {
"@electron/asar": "^4.2.0",
"@eslint/js": "^10.0.1",
"@types/node": "^22.0.0",
"electron": "npm:electron-nightly@42.0.0-nightly.20260227",
+146
View File
@@ -0,0 +1,146 @@
/**
* Build a standalone app.asar + checksums.sha256 for minor (patch) updates.
*
* Replicates what electron-builder packs into the asar (dist/ + package.json +
* required node_modules) without running the full installer build.
*
* Automatically resolves transitive dependencies so sub-deps are never missed.
*
* Usage:
* npm run build:asar # default (github) update source
* UPDATE_SOURCE=gitea npm run build:asar # gitea update source
*/
const { execSync } = require('child_process');
const { cpSync, mkdirSync, rmSync, readFileSync, writeFileSync, createReadStream, existsSync } = require('fs');
const { join } = require('path');
const { createHash } = require('crypto');
const asar = require('@electron/asar');
const ROOT = join(__dirname, '..');
const STAGING = join(ROOT, 'out', 'asar-staging');
const OUTPUT = join(ROOT, 'out', 'asar');
// Top-level packages to include (same as electron-builder.yml)
const SEED_MODULES = [
'electron-store', 'conf', 'dot-prop', 'type-fest', 'pkg-up',
'find-up', 'locate-path', 'p-locate', 'p-limit', 'yocto-queue',
'path-exists', 'env-paths', 'json-schema-typed', 'ajv', 'ajv-formats',
'atomically', 'debounce-fn', 'mimic-fn', 'semver', 'onetime',
];
/**
* Walk the dependency tree starting from seed modules and collect all
* transitive production dependencies.
*/
function resolveAllDeps(seeds) {
const resolved = new Set();
const queue = [...seeds];
while (queue.length > 0) {
const mod = queue.shift();
if (resolved.has(mod)) continue;
// Check if module exists in top-level node_modules
const modDir = join(ROOT, 'node_modules', mod);
if (!existsSync(modDir)) {
console.warn(` Warning: ${mod} not found in node_modules, skipping`);
continue;
}
resolved.add(mod);
// Read its dependencies and enqueue them
const pkgPath = join(modDir, 'package.json');
if (existsSync(pkgPath)) {
try {
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
const deps = Object.keys(pkg.dependencies || {});
for (const dep of deps) {
if (!resolved.has(dep)) queue.push(dep);
}
} catch { /* ignore malformed package.json */ }
}
}
return resolved;
}
function shouldExclude(src) {
return src.endsWith('.ts') || src.endsWith('.map');
}
async function main() {
// 1. Run Vite build (pass UPDATE_SOURCE through env)
const env = { ...process.env };
if (env.UPDATE_SOURCE) {
console.log(`Building with UPDATE_SOURCE=${env.UPDATE_SOURCE}`);
}
execSync('npm run build', { cwd: ROOT, stdio: 'inherit', env });
// 2. Resolve all dependencies (including transitive)
console.log('\nResolving dependencies...');
const allModules = resolveAllDeps(SEED_MODULES);
console.log(`Found ${allModules.size} modules (${SEED_MODULES.length} seed + ${allModules.size - SEED_MODULES.length} transitive)`);
// 3. Prepare staging directory
rmSync(STAGING, { recursive: true, force: true });
mkdirSync(STAGING, { recursive: true });
// Copy dist/
cpSync(join(ROOT, 'dist'), join(STAGING, 'dist'), { recursive: true });
// Copy package.json (Electron reads "main" from it)
cpSync(join(ROOT, 'package.json'), join(STAGING, 'package.json'));
// Copy all resolved node_modules (excluding .ts and .map files)
for (const mod of allModules) {
const src = join(ROOT, 'node_modules', mod);
const dest = join(STAGING, 'node_modules', mod);
// Some packages have nested node_modules with their own deps
// (hoisted vs non-hoisted). Check both the top-level and nested locations.
if (existsSync(src)) {
cpSync(src, dest, {
recursive: true,
filter: (s) => !shouldExclude(s),
});
}
}
// 4. Pack into asar
mkdirSync(OUTPUT, { recursive: true });
const asarPath = join(OUTPUT, 'app.asar');
await asar.createPackage(STAGING, asarPath);
console.log('\nCreated:', asarPath);
// 5. Generate checksums.sha256
const hex = await fileHash(asarPath);
const checksumsPath = join(OUTPUT, 'checksums.sha256');
writeFileSync(checksumsPath, `${hex} app.asar\n`);
console.log(`SHA-256: ${hex}`);
console.log('Created:', checksumsPath);
// 6. Cleanup staging
rmSync(STAGING, { recursive: true, force: true });
// Print size and module list
const size = readFileSync(asarPath).length;
console.log(`\napp.asar size: ${(size / 1024).toFixed(1)} KB`);
console.log('Modules:', [...allModules].sort().join(', '));
}
function fileHash(filePath) {
return new Promise((resolve, reject) => {
const hash = createHash('sha256');
const stream = createReadStream(filePath);
stream.on('data', (chunk) => hash.update(chunk));
stream.on('end', () => resolve(hash.digest('hex')));
stream.on('error', reject);
});
}
main().catch((err) => {
console.error('build-asar failed:', err);
process.exit(1);
});
+56 -13
View File
@@ -1,6 +1,7 @@
import { app, BrowserWindow, Menu, clipboard, ipcMain, safeStorage, session, shell } from 'electron';
import { join } from 'path';
import { join, dirname } from 'path';
import { existsSync, mkdirSync, promises as fsp } from 'fs';
import { existsSync as origExistsSync, unlinkSync as origUnlinkSync } from 'original-fs';
import { get as httpsGet } from 'https';
import { execFile } from 'child_process';
import * as os from 'os';
@@ -10,7 +11,7 @@ import { initSwapperProtocol, registerSwapperFileProtocol, ResourceSwapper } fro
import { UserscriptManager } from './userscripts';
import { ALL_CLIENT_CSS } from './client-ui';
import { electronLog, getLogPath, closeLogStreams } from './logger';
import { checkForUpdate, downloadUpdate, installUpdate } from './updater';
import { checkForUpdate, downloadUpdate, installUpdate, applyMinorUpdate } from './updater';
import { showUpdateWindow } from './update-window';
import { DiscordRPC } from './discord-rpc';
import { listThemes, getThemeCSS, listLoadingThemes, getLoadingScreenCSS } from './css-themes';
@@ -164,45 +165,87 @@ function saveWindowState(win: BrowserWindow): void {
app.whenReady().then(async () => {
electronLog.log('[KCC] App ready');
electronLog.log('[KCC] Minor update test — asar swap successful');
// ── Auto-update check (mandatory, Windows NSIS install only) ──
// ── Auto-update check (2-stage: minor asar swap or major installer) ──
const isPortable = !!process.env.PORTABLE_EXECUTABLE_DIR;
const isAppImage = !!process.env.APPIMAGE;
const isDev = !app.isPackaged;
if (isDev || process.platform !== 'win32' || isPortable || isAppImage) {
electronLog.log('[KCC] Skipping auto-update (portable or non-Windows)');
const canMajorUpdate = process.platform === 'win32' && !isPortable;
const canMinorUpdate = !isPortable && !isAppImage;
if (isDev || (!canMajorUpdate && !canMinorUpdate)) {
electronLog.log('[KCC] Skipping auto-update');
} else {
// Clean up stale pending asar from a previous failed swap
const resourcesDir = join(dirname(app.getPath('exe')), 'resources');
const stalePending = join(resourcesDir, 'app-pending.asar');
if (origExistsSync(stalePending)) {
try { origUnlinkSync(stalePending); } catch { /* ignore */ }
}
try {
electronLog.log('[KCC] Checking for updates...');
const update = await checkForUpdate(appVersion);
if (update) {
electronLog.log(`[KCC] Update available: v${update.version}`);
electronLog.log(`[KCC] Update available: v${update.version} (${update.updateType})`);
const { window: updateWin, sendProgress } = showUpdateWindow();
sendProgress(`Update available (v${update.version})`, 0);
const tempDir = join(app.getPath('temp'), 'kcc-update');
if (!existsSync(tempDir)) mkdirSync(tempDir, { recursive: true });
const installerPath = join(tempDir, `KCC-${update.version}-Setup.exe`);
let cancelled = false;
updateWin.on('closed', () => { cancelled = true; });
if (update.updateType === 'minor' && canMinorUpdate) {
// Minor update: download app.asar, swap via external script, restart
sendProgress(`Patch available (v${update.version})`, 0);
const pendingPath = join(resourcesDir, 'app-pending.asar');
try {
await downloadUpdate(update.downloadUrl, pendingPath, (pct) => {
if (!cancelled && !updateWin.isDestroyed()) {
sendProgress(`Downloading patch... ${pct}%`, pct);
}
}, update.sha256 || undefined);
if (!cancelled) {
sendProgress('Applying patch...', 100);
applyMinorUpdate(pendingPath);
return;
}
} catch (err) {
electronLog.error('[KCC] Patch download failed:', err);
// Clean up failed download
if (origExistsSync(pendingPath)) {
try { origUnlinkSync(pendingPath); } catch { /* ignore */ }
}
if (!updateWin.isDestroyed()) updateWin.close();
}
} else if (update.updateType === 'major' && canMajorUpdate) {
// Major update: download Setup.exe, run installer
sendProgress(`Update available (v${update.version})`, 0);
const tempDir = join(app.getPath('temp'), 'kcc-update');
if (!existsSync(tempDir)) mkdirSync(tempDir, { recursive: true });
const installerPath = join(tempDir, `KCC-${update.version}-Setup.exe`);
try {
await downloadUpdate(update.downloadUrl, installerPath, (pct) => {
if (!cancelled && !updateWin.isDestroyed()) {
sendProgress(`Downloading update... ${pct}%`, pct);
}
}, update.sha256);
}, update.sha256 || undefined);
if (!cancelled) {
sendProgress('Installing update...', 100);
installUpdate(installerPath);
return; // app.quit() called by installUpdate
return;
}
} catch (err) {
electronLog.error('[KCC] Update download failed:', err);
if (!updateWin.isDestroyed()) updateWin.close();
}
} else {
electronLog.log('[KCC] Update available but cannot auto-install on this platform');
if (!updateWin.isDestroyed()) updateWin.close();
}
} else {
electronLog.log('[KCC] No updates available');
}
+21 -1
View File
@@ -339,19 +339,39 @@ export class TabManager {
const tab = this.tabs.find(t => t.id === id);
if (!tab) return;
if (this.activeTabId !== null) {
if (this.activeTabId !== null && this.activeTabId !== id) {
const prev = this.tabs.find(t => t.id === this.activeTabId);
if (prev) {
this.containerView.removeChildView(prev.view);
this.freezeTab(prev);
}
}
this.activeTabId = id;
this.unfreezeTab(tab);
this.containerView.addChildView(tab.view);
this.updateLayout();
this.broadcastTabState();
}
// ── Freeze/unfreeze background tabs via CDP Page Lifecycle ──
private freezeTab(tab: TabInfo): void {
const wc = tab.view.webContents;
if (wc.isDestroyed()) return;
this.stopTitleWatcher(tab.id);
try { wc.debugger.attach('1.3'); } catch { /* already attached (DevTools open) */ }
wc.debugger.sendCommand('Page.setWebLifecycleState', { state: 'frozen' }).catch(() => {});
}
private unfreezeTab(tab: TabInfo): void {
const wc = tab.view.webContents;
if (wc.isDestroyed()) return;
wc.debugger.sendCommand('Page.setWebLifecycleState', { state: 'active' }).catch(() => {}).finally(() => {
try { wc.debugger.detach(); } catch { /* not attached */ }
if (!wc.isDestroyed()) this.startTitleWatcher(tab.id, wc);
});
}
// ── Close a tab ──
closeTab(id: number): void {
const idx = this.tabs.findIndex(t => t.id === id);
+236 -26
View File
@@ -1,12 +1,20 @@
import { get as httpsGet } from 'https';
import { createReadStream, createWriteStream, renameSync, unlinkSync, existsSync } from 'fs';
// Use original-fs to bypass Electron's asar interception — required for
// writing/renaming .asar files in the resources directory.
import { createReadStream, createWriteStream, writeFileSync, renameSync, unlinkSync, existsSync, mkdirSync } from 'original-fs';
import { join, dirname } from 'path';
import { createHash } from 'crypto';
import { spawn } from 'child_process';
import { app } from 'electron';
import { electronLog } from './logger';
// ── Types ──
export type UpdateType = 'minor' | 'major';
export interface UpdateInfo {
version: string;
updateType: UpdateType;
downloadUrl: string;
fileSize: number;
sha256: string;
@@ -14,18 +22,102 @@ export interface UpdateInfo {
export type ProgressCallback = (percent: number) => void;
const UPDATE_CONFIG = {
// ── Build-time update source (injected by Vite define) ──
declare const __UPDATE_SOURCE__: 'github' | 'gitea';
interface UpdateSourceConfig {
checkUrl: string;
allowedHosts: string[];
checksumSource: 'digest' | 'file';
}
const UPDATE_SOURCES: Record<string, UpdateSourceConfig> = {
github: {
checkUrl: 'https://api.github.com/repos/bigjakk/Krunker-Civilian-Client/releases/latest',
assetPattern: /Setup\.exe$/i,
allowedHosts: ['github.com', 'githubusercontent.com'],
checksumSource: 'digest',
},
gitea: {
checkUrl: 'https://gitea.crjlab.net/api/v1/repos/bigjakk/Krunker-Civilian-Client-Test/releases/latest',
allowedHosts: ['gitea.crjlab.net'],
checksumSource: 'file',
},
};
const sourceKey = typeof __UPDATE_SOURCE__ !== 'undefined' ? __UPDATE_SOURCE__ : 'github';
const UPDATE_CONFIG = UPDATE_SOURCES[sourceKey] || UPDATE_SOURCES.github;
const ASSET_PATTERNS = {
asar: /^app\.asar$/i,
setup: /Setup\.exe$/i,
checksums: /^checksums\.sha256$/i,
};
const CHECK_TIMEOUT_MS = 10000;
const DOWNLOAD_TIMEOUT_MS = 300000; // 5 minutes
/**
* Validate that a redirect URL stays on an allowed host.
*/
// ── Swap scripts (embedded, written to temp at runtime) ──
const SWAP_SCRIPT_PS1 = `param(
[int]$ProcessId,
[string]$ResourcesDir,
[string]$ExePath
)
try {
$proc = Get-Process -Id $ProcessId -ErrorAction SilentlyContinue
if ($proc) { $proc.WaitForExit(30000) | Out-Null }
} catch {}
Start-Sleep -Milliseconds 500
$asar = Join-Path $ResourcesDir "app.asar"
$pending = Join-Path $ResourcesDir "app-pending.asar"
$backup = Join-Path $ResourcesDir "app-backup.asar"
if (-not (Test-Path $pending)) { exit 1 }
try {
if (Test-Path $backup) { Remove-Item $backup -Force }
Move-Item $asar $backup -Force
Move-Item $pending $asar -Force
if (Test-Path $backup) { Remove-Item $backup -Force }
} catch {
if ((Test-Path $backup) -and -not (Test-Path $asar)) {
Move-Item $backup $asar -Force
}
exit 1
}
Start-Process $ExePath
`;
const SWAP_SCRIPT_BASH = `#!/bin/bash
PID="$1"
RESOURCES_DIR="$2"
EXE_PATH="$3"
while kill -0 "$PID" 2>/dev/null; do sleep 0.2; done
sleep 0.5
ASAR="$RESOURCES_DIR/app.asar"
PENDING="$RESOURCES_DIR/app-pending.asar"
BACKUP="$RESOURCES_DIR/app-backup.asar"
[ -f "$PENDING" ] || exit 1
rm -f "$BACKUP"
mv "$ASAR" "$BACKUP" && mv "$PENDING" "$ASAR" && rm -f "$BACKUP" || {
[ -f "$BACKUP" ] && [ ! -f "$ASAR" ] && mv "$BACKUP" "$ASAR"
exit 1
}
"$EXE_PATH" &
`;
// ── Helpers ──
function isAllowedRedirect(url: string): boolean {
try {
const parsed = new URL(url);
@@ -35,10 +127,6 @@ function isAllowedRedirect(url: string): boolean {
}
}
/**
* Simple semver comparison: returns true if a < b.
* Handles versions like "0.1.0", "1.2.3".
*/
function versionLessThan(a: string, b: string): boolean {
const pa = a.split('.').map(Number);
const pb = b.split('.').map(Number);
@@ -52,6 +140,66 @@ function versionLessThan(a: string, b: string): boolean {
return false;
}
function simpleGet(url: string): Promise<string> {
return new Promise((resolve, reject) => {
function doGet(getUrl: string, redirectCount = 0): void {
if (redirectCount > 5) {
reject(new Error('Too many redirects'));
return;
}
const req = httpsGet(getUrl, {
headers: { 'User-Agent': 'KrunkerCivilianClient' },
}, (res) => {
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
if (!isAllowedRedirect(res.headers.location)) {
reject(new Error('Redirect to untrusted host: ' + res.headers.location));
return;
}
doGet(res.headers.location, redirectCount + 1);
return;
}
if (res.statusCode !== 200) {
reject(new Error('HTTP ' + res.statusCode));
return;
}
let data = '';
res.on('data', (chunk: string) => { data += chunk; });
res.on('end', () => resolve(data));
res.on('error', reject);
});
req.setTimeout(CHECK_TIMEOUT_MS, () => {
req.destroy();
reject(new Error('Request timed out'));
});
req.on('error', reject);
}
doGet(url);
});
}
/**
* Fetch and parse a checksums.sha256 file from a release asset URL.
* Format: "<sha256> <filename>" per line.
*/
async function fetchChecksums(url: string): Promise<Map<string, string>> {
const text = await simpleGet(url);
const map = new Map<string, string>();
for (const line of text.split('\n')) {
const match = line.trim().match(/^([a-f0-9]{64})\s+(.+)$/i);
if (match) map.set(match[2].trim(), match[1].toLowerCase());
}
return map;
}
// ── Update check ──
interface ReleaseAsset {
name: string;
browser_download_url: string;
size: number;
digest?: string;
}
export function checkForUpdate(currentVersion: string): Promise<UpdateInfo | null> {
return new Promise((resolve) => {
electronLog.log('[KCC-Update] Checking for updates at:', UPDATE_CONFIG.checkUrl);
@@ -61,7 +209,6 @@ export function checkForUpdate(currentVersion: string): Promise<UpdateInfo | nul
headers: { 'User-Agent': 'KrunkerCivilianClient/' + currentVersion },
}, (res) => {
electronLog.log('[KCC-Update] Check response status:', res.statusCode);
// Follow redirects (with domain validation)
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
const redirectUrl = res.headers.location;
electronLog.log('[KCC-Update] Redirected to:', redirectUrl);
@@ -84,7 +231,7 @@ export function checkForUpdate(currentVersion: string): Promise<UpdateInfo | nul
handleResponse(res);
});
function handleResponse(res: import('http').IncomingMessage): void {
async function handleResponse(res: import('http').IncomingMessage): Promise<void> {
if (res.statusCode !== 200) {
electronLog.error('[KCC-Update] Check returned status', res.statusCode);
resolve(null);
@@ -93,7 +240,7 @@ export function checkForUpdate(currentVersion: string): Promise<UpdateInfo | nul
let data = '';
res.on('data', (chunk: string) => { data += chunk; });
res.on('end', () => {
res.on('end', async () => {
try {
const release = JSON.parse(data);
const tagName: string = release.tag_name || '';
@@ -106,34 +253,60 @@ export function checkForUpdate(currentVersion: string): Promise<UpdateInfo | nul
return;
}
const assets: Array<{ name: string; browser_download_url: string; size: number; digest: string }> = release.assets || [];
const setupAsset = assets.find((a) => UPDATE_CONFIG.assetPattern.test(a.name));
if (!setupAsset) {
electronLog.error('[KCC-Update] No Setup.exe asset found in release', remoteVersion);
const assets: ReleaseAsset[] = release.assets || [];
// Determine update type: prefer minor (asar) over major (setup)
const asarAsset = assets.find((a) => ASSET_PATTERNS.asar.test(a.name));
const setupAsset = assets.find((a) => ASSET_PATTERNS.setup.test(a.name));
const chosenAsset = asarAsset || setupAsset;
const updateType: UpdateType = asarAsset ? 'minor' : 'major';
if (!chosenAsset) {
electronLog.error('[KCC-Update] No app.asar or Setup.exe asset found in release', remoteVersion);
resolve(null);
return;
}
// Validate the download URL points to an allowed host
if (!isAllowedRedirect(setupAsset.browser_download_url)) {
electronLog.error('[KCC-Update] Download URL points to untrusted host:', setupAsset.browser_download_url);
if (!isAllowedRedirect(chosenAsset.browser_download_url)) {
electronLog.error('[KCC-Update] Download URL points to untrusted host:', chosenAsset.browser_download_url);
resolve(null);
return;
}
// Extract SHA-256 digest from GitHub API (format: "sha256:<hex>")
const sha256 = (setupAsset.digest || '').replace(/^sha256:/i, '');
// Resolve SHA-256 checksum
let sha256 = '';
if (UPDATE_CONFIG.checksumSource === 'digest') {
sha256 = (chosenAsset.digest || '').replace(/^sha256:/i, '');
if (!sha256) {
electronLog.error('[KCC-Update] No SHA-256 digest found for asset');
resolve(null);
return;
}
} else {
// Fetch checksums.sha256 companion file
const checksumAsset = assets.find((a) => ASSET_PATTERNS.checksums.test(a.name));
if (checksumAsset) {
try {
const checksums = await fetchChecksums(checksumAsset.browser_download_url);
sha256 = checksums.get(chosenAsset.name) || '';
} catch (err) {
electronLog.error('[KCC-Update] Failed to fetch checksums:', err);
}
}
if (!sha256) {
electronLog.warn('[KCC-Update] No checksum available — proceeding without verification');
}
}
electronLog.log('[KCC-Update] Update available:', remoteVersion,
'| Type:', updateType,
'| SHA-256:', sha256 ? sha256.substring(0, 16) + '...' : 'none');
electronLog.log('[KCC-Update] Update available:', remoteVersion, '| SHA-256:', sha256.substring(0, 16) + '...');
resolve({
version: remoteVersion,
downloadUrl: setupAsset.browser_download_url,
fileSize: setupAsset.size,
updateType,
downloadUrl: chosenAsset.browser_download_url,
fileSize: chosenAsset.size,
sha256,
});
} catch (err) {
@@ -160,6 +333,8 @@ export function checkForUpdate(currentVersion: string): Promise<UpdateInfo | nul
});
}
// ── Download ──
function verifyChecksum(filePath: string, expectedSha256: string): Promise<boolean> {
return new Promise((resolve, reject) => {
const hash = createHash('sha256');
@@ -188,7 +363,6 @@ export function downloadUpdate(url: string, destPath: string, onProgress: Progre
const req = httpsGet(downloadUrl, {
headers: { 'User-Agent': 'KrunkerCivilianClient' },
}, (res) => {
// Follow redirects (with domain validation)
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
const redirectUrl = res.headers.location;
electronLog.log('[KCC-Update] Download redirected to:', redirectUrl);
@@ -268,6 +442,8 @@ export function downloadUpdate(url: string, destPath: string, onProgress: Progre
});
}
// ── Install / Apply ──
export function installUpdate(installerPath: string): void {
electronLog.log('[KCC-Update] Launching installer:', installerPath);
const child = spawn(installerPath, [], {
@@ -277,3 +453,37 @@ export function installUpdate(installerPath: string): void {
child.unref();
app.quit();
}
export function applyMinorUpdate(pendingAsarPath: string): void {
const resourcesDir = dirname(pendingAsarPath);
const exePath = app.getPath('exe');
const tempDir = join(app.getPath('temp'), 'kcc-update');
if (!existsSync(tempDir)) mkdirSync(tempDir, { recursive: true });
electronLog.log('[KCC-Update] Applying minor update via swap script');
electronLog.log('[KCC-Update] Resources dir:', resourcesDir);
electronLog.log('[KCC-Update] Exe path:', exePath);
electronLog.log('[KCC-Update] PID:', process.pid);
if (process.platform === 'win32') {
const scriptPath = join(tempDir, 'swap-asar.ps1');
writeFileSync(scriptPath, SWAP_SCRIPT_PS1);
const child = spawn('powershell.exe', [
'-ExecutionPolicy', 'Bypass',
'-File', scriptPath,
'-ProcessId', String(process.pid),
'-ResourcesDir', resourcesDir,
'-ExePath', exePath,
], { detached: true, stdio: 'ignore' });
child.unref();
} else {
const scriptPath = join(tempDir, 'swap-asar.sh');
writeFileSync(scriptPath, SWAP_SCRIPT_BASH, { mode: 0o755 });
const child = spawn('bash', [
scriptPath, String(process.pid), resourcesDir, exePath,
], { detached: true, stdio: 'ignore' });
child.unref();
}
app.quit();
}
+4 -1
View File
@@ -8,6 +8,9 @@ const nodeBuiltins = builtinModules.flatMap((m) => [m, `node:${m}`]);
const isProd = process.env.NODE_ENV === 'production' || !process.argv.includes('--mode');
export default defineConfig({
define: {
__UPDATE_SOURCE__: JSON.stringify(process.env.UPDATE_SOURCE || 'gitea'),
},
build: {
lib: {
entry: resolve(__dirname, 'src/main/index.ts'),
@@ -17,7 +20,7 @@ export default defineConfig({
outDir: 'dist/main',
emptyDirBefore: true,
rollupOptions: {
external: ['electron', 'electron-store', ...nodeBuiltins],
external: ['electron', 'electron-store', 'original-fs', ...nodeBuiltins],
},
target: 'node20',
minify: isProd,