2 Commits
14 changed files with 351 additions and 35 deletions
+13
View File
@@ -1,5 +1,18 @@
# Changelog # Changelog
## 0.8.5 - 2026-07-26
- fixed packaged update downloads when Gitea reports an asset URL with a different public origin;
- downloads release assets by immutable Gitea asset ID on the configured trusted origin;
- retains strict token isolation and never follows authenticated downloads to another host.
## 0.8.4 - 2026-07-26
- added interactive animated release-flow illustrations to high-value project surfaces;
- introduced cursor-responsive depth, travelling deployment signals and living status nodes;
- added compact repository illustration watermarks without reducing usable header space;
- made every illustration theme-aware, responsive, semantic and reduced-motion safe.
## 0.8.3 - 2026-07-26 ## 0.8.3 - 2026-07-26
- enriched light mode with layered color, depth and stronger navigation hierarchy; - enriched light mode with layered color, depth and stronger navigation hierarchy;
+14 -12
View File
@@ -1,4 +1,4 @@
ForgeFlow 0.8.3 source manifest ForgeFlow 0.8.5 source manifest
SHA-256 BYTES PATH SHA-256 BYTES PATH
(The manifest excludes itself, dependencies and generated release artifacts.) (The manifest excludes itself, dependencies and generated release artifacts.)
755f4db7d76bfec0963ef051748a82810c0d58acd4ffd823aa6928a5167fceb4 58 .gitignore 755f4db7d76bfec0963ef051748a82810c0d58acd4ffd823aa6928a5167fceb4 58 .gitignore
@@ -12,7 +12,7 @@ ca32a76e708d565c4af659f0f4d2615fc32114c3f75aec1454862a3ed1e72c41 2263
4633990a4b055bb3d00fef915ee29e85be5ee8413f809334728ad9688973c183 3364 build/icon-64.png 4633990a4b055bb3d00fef915ee29e85be5ee8413f809334728ad9688973c183 3364 build/icon-64.png
25048ed854e8ce8fece115e555c98d25507b002f8019b6ae717b54604c868c50 46223 build/icon.ico 25048ed854e8ce8fece115e555c98d25507b002f8019b6ae717b54604c868c50 46223 build/icon.ico
16efd2fca83004f781eae40ae0f706a004ce0bddf338dd087b8adf7eb10c1d84 85704 build/icon.png 16efd2fca83004f781eae40ae0f706a004ce0bddf338dd087b8adf7eb10c1d84 85704 build/icon.png
02c39f04f01394f687eac1d8bae0cdd85018d8e95ff941b6ce6d9f9d7b19d697 8606 CHANGELOG.md ddd7faeae0a1700c3459b48ea086da1103c41efaf87f05a50e349389169f5fed 9288 CHANGELOG.md
21cb96e7afe71b1dc791c818dedd244d92f9a6ed4d9ffbb3022ccb187e1bdf0f 852 docs/ACCEPTANCE.md 21cb96e7afe71b1dc791c818dedd244d92f9a6ed4d9ffbb3022ccb187e1bdf0f 852 docs/ACCEPTANCE.md
a17f95d96d3c9fbc69d870874e6fbb7472091adefc454b24f835db1279511d72 8296 docs/ARCHITECTURE.md a17f95d96d3c9fbc69d870874e6fbb7472091adefc454b24f835db1279511d72 8296 docs/ARCHITECTURE.md
30a92bcf5daadb019efa2f82cb820ea302490dd1d68fb772674dc3faccd3e594 2045 docs/DEPLOYMENT_SETUP.md 30a92bcf5daadb019efa2f82cb820ea302490dd1d68fb772674dc3faccd3e594 2045 docs/DEPLOYMENT_SETUP.md
@@ -41,6 +41,8 @@ d7bdc61d9b617ad5acf0b2d468eda547fd7509d4af08f33d2661393f25bdcb5a 576
1e056bfcf2105843402f4b14c63480240cc55456a4a63e229b3fdbaf3156b803 754 docs/RELEASE_NOTES_0.8.1.md 1e056bfcf2105843402f4b14c63480240cc55456a4a63e229b3fdbaf3156b803 754 docs/RELEASE_NOTES_0.8.1.md
7f1d7c8bc895d309dad2f8ab444d6d2ba3e68c8daa240fecd2abdd9d8a56ba20 729 docs/RELEASE_NOTES_0.8.2.md 7f1d7c8bc895d309dad2f8ab444d6d2ba3e68c8daa240fecd2abdd9d8a56ba20 729 docs/RELEASE_NOTES_0.8.2.md
c2802fa5dbff392c846b82b55e84a8bfb8e1625546fd1eba39f7129318bece96 654 docs/RELEASE_NOTES_0.8.3.md c2802fa5dbff392c846b82b55e84a8bfb8e1625546fd1eba39f7129318bece96 654 docs/RELEASE_NOTES_0.8.3.md
8c13279987672314332f648889f52338bcdcb243249f9e1c20fb09d85d7808f5 505 docs/RELEASE_NOTES_0.8.4.md
b516db97a0353babc810c24a87a971d30d72a8021d809e6b833ff7ae0458f442 538 docs/RELEASE_NOTES_0.8.5.md
2b631b9d6d973bdd70869d84886ff339da351e29e17598970b3b27915674661d 4175 docs/ROADMAP.md 2b631b9d6d973bdd70869d84886ff339da351e29e17598970b3b27915674661d 4175 docs/ROADMAP.md
1ccde232c060395d7aedce27e89a7647b77afe28ab71de0a5a3efeded57369d3 140415 docs/screenshots/deploy-confirmation.png 1ccde232c060395d7aedce27e89a7647b77afe28ab71de0a5a3efeded57369d3 140415 docs/screenshots/deploy-confirmation.png
b39506254ffa2c73c389fb4795b3a745368bbeb7d8514cc47a636316d6d9a6aa 107166 docs/screenshots/deployment-run.png b39506254ffa2c73c389fb4795b3a745368bbeb7d8514cc47a636316d6d9a6aa 107166 docs/screenshots/deployment-run.png
@@ -64,8 +66,8 @@ c230b931abf2293d2d44b7a69b94c35f1142c093cc46b88739a0de5cbd6d1896 1532
4a561ead5ba7cdfaf4efce91842a4308c5f2a77980205879d83835efb8a579db 1067 LICENSE 4a561ead5ba7cdfaf4efce91842a4308c5f2a77980205879d83835efb8a579db 1067 LICENSE
6765015bdf27b288a250192272750b243c3cb8d1326b752d056d1e43317b6344 12935 main.cjs 6765015bdf27b288a250192272750b243c3cb8d1326b752d056d1e43317b6344 12935 main.cjs
91a984a89dd57a084b9a2331763cacdb061582fb590f13df379d92c1a77a2ee1 352 OVERLAY-INSTRUCTIONS.md 91a984a89dd57a084b9a2331763cacdb061582fb590f13df379d92c1a77a2ee1 352 OVERLAY-INSTRUCTIONS.md
8f1b45693760790cca87c0643a0e43adb38b1b7182a592b0ac5e1f3883d47566 130466 package-lock.json 3ffef38b3c056134c2ed8394e7bdcbb1c73e388024aa4f04a668e791060c600a 130466 package-lock.json
17ec2a34f0182f0752212765674afd79df9aa47c6b833065b943743737ce6bc8 3721 package.json ca3f27afd345ca2880f33f619e01bb161272d99f289f19f94831f4625f286006 3795 package.json
3d2ac366a13e9418e3ec6d13ce95b611f30f0228eb3a80ef9e7a936ce9578e24 9080 preload.cjs 3d2ac366a13e9418e3ec6d13ce95b611f30f0228eb3a80ef9e7a936ce9578e24 9080 preload.cjs
b31c43d9355c13b5ae4efc0f3649d8cb8d509b2bb7ebb042ff546b7820fb7de8 8411 Publish-ForgeFlow-Release.ps1 b31c43d9355c13b5ae4efc0f3649d8cb8d509b2bb7ebb042ff546b7820fb7de8 8411 Publish-ForgeFlow-Release.ps1
a6d32a742412b7836606be00f17be0465f1b6f55d3911f6c73a14029787ba206 14037 README.md a6d32a742412b7836606be00f17be0465f1b6f55d3911f6c73a14029787ba206 14037 README.md
@@ -78,7 +80,7 @@ f8359a69d20deb2dfe10042d1bec7b12a95e76e58e36bc5f265f073c3111d056 10287
74433d8a6b24afe368197a469e2fe0c5050c239d7250b84c2f3f598c304778b0 4736 scripts/publish-binary-release.cjs 74433d8a6b24afe368197a469e2fe0c5050c239d7250b84c2f3f598c304778b0 4736 scripts/publish-binary-release.cjs
444b397d515d65a7ee59d3088cba869cbb812d2b8cc18fc5d255105e3edb58c2 1468 scripts/serve-demo.mjs 444b397d515d65a7ee59d3088cba869cbb812d2b8cc18fc5d255105e3edb58c2 1468 scripts/serve-demo.mjs
42203f9e0fd4aae517284d387f265cf1b0b180379bc253a092b5c3c5c4caef0a 2992 scripts/validate-installed-connections.cjs 42203f9e0fd4aae517284d387f265cf1b0b180379bc253a092b5c3c5c4caef0a 2992 scripts/validate-installed-connections.cjs
a2f19d7c45132973b3db3a56b57bd4a8e34de98e7c446b19cfaf3c4904fe5715 12209 scripts/verify.mjs c73c8bed917ee7dbf5f7fa57923456a4fcf43b1b60b3a67da7cfe9b5e2c223f5 12275 scripts/verify.mjs
0079701b5acbfef07b71a9623613d1940805ccd20649d77e3f34c37e79df7655 735 scripts/write-release-checksums.mjs 0079701b5acbfef07b71a9623613d1940805ccd20649d77e3f34c37e79df7655 735 scripts/write-release-checksums.mjs
619515f524cb89960370ffcbd3fafd3c0e178b95f69c5868b1dd44777f23ec1e 2081 setup-windows.ps1 619515f524cb89960370ffcbd3fafd3c0e178b95f69c5868b1dd44777f23ec1e 2081 setup-windows.ps1
dd613d04b366f2cd071a1685a414016a5fb008082ed1b4cb8b24b79c100f640a 2412 src/main/audit-service.cjs dd613d04b366f2cd071a1685a414016a5fb008082ed1b4cb8b24b79c100f640a 2412 src/main/audit-service.cjs
@@ -88,7 +90,7 @@ a381848a296c28f6d14093c96f722967acf9c994ffb867d54dd92bf5ada2729b 23648
c157640e76d558906a9aa9881eda811196623ef1c65fa3467f32f0f84b0ddd0c 15095 src/main/diagnostics-service.cjs c157640e76d558906a9aa9881eda811196623ef1c65fa3467f32f0f84b0ddd0c 15095 src/main/diagnostics-service.cjs
a2ef47d5330095b92c2bd22fcc39962091881f9cb60d02e261eb1dd1bd693170 1974 src/main/external-tools-service.cjs a2ef47d5330095b92c2bd22fcc39962091881f9cb60d02e261eb1dd1bd693170 1974 src/main/external-tools-service.cjs
0b7476c2cfe1872601978c20a466c20fe58be35e81b2303e38a753fea62bbc27 32548 src/main/git-service.cjs 0b7476c2cfe1872601978c20a466c20fe58be35e81b2303e38a753fea62bbc27 32548 src/main/git-service.cjs
f5b4e468c92eb0d96d02357290ac4bfe2d30eef9c7287267882bc146237a8693 16559 src/main/gitea-service.cjs 75f25fa8ee520b03b0bbe4c4cea439853202d318547343cda49a19c6ab868901 16984 src/main/gitea-service.cjs
79593a28b8f40f48a73028be34464f94be2e4c67a027a3f5da78a33bcadc76eb 45539 src/main/ipc.cjs 79593a28b8f40f48a73028be34464f94be2e4c67a027a3f5da78a33bcadc76eb 45539 src/main/ipc.cjs
62f2c80c8210e19370b8556b1f296cbae50dae6b758a39e209f8fb461691fd4c 4235 src/main/log-redaction.cjs 62f2c80c8210e19370b8556b1f296cbae50dae6b758a39e209f8fb461691fd4c 4235 src/main/log-redaction.cjs
958595a99fb242c127f475f3d8622bdba4c07b2d658703f69fe3992227a9107e 12909 src/main/preflight-service.cjs 958595a99fb242c127f475f3d8622bdba4c07b2d658703f69fe3992227a9107e 12909 src/main/preflight-service.cjs
@@ -97,15 +99,15 @@ e89b54e7e3174b4b0a1dcd9058d8344e29431f9d16d0e6bb8d11559b691440a0 2508
17e2a53f61cd7faba461b9f332967143087eaac95b72001462292976278ca305 7782 src/main/repository-service.cjs 17e2a53f61cd7faba461b9f332967143087eaac95b72001462292976278ca305 7782 src/main/repository-service.cjs
b31a63bf8cb1807b3e838e2bf8a0e742738f119d13de8ca9f42e471f072217d3 8328 src/main/ssh-service.cjs b31a63bf8cb1807b3e838e2bf8a0e742738f119d13de8ca9f42e471f072217d3 8328 src/main/ssh-service.cjs
8cf5013de91737dd9345b121586ceec38a6fc8518648975d60593d81fc225c4b 67376 src/main/unraid-deployment-service.cjs 8cf5013de91737dd9345b121586ceec38a6fc8518648975d60593d81fc225c4b 67376 src/main/unraid-deployment-service.cjs
36cc05deda3395e5e9de92b880c8315f508cb88e9b080ae34705057ae696804f 20696 src/main/update-service.cjs 27b9e15dd6530bc7bfab51414430eaef169e0cc4acd027257b1c63f891af8c85 20733 src/main/update-service.cjs
982039f1243c3e48b589b8ad6f8fe712b5b8dde3d23f6f171ad31b09ff1a70c2 192055 src/renderer/app.js ef029189791024421d78b3ac92981f03b20f2086fc4846eaa95d509f65e096a9 194049 src/renderer/app.js
16efd2fca83004f781eae40ae0f706a004ce0bddf338dd087b8adf7eb10c1d84 85704 src/renderer/assets/itworx-mark.png 16efd2fca83004f781eae40ae0f706a004ce0bddf338dd087b8adf7eb10c1d84 85704 src/renderer/assets/itworx-mark.png
813b8cdeecac43794166f3db9d3c5d2c441e0292f9ab7bd465ba136d6201e95d 82476 src/renderer/assets/itworx-wordmark-dark.png 813b8cdeecac43794166f3db9d3c5d2c441e0292f9ab7bd465ba136d6201e95d 82476 src/renderer/assets/itworx-wordmark-dark.png
094c1b71cc2482a9db250ac175f45f3de68f53277dfbde371a03e61923d00988 75240 src/renderer/assets/itworx-wordmark-light.png 094c1b71cc2482a9db250ac175f45f3de68f53277dfbde371a03e61923d00988 75240 src/renderer/assets/itworx-wordmark-light.png
813b8cdeecac43794166f3db9d3c5d2c441e0292f9ab7bd465ba136d6201e95d 82476 src/renderer/assets/itworx-wordmark.png 813b8cdeecac43794166f3db9d3c5d2c441e0292f9ab7bd465ba136d6201e95d 82476 src/renderer/assets/itworx-wordmark.png
e1c463d6cda9f2b9b78c468845c0a7e8688f0362be5642074a1a5f7122dfe811 762 src/renderer/index.html e1c463d6cda9f2b9b78c468845c0a7e8688f0362be5642074a1a5f7122dfe811 762 src/renderer/index.html
68e565f0dc3d8a7fbce0d57fc55535c0fe022fd96ebd716d1adf279f995b0256 50922 src/renderer/mock-bridge.js 34dd57cd916b3b2faabaf73d3063d5e6ed734393f13db795b51bff150f4bdc2d 50922 src/renderer/mock-bridge.js
09c832aaea5994558eddcd17b20e7edd6840df5fc80e50354c627c41989711d9 63138 src/renderer/styles.css ebdd804b55c0b13cbf57e51c6d51a60dd838d67bb4c4eaf17ca34c86938a6865 67440 src/renderer/styles.css
0a1e9d9d6cd4d190eb7f85dbc6668d80600b1cf2749cc0c2c51cc428f506f20d 1121 src/shared/clone-target.cjs 0a1e9d9d6cd4d190eb7f85dbc6668d80600b1cf2749cc0c2c51cc428f506f20d 1121 src/shared/clone-target.cjs
5d425d5c2f939d0f6beebee7ebb0c77146cb7e318535ba7286ec7081a4dc2269 2497 src/shared/deployment-policy.cjs 5d425d5c2f939d0f6beebee7ebb0c77146cb7e318535ba7286ec7081a4dc2269 2497 src/shared/deployment-policy.cjs
029e600229714d033c28e2dcb77817aa8269847001782ae0012960e83ffd183f 3057 src/shared/git-status.cjs 029e600229714d033c28e2dcb77817aa8269847001782ae0012960e83ffd183f 3057 src/shared/git-status.cjs
@@ -133,7 +135,7 @@ e7aebcc0d484a6a59d463d5cb26c11b3ad56e28f6535e7c38a0fe166a41565ea 13690
caf98cbd9de9b119dae610ee53fa333a7a11214f34762247452fbb85e8bbf725 2392 tests/log-redaction.test.mjs caf98cbd9de9b119dae610ee53fa333a7a11214f34762247452fbb85e8bbf725 2392 tests/log-redaction.test.mjs
96432a97d313f331694900bf0a2c21e38c20eac96d59147977aeed9055a9e3ad 2287 tests/partial-staging.test.mjs 96432a97d313f331694900bf0a2c21e38c20eac96d59147977aeed9055a9e3ad 2287 tests/partial-staging.test.mjs
c0f8f5a3784835f19d9ff1015185ccb385840b6fa1c9ec19f233393a7d952b65 3718 tests/preflight.test.mjs c0f8f5a3784835f19d9ff1015185ccb385840b6fa1c9ec19f233393a7d952b65 3718 tests/preflight.test.mjs
128a3b87c26db9edd4413d6ab8c06bdfada311d2135d342ea298ebea89e40712 7061 tests/renderer-workflow.test.mjs bc29c4e0c08305b2d00bf59ae9d486cad323c94879cf2a35d596a6b58fc17a0b 7784 tests/renderer-workflow.test.mjs
2b4956fa4df4624a04117737e57ba74020564330ff71303b5746d8ccc881e880 854 tests/repository-matching.test.mjs 2b4956fa4df4624a04117737e57ba74020564330ff71303b5746d8ccc881e880 854 tests/repository-matching.test.mjs
f679072548554a64974f0452337ce5e7b0c567343c287223770cc0974b905348 1068 tests/repository-monitor.test.mjs f679072548554a64974f0452337ce5e7b0c567343c287223770cc0974b905348 1068 tests/repository-monitor.test.mjs
75b5b83836c75675bb9a48fe4363fcb8a24fc425e6af6f822d7955c6f3c79eac 2265 tests/repository-service.test.mjs 75b5b83836c75675bb9a48fe4363fcb8a24fc425e6af6f822d7955c6f3c79eac 2265 tests/repository-service.test.mjs
@@ -142,7 +144,7 @@ bab853feb0e22aa25af17989baaa632c01efa636533ea67407fecfdd973c7024 627
020eccfa9c4aef7a4ac4736d9af90518fcb6d1ad75aedcfaa1c92832a9e3d6d8 4609 tests/shell-verification.test.mjs 020eccfa9c4aef7a4ac4736d9af90518fcb6d1ad75aedcfaa1c92832a9e3d6d8 4609 tests/shell-verification.test.mjs
8a6a8477eb94b85ccef18cddd2640afb0d1eafa679c96bc7de20428d5d69e1be 1794 tests/tool-invocation.test.mjs 8a6a8477eb94b85ccef18cddd2640afb0d1eafa679c96bc7de20428d5d69e1be 1794 tests/tool-invocation.test.mjs
05c791ea262aef85a7c79874ed900c5d792a78778c577cb803b88a433cd6e2b6 21413 tests/unraid-deployment.test.mjs 05c791ea262aef85a7c79874ed900c5d792a78778c577cb803b88a433cd6e2b6 21413 tests/unraid-deployment.test.mjs
0c49d3222ea362dbef171f5ad556a335bad670b47adef660ede101d84814d05e 14849 tests/update-service.test.mjs f1f0f13ac41f47c9df8ef778d7b98c09a589f61e6f76e14bc0e8943c5ede7560 14990 tests/update-service.test.mjs
9cea5c1d5ba3e0972a0b5c7236cf1f7c5616373e0a39ea4a492ecebf70452e40 948 tests/validation.test.mjs 9cea5c1d5ba3e0972a0b5c7236cf1f7c5616373e0a39ea4a492ecebf70452e40 948 tests/validation.test.mjs
7ef4d4b9f5f3e6979293b29d571ce0e39f83197f3cade2d999a9cea7bacdd84d 1781 tests/zip-writer.test.mjs 7ef4d4b9f5f3e6979293b29d571ce0e39f83197f3cade2d999a9cea7bacdd84d 1781 tests/zip-writer.test.mjs
8f36b542736f2933bad8b9464ad7fa37b68196009c81cf702ce3b677cd637dea 767 UPDATE_FROM_0.3.2.md 8f36b542736f2933bad8b9464ad7fa37b68196009c81cf702ce3b677cd637dea 767 UPDATE_FROM_0.3.2.md
+10
View File
@@ -0,0 +1,10 @@
# ForgeFlow 0.8.4
ForgeFlow 0.8.4 adds interactive project illustrations where they reinforce the
release story. The overview now visualizes commits travelling through a release
topology, with animated status nodes and cursor-responsive depth.
Repository headers reuse the same visual language as a subtle animated
watermark. Illustrations adapt to light and dark themes, collapse gracefully at
compact widths and disable non-essential movement when reduced motion is
requested by the operating system.
+11
View File
@@ -0,0 +1,11 @@
# ForgeFlow 0.8.5
ForgeFlow 0.8.5 fixes packaged update downloads for Gitea instances whose
release metadata reports a public asset URL with a different scheme or origin.
The updater now ignores that mutable browser URL and downloads each release
asset through its immutable asset ID on the configured, trusted Gitea origin.
Authentication tokens remain protected and are never sent to another host.
Install 0.8.5 manually when upgrading from 0.8.4 because the affected download
path runs before the new updater code can be installed.
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "forgeflow", "name": "forgeflow",
"version": "0.8.3", "version": "0.8.5",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "forgeflow", "name": "forgeflow",
"version": "0.8.3", "version": "0.8.5",
"dependencies": { "dependencies": {
"ssh2": "1.17.0" "ssh2": "1.17.0"
}, },
+3 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "forgeflow", "name": "forgeflow",
"version": "0.8.3", "version": "0.8.5",
"private": true, "private": true,
"description": "Desktop release cockpit for local Git, Gitea Actions and controlled exact-commit deployments.", "description": "Desktop release cockpit for local Git, Gitea Actions and controlled exact-commit deployments.",
"main": "main.cjs", "main": "main.cjs",
@@ -76,6 +76,8 @@
"docs/RELEASE_NOTES_0.8.1.md", "docs/RELEASE_NOTES_0.8.1.md",
"docs/RELEASE_NOTES_0.8.2.md", "docs/RELEASE_NOTES_0.8.2.md",
"docs/RELEASE_NOTES_0.8.3.md", "docs/RELEASE_NOTES_0.8.3.md",
"docs/RELEASE_NOTES_0.8.4.md",
"docs/RELEASE_NOTES_0.8.5.md",
"docs/ACCEPTANCE.md" "docs/ACCEPTANCE.md"
], ],
"asarUnpack": [ "asarUnpack": [
+4 -2
View File
@@ -64,6 +64,8 @@ const required = [
"docs/RELEASE_NOTES_0.8.1.md", "docs/RELEASE_NOTES_0.8.1.md",
"docs/RELEASE_NOTES_0.8.2.md", "docs/RELEASE_NOTES_0.8.2.md",
"docs/RELEASE_NOTES_0.8.3.md", "docs/RELEASE_NOTES_0.8.3.md",
"docs/RELEASE_NOTES_0.8.4.md",
"docs/RELEASE_NOTES_0.8.5.md",
"docs/UPDATING.md", "docs/UPDATING.md",
"docs/DIAGNOSTICS.md", "docs/DIAGNOSTICS.md",
"docs/DEPLOYMENT_SETUP.md", "docs/DEPLOYMENT_SETUP.md",
@@ -102,9 +104,9 @@ for (const file of required) await access(path.join(root, file));
const packageJson = JSON.parse( const packageJson = JSON.parse(
await readFile(path.join(root, "package.json"), "utf8"), await readFile(path.join(root, "package.json"), "utf8"),
); );
if (packageJson.version !== "0.8.3") if (packageJson.version !== "0.8.5")
throw new Error( throw new Error(
`Expected package version 0.8.3, got ${packageJson.version}.`, `Expected package version 0.8.5, got ${packageJson.version}.`,
); );
const sourceManifest = await readFile( const sourceManifest = await readFile(
path.join(root, "SOURCE_MANIFEST.txt"), path.join(root, "SOURCE_MANIFEST.txt"),
+8
View File
@@ -331,6 +331,14 @@ class GiteaService {
throw new Error("The update download exceeded the redirect limit."); throw new Error("The update download exceeded the redirect limit.");
} }
async downloadReleaseAsset(owner, repo, assetId, options = {}) {
const numericId = Number(assetId);
if (!Number.isSafeInteger(numericId) || numericId <= 0)
throw new Error("Gitea returned an invalid release asset ID.");
const assetPath = `/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/assets/${numericId}`;
return this.downloadAuthenticated(assetPath, options);
}
async dispatchWorkflow({ owner, repo, workflowFile, ref, inputs = {} }) { async dispatchWorkflow({ owner, repo, workflowFile, ref, inputs = {} }) {
const result = await this.request( const result = await this.request(
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/actions/workflows/${encodeURIComponent(workflowFile)}/dispatches`, `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/actions/workflows/${encodeURIComponent(workflowFile)}/dispatches`,
+11 -4
View File
@@ -291,7 +291,7 @@ class UpdateService {
const assets = Array.isArray(release.assets) ? release.assets : []; const assets = Array.isArray(release.assets) ? release.assets : [];
const asset = assets.find((item) => item.name === assetName); const asset = assets.find((item) => item.name === assetName);
const checksumAsset = assets.find((item) => item.name === checksumName); const checksumAsset = assets.find((item) => item.name === checksumName);
if (!asset?.browser_download_url || !checksumAsset?.browser_download_url) { if (!asset?.id || !checksumAsset?.id) {
const error = new Error( const error = new Error(
`Release v${update.remoteVersion} is missing ${assetName} or its SHA-256 file.`, `Release v${update.remoteVersion} is missing ${assetName} or its SHA-256 file.`,
); );
@@ -300,8 +300,12 @@ class UpdateService {
} }
const [binary, checksumBytes] = await Promise.all([ const [binary, checksumBytes] = await Promise.all([
this.gitea.downloadAuthenticated(asset.browser_download_url), this.gitea.downloadReleaseAsset(update.owner, update.repo, asset.id),
this.gitea.downloadAuthenticated(checksumAsset.browser_download_url), this.gitea.downloadReleaseAsset(
update.owner,
update.repo,
checksumAsset.id,
),
]); ]);
if (binary.length < 1_000_000 || binary[0] !== 0x4d || binary[1] !== 0x5a) { if (binary.length < 1_000_000 || binary[0] !== 0x4d || binary[1] !== 0x5a) {
throw new Error( throw new Error(
@@ -639,7 +643,10 @@ class UpdateService {
.catch(() => []); .catch(() => []);
const candidates = []; const candidates = [];
for (const entry of entries) { for (const entry of entries) {
if (!entry.isFile() || !/^(?:apply|binary)-.*\.status\.json$/i.test(entry.name)) if (
!entry.isFile() ||
!/^(?:apply|binary)-.*\.status\.json$/i.test(entry.name)
)
continue; continue;
const filePath = path.join(this.updateDirectory, entry.name); const filePath = path.join(this.updateDirectory, entry.name);
const stat = await fs.stat(filePath).catch(() => null); const stat = await fs.stat(filePath).catch(() => null);
+36 -2
View File
@@ -699,6 +699,19 @@ function queueActionFor(repository) {
return mapping[action.kind] || mapping.clean; return mapping[action.kind] || mapping.clean;
} }
function projectIllustration(kind = "flow") {
return `<div class="project-illustration ${attr(kind)}" data-project-illustration aria-hidden="true">
<div class="illustration-glow"></div><svg viewBox="0 0 260 150" role="presentation">
<path class="orbit orbit-a" d="M32 92 C72 20 190 18 230 82"/><path class="orbit orbit-b" d="M42 116 C98 150 190 136 222 60"/>
<g class="illustration-core"><rect x="83" y="39" width="94" height="74" rx="17"/><path d="M103 66h54M103 79h36M103 92h45"/><circle cx="160" cy="92" r="5"/></g>
<g class="illustration-node node-a"><circle cx="37" cy="92" r="12"/><path d="m32 92 4 4 7-9"/></g>
<g class="illustration-node node-b"><circle cx="226" cy="82" r="12"/><path d="M221 82h10M226 77v10"/></g>
<g class="illustration-node node-c"><circle cx="74" cy="31" r="8"/></g>
<circle class="signal signal-a" cx="0" cy="0" r="4"/><circle class="signal signal-b" cx="0" cy="0" r="3"/>
</svg><span class="illustration-label">${kind === "deploy" ? "Live release topology" : kind === "repo" ? "Project signal" : "Release flow"}</span>
</div>`;
}
function renderOverview() { function renderOverview() {
const changed = ui.repositories.filter( const changed = ui.repositories.filter(
(repository) => repository.localStatus?.counts.changed, (repository) => repository.localStatus?.counts.changed,
@@ -720,7 +733,7 @@ function renderOverview() {
(operation) => !isTerminalOperation(operation.status), (operation) => !isTerminalOperation(operation.status),
); );
return `<div class="page"> return `<div class="page">
<div class="page-header"><div><div class="eyebrow">Coding flow</div><h1>Release overview</h1><p>One decision surface for local work, Gitea synchronization and the exact version running on your server.</p></div><button class="button" data-action="refresh">${icon("refresh")}Refresh all</button></div> <div class="page-header visual-page-header"><div><div class="eyebrow">Coding flow</div><h1>Release overview</h1><p>One decision surface for local work, Gitea synchronization and the exact version running on your server.</p></div>${projectIllustration("flow")}<button class="button" data-action="refresh">${icon("refresh")}Refresh all</button></div>
${ui.refreshError ? `<div class="notice danger">${icon("error")} ${escapeHtml(ui.refreshError)}</div>` : ""} ${ui.refreshError ? `<div class="notice danger">${icon("error")} ${escapeHtml(ui.refreshError)}</div>` : ""}
<div class="summary-grid"> <div class="summary-grid">
${renderSummaryCard("Local work", changed, changed === 1 ? "repository has changes" : "repositories have changes", "file", changed ? "warning" : "success")} ${renderSummaryCard("Local work", changed, changed === 1 ? "repository has changes" : "repositories have changes", "file", changed ? "warning" : "success")}
@@ -951,7 +964,7 @@ function renderRepositoryWorkspace(repository) {
settings: renderRepositorySettings, settings: renderRepositorySettings,
}[ui.repositoryTab] || renderChanges }[ui.repositoryTab] || renderChanges
)(repository); )(repository);
return `<div class="repo-workspace"><header class="repo-header"><div class="repo-heading"><h1><button class="favorite-button ${repository.favorite ? "active" : ""}" data-action="toggle-favorite" title="Toggle favorite">${icon("star")}</button>${escapeHtml(repository.fullName)}</h1><p>${escapeHtml(repository.localPath || "No local working tree linked")}</p></div><div class="repo-header-actions"><button class="button" data-action="fetch" ${!repository.localPath ? "disabled" : ""}>${icon("refresh")}Fetch</button><button class="button" data-action="open-path" ${!repository.localPath ? "disabled" : ""}>${icon("folder")}Folder</button><button class="button" data-action="open-gitea" ${!repository.htmlUrl ? "disabled" : ""}>${icon("external")}Gitea</button></div></header> return `<div class="repo-workspace"><header class="repo-header illustrated-repo-header"><div class="repo-heading"><h1><button class="favorite-button ${repository.favorite ? "active" : ""}" data-action="toggle-favorite" title="Toggle favorite">${icon("star")}</button>${escapeHtml(repository.fullName)}</h1><p>${escapeHtml(repository.localPath || "No local working tree linked")}</p></div>${projectIllustration("repo")}<div class="repo-header-actions"><button class="button" data-action="fetch" ${!repository.localPath ? "disabled" : ""}>${icon("refresh")}Fetch</button><button class="button" data-action="open-path" ${!repository.localPath ? "disabled" : ""}>${icon("folder")}Folder</button><button class="button" data-action="open-gitea" ${!repository.htmlUrl ? "disabled" : ""}>${icon("external")}Gitea</button></div></header>
${repository.localPath ? `<div class="repo-quick-actions"><button class="button" data-action="open-editor">${icon("external")}Open in editor</button><button class="button" data-action="open-terminal">${icon("terminal")}Open terminal</button><button class="button" data-action="check-branch-protection">${icon("shield")}Check branch protection</button><button class="button primary" data-action="open-pull-request">${icon("git")}Create pull request</button>${ui.branchProtection ? `<span class="status-pill ${ui.branchProtection.protected ? "warning" : "success"}">${ui.branchProtection.protected ? `Protected · ${ui.branchProtection.requiredApprovals || 0} approval(s)` : "Direct pushes allowed"}</span>` : ""}</div>` : ""} ${repository.localPath ? `<div class="repo-quick-actions"><button class="button" data-action="open-editor">${icon("external")}Open in editor</button><button class="button" data-action="open-terminal">${icon("terminal")}Open terminal</button><button class="button" data-action="check-branch-protection">${icon("shield")}Check branch protection</button><button class="button primary" data-action="open-pull-request">${icon("git")}Create pull request</button>${ui.branchProtection ? `<span class="status-pill ${ui.branchProtection.protected ? "warning" : "success"}">${ui.branchProtection.protected ? `Protected · ${ui.branchProtection.requiredApprovals || 0} approval(s)` : "Direct pushes allowed"}</span>` : ""}</div>` : ""}
<div class="release-rail">${releaseNode("Local", status?.shortHead || "Not linked", status ? `${status.counts.changed} changes · ${status.branch.head}` : "No working tree", localTone)}${releaseNode("Gitea", status?.shortHead || "Unknown", status?.branch.upstream ? `${status.branch.ahead} ahead · ${status.branch.behind} behind` : "Branch not published", remoteTone)}${releaseNode(`Server${profile ? ` · ${profile.environment}` : ""}`, serverState.liveSha ? shortSha(serverState.liveSha) : "Unknown", profile ? (serverState.checkedAt ? `checked ${formatDate(serverState.checkedAt)}` : "not checked") : "No deployment profile", serverTone)}</div> <div class="release-rail">${releaseNode("Local", status?.shortHead || "Not linked", status ? `${status.counts.changed} changes · ${status.branch.head}` : "No working tree", localTone)}${releaseNode("Gitea", status?.shortHead || "Unknown", status?.branch.upstream ? `${status.branch.ahead} ahead · ${status.branch.behind} behind` : "Branch not published", remoteTone)}${releaseNode(`Server${profile ? ` · ${profile.environment}` : ""}`, serverState.liveSha ? shortSha(serverState.liveSha) : "Unknown", profile ? (serverState.checkedAt ? `checked ${formatDate(serverState.checkedAt)}` : "not checked") : "No deployment profile", serverTone)}</div>
<nav class="tabs">${[ <nav class="tabs">${[
@@ -3244,6 +3257,27 @@ document.addEventListener("keydown", (event) => {
} }
}); });
document.addEventListener("pointermove", (event) => {
const illustration = event.target.closest?.("[data-project-illustration]");
if (!illustration) return;
const bounds = illustration.getBoundingClientRect();
illustration.style.setProperty(
"--tilt-x",
`${((event.clientY - bounds.top) / bounds.height - 0.5) * -7}deg`,
);
illustration.style.setProperty(
"--tilt-y",
`${((event.clientX - bounds.left) / bounds.width - 0.5) * 9}deg`,
);
});
document.addEventListener("pointerout", (event) => {
const illustration = event.target.closest?.("[data-project-illustration]");
if (!illustration || illustration.contains(event.relatedTarget)) return;
illustration.style.removeProperty("--tilt-x");
illustration.style.removeProperty("--tilt-y");
});
window.addEventListener("error", (event) => { window.addEventListener("error", (event) => {
window.forgeflow window.forgeflow
.reportRendererEvent?.("error", "uncaught-error", { .reportRendererEvent?.("error", "uncaught-error", {
+1 -1
View File
@@ -564,7 +564,7 @@
await wait(80); await wait(80);
snapshot(); snapshot();
return { return {
appVersion: "0.8.3-demo", appVersion: "0.8.5-demo",
platform: "win32", platform: "win32",
state: clone(state), state: clone(state),
git: { available: true, version: "git version 2.47.3" }, git: { available: true, version: "git version 2.47.3" },
+201
View File
@@ -3205,6 +3205,197 @@ select:focus {
backdrop-filter: blur(16px); backdrop-filter: blur(16px);
} }
.visual-page-header {
min-height: 142px;
position: relative;
overflow: hidden;
padding: 20px 22px;
border: 1px solid color-mix(in srgb, var(--primary) 22%, var(--line));
border-radius: 16px;
background:
linear-gradient(
105deg,
var(--surface-1) 0 52%,
color-mix(in srgb, var(--primary-soft) 62%, var(--surface-1))
),
var(--surface-1);
box-shadow: var(--shadow);
}
.project-illustration {
--tilt-x: 0deg;
--tilt-y: 0deg;
width: 238px;
height: 128px;
position: relative;
flex: 0 0 238px;
perspective: 700px;
cursor: crosshair;
isolation: isolate;
}
.project-illustration svg {
width: 100%;
height: 100%;
position: relative;
z-index: 1;
overflow: visible;
transform: rotateX(var(--tilt-x)) rotateY(var(--tilt-y));
filter: drop-shadow(0 16px 24px rgba(10, 20, 48, 0.2));
transition: transform 140ms ease-out;
}
.illustration-glow {
width: 150px;
height: 85px;
position: absolute;
inset: 24px auto auto 49px;
border-radius: 50%;
background: color-mix(in srgb, var(--primary) 24%, transparent);
filter: blur(24px);
animation: illustration-breathe 4.8s ease-in-out infinite;
}
.project-illustration .orbit {
fill: none;
stroke: color-mix(in srgb, var(--primary) 48%, var(--line));
stroke-width: 1.5;
stroke-dasharray: 4 7;
}
.project-illustration .orbit-b {
stroke: color-mix(in srgb, var(--success) 48%, var(--line));
}
.illustration-core rect {
fill: color-mix(in srgb, var(--surface-1) 86%, var(--primary));
stroke: color-mix(in srgb, var(--primary) 58%, var(--line));
stroke-width: 1.5;
}
.illustration-core path {
fill: none;
stroke: var(--text-muted);
stroke-width: 3;
stroke-linecap: round;
}
.illustration-core circle {
fill: var(--success);
animation: illustration-pulse 2.4s ease-in-out infinite;
}
.illustration-node {
animation: illustration-float 4s ease-in-out infinite;
}
.illustration-node circle {
fill: var(--surface-1);
stroke: var(--primary);
stroke-width: 2;
}
.illustration-node path {
fill: none;
stroke: var(--primary);
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
.illustration-node.node-b {
animation-delay: -1.4s;
}
.illustration-node.node-c {
animation-delay: -2.7s;
}
.illustration-node.node-c circle {
fill: var(--warning);
stroke: color-mix(in srgb, var(--warning) 55%, var(--surface-1));
}
.project-illustration .signal {
fill: var(--primary);
}
.project-illustration .signal-a {
offset-path: path("M32 92 C72 20 190 18 230 82");
animation: signal-travel 3.6s linear infinite;
}
.project-illustration .signal-b {
fill: var(--success);
offset-path: path("M42 116 C98 150 190 136 222 60");
animation: signal-travel 4.4s -2s linear infinite;
}
.illustration-label {
position: absolute;
right: 8px;
bottom: 3px;
z-index: 2;
color: var(--text-faint);
font: 650 9px var(--font-mono);
letter-spacing: 0.06em;
text-transform: uppercase;
}
.project-illustration.repo {
width: 150px;
height: 82px;
position: absolute;
top: -9px;
left: 43%;
z-index: 0;
margin: 0;
opacity: 0.28;
pointer-events: none;
}
.project-illustration.repo .illustration-label {
display: none;
}
.illustrated-repo-header {
position: relative;
overflow: hidden;
}
.illustrated-repo-header > :not(.project-illustration) {
position: relative;
z-index: 1;
}
@keyframes signal-travel {
to {
offset-distance: 100%;
}
}
@keyframes illustration-float {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-5px);
}
}
@keyframes illustration-breathe {
0%,
100% {
opacity: 0.55;
transform: scale(0.92);
}
50% {
opacity: 0.95;
transform: scale(1.08);
}
}
@keyframes illustration-pulse {
0%,
100% {
opacity: 0.55;
}
50% {
opacity: 1;
}
}
html[data-theme="light"] .visual-page-header {
background:
radial-gradient(
circle at 74% 20%,
rgba(102, 82, 235, 0.12),
transparent 28%
),
linear-gradient(
105deg,
rgba(255, 255, 255, 0.96) 0 52%,
rgba(229, 239, 255, 0.94)
);
}
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
*, *,
*::before, *::before,
@@ -3214,6 +3405,9 @@ select:focus {
animation-duration: 0.01ms !important; animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important; animation-iteration-count: 1 !important;
} }
.project-illustration svg {
transform: none !important;
}
} }
@media (max-width: 1180px) { @media (max-width: 1180px) {
@@ -3227,4 +3421,11 @@ select:focus {
.summary-grid { .summary-grid {
gap: 8px; gap: 8px;
} }
.project-illustration {
width: 190px;
flex-basis: 190px;
}
.project-illustration.repo {
display: none;
}
} }
+22 -1
View File
@@ -182,7 +182,10 @@ test("one-click troubleshooting excludes destructive or publishing Git actions",
assert.match(ipc, /action:\s*["']abort-operation["'],\s*safe:\s*false/); assert.match(ipc, /action:\s*["']abort-operation["'],\s*safe:\s*false/);
assert.match(ipc, /action:\s*["']push["'],\s*safe:\s*false/); assert.match(ipc, /action:\s*["']push["'],\s*safe:\s*false/);
assert.match(ipc, /const stale = lock\.ageMs >= 10_000/); assert.match(ipc, /const stale = lock\.ageMs >= 10_000/);
assert.match(ipc, /\[\s*["']fast-forward["'],\s*["']fetch["']\s*\]\.includes\(issue\.action\)/); assert.match(
ipc,
/\[\s*["']fast-forward["'],\s*["']fetch["']\s*\]\.includes\(issue\.action\)/,
);
assert.doesNotMatch( assert.doesNotMatch(
ipc, ipc,
/\[\s*["']fast-forward["'],\s*["']push["'],\s*["']fetch["']\s*\]\.includes\(issue\.action\)/, /\[\s*["']fast-forward["'],\s*["']push["'],\s*["']fetch["']\s*\]\.includes\(issue\.action\)/,
@@ -205,3 +208,21 @@ test("premium repository workspace reserves separate rows for actions and releas
assert.match(styles, /prefers-reduced-motion/); assert.match(styles, /prefers-reduced-motion/);
assert.match(styles, /ForgeFlow 0\.8 premium visual system/); assert.match(styles, /ForgeFlow 0\.8 premium visual system/);
}); });
test("interactive project illustrations are semantic, responsive and motion-safe", async () => {
const renderer = await readFile(
new URL("../src/renderer/app.js", import.meta.url),
"utf8",
);
const styles = await readFile(
new URL("../src/renderer/styles.css", import.meta.url),
"utf8",
);
assert.match(renderer, /function projectIllustration/);
assert.match(renderer, /data-project-illustration/);
assert.match(renderer, /document\.addEventListener\("pointermove"/);
assert.match(styles, /\.project-illustration/);
assert.match(styles, /@keyframes signal-travel/);
assert.match(styles, /prefers-reduced-motion/);
assert.match(styles, /transform: none !important/);
});
+15 -10
View File
@@ -340,18 +340,21 @@ test("packaged updater downloads only a published checksum-matched Windows asset
draft: false, draft: false,
prerelease: false, prerelease: false,
assets: [ assets: [
{ name: assetName, browser_download_url: "https://gitea.test/setup" }, {
id: 41,
name: assetName,
browser_download_url: "http://wrong-origin.test/setup",
},
{ {
name: `${assetName}.sha256`, name: `${assetName}.sha256`,
browser_download_url: "https://gitea.test/checksum", id: 42,
browser_download_url: "http://wrong-origin.test/checksum",
}, },
], ],
}; };
}, },
async downloadAuthenticated(url) { async downloadReleaseAsset(_owner, _repo, assetId) {
return url.endsWith("/checksum") return assetId === 42 ? Buffer.from(`${sha256} ${assetName}\n`) : binary;
? Buffer.from(`${sha256} ${assetName}\n`)
: binary;
}, },
}; };
const service = new UpdateService({ const service = new UpdateService({
@@ -398,18 +401,20 @@ test("packaged updater rejects a binary whose checksum does not match", async ()
tag_name: "v0.8.2", tag_name: "v0.8.2",
assets: [ assets: [
{ {
id: 51,
name: assetName, name: assetName,
browser_download_url: "https://gitea.test/portable", browser_download_url: "http://wrong-origin.test/portable",
}, },
{ {
id: 52,
name: `${assetName}.sha256`, name: `${assetName}.sha256`,
browser_download_url: "https://gitea.test/checksum", browser_download_url: "http://wrong-origin.test/checksum",
}, },
], ],
}; };
}, },
async downloadAuthenticated(url) { async downloadReleaseAsset(_owner, _repo, assetId) {
return url.endsWith("/checksum") return assetId === 52
? Buffer.from(`${"0".repeat(64)} ${assetName}`) ? Buffer.from(`${"0".repeat(64)} ${assetName}`)
: binary; : binary;
}, },