mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-02-13 14:17:44 +01:00
Compare commits
104 Commits
omnara/rep
...
fix/git-pu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f27d500a2c | ||
|
|
d84fec8c7c | ||
|
|
52732e12ec | ||
|
|
1127d7e3fa | ||
|
|
7d4d228236 | ||
|
|
565e053ee8 | ||
|
|
26aba6034f | ||
|
|
9a1d613034 | ||
|
|
3e4de7d3c4 | ||
|
|
b64b5ec0f8 | ||
|
|
510d1c7d17 | ||
|
|
ed13a62269 | ||
|
|
935d613959 | ||
|
|
adeaaccc45 | ||
|
|
d253093333 | ||
|
|
f265b7a572 | ||
|
|
68b2ff016f | ||
|
|
a1c6295810 | ||
|
|
76ee3fa61b | ||
|
|
7fef35ce0a | ||
|
|
654af09951 | ||
|
|
484dcfade0 | ||
|
|
fda18c5434 | ||
|
|
a8176d6e9e | ||
|
|
957d8d9d46 | ||
|
|
5f18bf25e2 | ||
|
|
66942eaf2c | ||
|
|
38796b1833 | ||
|
|
49ffa6fc45 | ||
|
|
1f56ba2eb6 | ||
|
|
f98a70ecb4 | ||
|
|
2984eb40c9 | ||
|
|
cc5d4742f0 | ||
|
|
5b8e4b98a0 | ||
|
|
8637c90a21 | ||
|
|
b88c5e71a0 | ||
|
|
1899d512ab | ||
|
|
7c31718f5e | ||
|
|
8f1463e5d0 | ||
|
|
0dc8807808 | ||
|
|
f24a159b8a | ||
|
|
0b91d3aaff | ||
|
|
431dc1c896 | ||
|
|
bc8277b56b | ||
|
|
0afed185d9 | ||
|
|
55cee00601 | ||
|
|
b41a8e04cb | ||
|
|
eff4519d91 | ||
|
|
c4ce458f79 | ||
|
|
f02ae35634 | ||
|
|
c2f068970b | ||
|
|
eec2d6bc38 | ||
|
|
efa22e470e | ||
|
|
c00d2e981f | ||
|
|
9c45254952 | ||
|
|
d031ff231a | ||
|
|
f056894ddb | ||
|
|
1b0315165f | ||
|
|
bd7e840a57 | ||
|
|
8969748c3c | ||
|
|
4e15ac10a6 | ||
|
|
47a3d44888 | ||
|
|
eb10910d20 | ||
|
|
6ba83d424d | ||
|
|
beb47a6b6a | ||
|
|
1893b8f8dd | ||
|
|
7a5bca7aae | ||
|
|
9a75bc2ae7 | ||
|
|
65514e3882 | ||
|
|
9ddaafb79f | ||
|
|
de47ee19ec | ||
|
|
ea730d0184 | ||
|
|
fe706998d4 | ||
|
|
99209e088f | ||
|
|
3eb29ff2fe | ||
|
|
b759003c83 | ||
|
|
6cba38ac89 | ||
|
|
ba8f85baaf | ||
|
|
9970d5fa6f | ||
|
|
d550b42ca3 | ||
|
|
2e1f0cb53f | ||
|
|
eead422ada | ||
|
|
b5753da3b7 | ||
|
|
ae2f2459e9 | ||
|
|
306e6f358a | ||
|
|
822d52a57e | ||
|
|
e665ce04df | ||
|
|
e4828e1b17 | ||
|
|
42143249a2 | ||
|
|
72a7e6963d | ||
|
|
494e9efb64 | ||
|
|
9fe077f598 | ||
|
|
a6eca1cf2e | ||
|
|
31edd1013f | ||
|
|
28e9657ea5 | ||
|
|
ff084a224a | ||
|
|
bbcae34575 | ||
|
|
2a5587c128 | ||
|
|
c41e173a63 | ||
|
|
2b43407ddf | ||
|
|
4d75b8ef06 | ||
|
|
aa79fb05f9 | ||
|
|
fe01796536 | ||
|
|
6654d6c346 |
@@ -37,3 +37,13 @@ The skill generates markdown-formatted release notes following this structure:
|
||||
|
||||
**IMPORTANT**: Always add a blank lines around the markdown code fence and output the markdown code block last
|
||||
**IMPORTANT**: PRs by `@gschier` should not mention the @username
|
||||
|
||||
## After Generating Release Notes
|
||||
|
||||
After outputting the release notes, ask the user if they would like to create a draft GitHub release with these notes. If they confirm, create the release using:
|
||||
|
||||
```bash
|
||||
gh release create <tag> --draft --prerelease --title "Release <version>" --notes '<release notes>'
|
||||
```
|
||||
|
||||
**IMPORTANT**: The release title format is "Release XXXX" where XXXX is the version WITHOUT the `v` prefix. For example, tag `v2026.2.1-beta.1` gets title "Release 2026.2.1-beta.1".
|
||||
|
||||
52
.github/workflows/flathub.yml
vendored
Normal file
52
.github/workflows/flathub.yml
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
name: Update Flathub
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
update-flathub:
|
||||
name: Update Flathub manifest
|
||||
runs-on: ubuntu-latest
|
||||
# Only run for stable releases (skip betas/pre-releases)
|
||||
if: ${{ !github.event.release.prerelease }}
|
||||
steps:
|
||||
- name: Checkout app repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Checkout Flathub repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: flathub/app.yaak.Yaak
|
||||
token: ${{ secrets.FLATHUB_TOKEN }}
|
||||
path: flathub-repo
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
- name: Install source generators
|
||||
run: |
|
||||
pip install flatpak-node-generator tomlkit aiohttp
|
||||
git clone --depth 1 https://github.com/flatpak/flatpak-builder-tools flatpak/flatpak-builder-tools
|
||||
|
||||
- name: Run update-manifest.sh
|
||||
run: bash flatpak/update-manifest.sh "${{ github.event.release.tag_name }}" flathub-repo
|
||||
|
||||
- name: Commit and push to Flathub
|
||||
working-directory: flathub-repo
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add -A
|
||||
git diff --cached --quiet && echo "No changes to commit" && exit 0
|
||||
git commit -m "Update to ${{ github.event.release.tag_name }}"
|
||||
git push
|
||||
96
.github/workflows/release.yml
vendored
96
.github/workflows/release.yml
vendored
@@ -1,7 +1,7 @@
|
||||
name: Generate Artifacts
|
||||
on:
|
||||
push:
|
||||
tags: [ v* ]
|
||||
tags: [v*]
|
||||
|
||||
jobs:
|
||||
build-artifacts:
|
||||
@@ -13,37 +13,37 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: 'macos-latest' # for Arm-based Macs (M1 and above).
|
||||
args: '--target aarch64-apple-darwin'
|
||||
yaak_arch: 'arm64'
|
||||
os: 'macos'
|
||||
targets: 'aarch64-apple-darwin'
|
||||
- platform: 'macos-latest' # for Intel-based Macs.
|
||||
args: '--target x86_64-apple-darwin'
|
||||
yaak_arch: 'x64'
|
||||
os: 'macos'
|
||||
targets: 'x86_64-apple-darwin'
|
||||
- platform: 'ubuntu-22.04'
|
||||
args: ''
|
||||
yaak_arch: 'x64'
|
||||
os: 'ubuntu'
|
||||
targets: ''
|
||||
- platform: 'ubuntu-22.04-arm'
|
||||
args: ''
|
||||
yaak_arch: 'arm64'
|
||||
os: 'ubuntu'
|
||||
targets: ''
|
||||
- platform: 'windows-latest'
|
||||
args: ''
|
||||
yaak_arch: 'x64'
|
||||
os: 'windows'
|
||||
targets: ''
|
||||
- platform: "macos-latest" # for Arm-based Macs (M1 and above).
|
||||
args: "--target aarch64-apple-darwin"
|
||||
yaak_arch: "arm64"
|
||||
os: "macos"
|
||||
targets: "aarch64-apple-darwin"
|
||||
- platform: "macos-latest" # for Intel-based Macs.
|
||||
args: "--target x86_64-apple-darwin"
|
||||
yaak_arch: "x64"
|
||||
os: "macos"
|
||||
targets: "x86_64-apple-darwin"
|
||||
- platform: "ubuntu-22.04"
|
||||
args: ""
|
||||
yaak_arch: "x64"
|
||||
os: "ubuntu"
|
||||
targets: ""
|
||||
- platform: "ubuntu-22.04-arm"
|
||||
args: ""
|
||||
yaak_arch: "arm64"
|
||||
os: "ubuntu"
|
||||
targets: ""
|
||||
- platform: "windows-latest"
|
||||
args: ""
|
||||
yaak_arch: "x64"
|
||||
os: "windows"
|
||||
targets: ""
|
||||
# Windows ARM64
|
||||
- platform: 'windows-latest'
|
||||
args: '--target aarch64-pc-windows-msvc'
|
||||
yaak_arch: 'arm64'
|
||||
os: 'windows'
|
||||
targets: 'aarch64-pc-windows-msvc'
|
||||
- platform: "windows-latest"
|
||||
args: "--target aarch64-pc-windows-msvc"
|
||||
yaak_arch: "arm64"
|
||||
os: "windows"
|
||||
targets: "aarch64-pc-windows-msvc"
|
||||
runs-on: ${{ matrix.platform }}
|
||||
timeout-minutes: 40
|
||||
steps:
|
||||
@@ -88,6 +88,9 @@ jobs:
|
||||
& $exe --version
|
||||
|
||||
- run: npm ci
|
||||
- run: npm run bootstrap
|
||||
env:
|
||||
YAAK_TARGET_ARCH: ${{ matrix.yaak_arch }}
|
||||
- run: npm run lint
|
||||
- name: Run JS Tests
|
||||
run: npm test
|
||||
@@ -99,6 +102,29 @@ jobs:
|
||||
env:
|
||||
YAAK_VERSION: ${{ github.ref_name }}
|
||||
|
||||
- name: Sign vendored binaries (macOS only)
|
||||
if: matrix.os == 'macos'
|
||||
env:
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
|
||||
run: |
|
||||
# Create keychain
|
||||
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
|
||||
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
|
||||
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||
|
||||
# Import certificate
|
||||
echo "$APPLE_CERTIFICATE" | base64 --decode > certificate.p12
|
||||
security import certificate.p12 -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
|
||||
security list-keychain -d user -s $KEYCHAIN_PATH
|
||||
|
||||
# Sign vendored binaries with hardened runtime and their specific entitlements
|
||||
codesign --force --options runtime --entitlements crates-tauri/yaak-app/macos/entitlements.yaakprotoc.plist --sign "$APPLE_SIGNING_IDENTITY" crates-tauri/yaak-app/vendored/protoc/yaakprotoc || true
|
||||
codesign --force --options runtime --entitlements crates-tauri/yaak-app/macos/entitlements.yaaknode.plist --sign "$APPLE_SIGNING_IDENTITY" crates-tauri/yaak-app/vendored/node/yaaknode || true
|
||||
|
||||
- uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
YAAK_TARGET_ARCH: ${{ matrix.yaak_arch }}
|
||||
@@ -121,9 +147,9 @@ jobs:
|
||||
AZURE_CLIENT_SECRET: ${{ matrix.os == 'windows' && secrets.AZURE_CLIENT_SECRET }}
|
||||
AZURE_TENANT_ID: ${{ matrix.os == 'windows' && secrets.AZURE_TENANT_ID }}
|
||||
with:
|
||||
tagName: 'v__VERSION__'
|
||||
releaseName: 'Release __VERSION__'
|
||||
releaseBody: '[Changelog __VERSION__](https://yaak.app/blog/__VERSION__)'
|
||||
tagName: "v__VERSION__"
|
||||
releaseName: "Release __VERSION__"
|
||||
releaseBody: "[Changelog __VERSION__](https://yaak.app/blog/__VERSION__)"
|
||||
releaseDraft: true
|
||||
prerelease: true
|
||||
args: '${{ matrix.args }} --config ./crates-tauri/yaak-app/tauri.release.conf.json'
|
||||
args: "${{ matrix.args }} --config ./crates-tauri/yaak-app/tauri.release.conf.json"
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -44,3 +44,10 @@ crates-tauri/yaak-app/tauri.worktree.conf.json
|
||||
# Tauri auto-generated permission files
|
||||
**/permissions/autogenerated
|
||||
**/permissions/schemas
|
||||
|
||||
# Flatpak build artifacts
|
||||
flatpak-repo/
|
||||
.flatpak-builder/
|
||||
flatpak/flatpak-builder-tools/
|
||||
flatpak/cargo-sources.json
|
||||
flatpak/node-sources.json
|
||||
|
||||
362
Cargo.lock
generated
362
Cargo.lock
generated
@@ -52,6 +52,15 @@ dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "0.6.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81ce3d38065e618af2d7b77e10c5ad9a069859b4be3c2250f674af3840d9c8a5"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.3"
|
||||
@@ -90,7 +99,7 @@ checksum = "f6f39be698127218cca460cb624878c9aa4e2b47dba3b277963d2bf00bad263b"
|
||||
dependencies = [
|
||||
"android_log-sys",
|
||||
"env_filter",
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -175,7 +184,7 @@ checksum = "c1df21f715862ede32a0c525ce2ca4d52626bb0007f8c18b87a384503ac33e70"
|
||||
dependencies = [
|
||||
"clipboard-win",
|
||||
"image",
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
"objc2 0.6.1",
|
||||
"objc2-app-kit",
|
||||
"objc2-core-foundation",
|
||||
@@ -689,9 +698,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.10.1"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
|
||||
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@@ -999,7 +1008,7 @@ version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"lazy_static 1.5.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
@@ -1316,12 +1325,12 @@ checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b"
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.4.0"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
|
||||
checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587"
|
||||
dependencies = [
|
||||
"powerfmt",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1519,7 +1528,7 @@ dependencies = [
|
||||
"rustc_version",
|
||||
"toml 0.8.23",
|
||||
"vswhom",
|
||||
"winreg",
|
||||
"winreg 0.55.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1570,8 +1579,8 @@ version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"regex",
|
||||
"log 0.4.29",
|
||||
"regex 1.11.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1584,7 +1593,7 @@ dependencies = [
|
||||
"anstyle",
|
||||
"env_filter",
|
||||
"jiff",
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1645,7 +1654,7 @@ name = "eventsource-client"
|
||||
version = "0.14.0"
|
||||
source = "git+https://github.com/yaakapp/rust-eventsource-client#60e0e3ac5038149c4778dc4979b09b152214f9a8"
|
||||
dependencies = [
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
"pin-project",
|
||||
]
|
||||
|
||||
@@ -1693,7 +1702,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4316185f709b23713e41e3195f90edef7fb00c3ed4adc79769cf09cc762a3b29"
|
||||
dependencies = [
|
||||
"colored",
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1702,7 +1711,7 @@ version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f"
|
||||
dependencies = [
|
||||
"memoffset",
|
||||
"memoffset 0.9.1",
|
||||
"rustc_version",
|
||||
]
|
||||
|
||||
@@ -2136,14 +2145,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "git2"
|
||||
version = "0.20.2"
|
||||
version = "0.20.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2deb07a133b1520dc1a5690e9bd08950108873d7ed5de38dcc74d3b5ebffa110"
|
||||
checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"libc",
|
||||
"libgit2-sys",
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
"openssl-probe",
|
||||
"openssl-sys",
|
||||
"url",
|
||||
@@ -2284,6 +2293,21 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "handlebars"
|
||||
version = "0.29.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb04af2006ea09d985fef82b81e0eb25337e51b691c76403332378a53d521edc"
|
||||
dependencies = [
|
||||
"lazy_static 0.2.11",
|
||||
"log 0.3.9",
|
||||
"pest",
|
||||
"quick-error",
|
||||
"regex 0.2.11",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
@@ -2356,7 +2380,7 @@ version = "0.29.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c"
|
||||
dependencies = [
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
"mac",
|
||||
"markup5ever",
|
||||
"match_token",
|
||||
@@ -2517,7 +2541,7 @@ dependencies = [
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
"wasm-bindgen",
|
||||
"windows-core",
|
||||
]
|
||||
@@ -2758,6 +2782,22 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "interfaces"
|
||||
version = "0.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ec8f50a973916cac3da5057c986db05cd3346f38c78e9bc24f64cc9f6a3978f"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"cc",
|
||||
"handlebars",
|
||||
"lazy_static 1.5.0",
|
||||
"libc",
|
||||
"nix 0.23.2",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.11.0"
|
||||
@@ -2844,7 +2884,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e67e8da4c49d6d9909fe03361f9b620f58898859f5c7aded68351e85e71ecf50"
|
||||
dependencies = [
|
||||
"jiff-static",
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
"portable-atomic",
|
||||
"portable-atomic-util",
|
||||
"serde_core",
|
||||
@@ -2871,7 +2911,7 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"combine",
|
||||
"jni-sys",
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
"thiserror 1.0.69",
|
||||
"walkdir",
|
||||
"windows-sys 0.45.0",
|
||||
@@ -2950,7 +2990,7 @@ checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"dbus-secret-service",
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
"security-framework 2.11.1",
|
||||
"security-framework 3.5.1",
|
||||
"windows-sys 0.60.2",
|
||||
@@ -2989,6 +3029,12 @@ dependencies = [
|
||||
"selectors",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76f033c7ad61445c5b347c7382dd1237847eb1bce590fe50365dcb33d546be73"
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
@@ -3005,7 +3051,7 @@ dependencies = [
|
||||
"gtk",
|
||||
"gtk-sys",
|
||||
"libappindicator-sys",
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3036,9 +3082,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libgit2-sys"
|
||||
version = "0.18.1+1.9.0"
|
||||
version = "0.18.3+1.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1dcb20f84ffcdd825c7a311ae347cce604a6f084a767dec4a4929829645290e"
|
||||
checksum = "c9b3acc4b91781bb0b3386669d325163746af5f6e4f73e6d2d630e09a35f3487"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
@@ -3163,6 +3209,15 @@ dependencies = [
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b"
|
||||
dependencies = [
|
||||
"log 0.4.29",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.29"
|
||||
@@ -3199,7 +3254,7 @@ version = "0.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18"
|
||||
dependencies = [
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
"phf 0.11.3",
|
||||
"phf_codegen 0.11.3",
|
||||
"string_cache",
|
||||
@@ -3248,6 +3303,15 @@ version = "2.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
version = "0.6.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
version = "0.9.1"
|
||||
@@ -3302,7 +3366,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
@@ -3344,7 +3408,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
"openssl",
|
||||
"openssl-probe",
|
||||
"openssl-sys",
|
||||
@@ -3362,7 +3426,7 @@ checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"jni-sys",
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
"ndk-sys",
|
||||
"num_enum",
|
||||
"raw-window-handle",
|
||||
@@ -3390,6 +3454,19 @@ version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.23.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"memoffset 0.6.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.30.1"
|
||||
@@ -3400,7 +3477,7 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
"memoffset",
|
||||
"memoffset 0.9.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3431,7 +3508,7 @@ dependencies = [
|
||||
"inotify",
|
||||
"kqueue",
|
||||
"libc",
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
"mio",
|
||||
"notify-types",
|
||||
"walkdir",
|
||||
@@ -3446,9 +3523,9 @@ checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d"
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||
checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
@@ -3872,7 +3949,7 @@ version = "3.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41fc863e2ca13dc2d5c34fb22ea4a588248ac14db929616ba65c45f21744b1e9"
|
||||
dependencies = [
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
"serde",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
@@ -3912,7 +3989,7 @@ dependencies = [
|
||||
"des",
|
||||
"getrandom 0.2.16",
|
||||
"hmac",
|
||||
"lazy_static",
|
||||
"lazy_static 1.5.0",
|
||||
"rc2",
|
||||
"sha1",
|
||||
"yasna",
|
||||
@@ -4000,6 +4077,12 @@ version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
||||
|
||||
[[package]]
|
||||
name = "pest"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0a6dda33d67c26f0aac90d324ab2eb7239c819fc7b2552fe9faa4fe88441edc8"
|
||||
|
||||
[[package]]
|
||||
name = "petgraph"
|
||||
version = "0.6.5"
|
||||
@@ -4435,6 +4518,12 @@ dependencies = [
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-error"
|
||||
version = "1.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.32.0"
|
||||
@@ -4529,7 +4618,7 @@ version = "0.8.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93"
|
||||
dependencies = [
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
"parking_lot",
|
||||
"scheduled-thread-pool",
|
||||
]
|
||||
@@ -4696,16 +4785,29 @@ dependencies = [
|
||||
"thiserror 2.0.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9329abc99e39129fcceabd24cf5d85b4671ef7c29c50e972bc5afe32438ec384"
|
||||
dependencies = [
|
||||
"aho-corasick 0.6.10",
|
||||
"memchr",
|
||||
"regex-syntax 0.5.6",
|
||||
"thread_local",
|
||||
"utf8-ranges",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"aho-corasick 1.1.3",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
"regex-syntax 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4714,9 +4816,18 @@ version = "0.4.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"aho-corasick 1.1.3",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
"regex-syntax 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d707a4fa2637f2dca2ef9fd02225ec7661fe01a53623c1e6515b6916511f7a7"
|
||||
dependencies = [
|
||||
"ucd-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4755,7 +4866,7 @@ dependencies = [
|
||||
"hyper-tls",
|
||||
"hyper-util",
|
||||
"js-sys",
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
"mime",
|
||||
"mime_guess",
|
||||
"native-tls",
|
||||
@@ -4796,7 +4907,7 @@ dependencies = [
|
||||
"gobject-sys",
|
||||
"gtk-sys",
|
||||
"js-sys",
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
"objc2 0.6.1",
|
||||
"objc2-app-kit",
|
||||
"objc2-core-foundation",
|
||||
@@ -4988,7 +5099,7 @@ dependencies = [
|
||||
"core-foundation 0.10.1",
|
||||
"core-foundation-sys",
|
||||
"jni",
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
"once_cell",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
@@ -5176,7 +5287,7 @@ dependencies = [
|
||||
"cssparser",
|
||||
"derive_more",
|
||||
"fxhash",
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
"phf 0.8.0",
|
||||
"phf_codegen 0.8.0",
|
||||
"precomputed-hash",
|
||||
@@ -5544,7 +5655,7 @@ dependencies = [
|
||||
"core-graphics 0.24.0",
|
||||
"foreign-types 0.5.0",
|
||||
"js-sys",
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
"objc2 0.5.2",
|
||||
"objc2-foundation 0.2.2",
|
||||
"objc2-quartz-core 0.2.2",
|
||||
@@ -5692,6 +5803,18 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sysproxy"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9707a79d3b95683aa5a9521e698ffd878b8fb289727c25a69157fb85d529ffff"
|
||||
dependencies = [
|
||||
"interfaces",
|
||||
"thiserror 1.0.69",
|
||||
"winapi",
|
||||
"winreg 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration"
|
||||
version = "0.6.1"
|
||||
@@ -5744,9 +5867,9 @@ dependencies = [
|
||||
"gdkx11-sys",
|
||||
"gtk",
|
||||
"jni",
|
||||
"lazy_static",
|
||||
"lazy_static 1.5.0",
|
||||
"libc",
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
"ndk",
|
||||
"ndk-context",
|
||||
"ndk-sys",
|
||||
@@ -5820,7 +5943,7 @@ dependencies = [
|
||||
"http-range",
|
||||
"jni",
|
||||
"libc",
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
"mime",
|
||||
"muda",
|
||||
"objc2 0.6.1",
|
||||
@@ -5939,7 +6062,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "206dc20af4ed210748ba945c2774e60fd0acd52b9a73a028402caf809e9b6ecf"
|
||||
dependencies = [
|
||||
"arboard",
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
@@ -5974,7 +6097,7 @@ version = "2.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "313f8138692ddc4a2127c4c9607d616a46f5c042e77b3722450866da0aad2f19"
|
||||
dependencies = [
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
"raw-window-handle",
|
||||
"rfd",
|
||||
"serde",
|
||||
@@ -6017,7 +6140,7 @@ dependencies = [
|
||||
"android_logger",
|
||||
"byte-unit",
|
||||
"fern",
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
"objc2 0.6.1",
|
||||
"objc2-foundation 0.3.1",
|
||||
"serde",
|
||||
@@ -6059,7 +6182,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8f08346c8deb39e96f86973da0e2d76cbb933d7ac9b750f6dc4daf955a6f997"
|
||||
dependencies = [
|
||||
"gethostname 1.0.2",
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
"os_info",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -6077,10 +6200,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c374b6db45f2a8a304f0273a15080d98c70cde86178855fc24653ba657a1144c"
|
||||
dependencies = [
|
||||
"encoding_rs",
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
"open",
|
||||
"os_pipe",
|
||||
"regex",
|
||||
"regex 1.11.1",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -6119,7 +6242,7 @@ dependencies = [
|
||||
"futures-util",
|
||||
"http",
|
||||
"infer",
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
"minisign-verify",
|
||||
"osakit",
|
||||
"percent-encoding",
|
||||
@@ -6146,7 +6269,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73736611e14142408d15353e21e3cca2f12a3cfb523ad0ce85999b6d2ef1a704"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
@@ -6188,7 +6311,7 @@ dependencies = [
|
||||
"gtk",
|
||||
"http",
|
||||
"jni",
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
"objc2 0.6.1",
|
||||
"objc2-app-kit",
|
||||
"objc2-foundation 0.3.1",
|
||||
@@ -6223,12 +6346,12 @@ dependencies = [
|
||||
"infer",
|
||||
"json-patch",
|
||||
"kuchikiki",
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
"memchr",
|
||||
"phf 0.11.3",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"regex 1.11.1",
|
||||
"schemars",
|
||||
"semver",
|
||||
"serde",
|
||||
@@ -6328,6 +6451,15 @@ dependencies = [
|
||||
"syn 2.0.101",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b"
|
||||
dependencies = [
|
||||
"lazy_static 1.5.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiff"
|
||||
version = "0.9.1"
|
||||
@@ -6341,9 +6473,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.41"
|
||||
version = "0.3.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
|
||||
checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"itoa",
|
||||
@@ -6351,22 +6483,22 @@ dependencies = [
|
||||
"num-conv",
|
||||
"num_threads",
|
||||
"powerfmt",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"time-core",
|
||||
"time-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time-core"
|
||||
version = "0.1.4"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
|
||||
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
|
||||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.22"
|
||||
version = "0.2.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
|
||||
checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
|
||||
dependencies = [
|
||||
"num-conv",
|
||||
"time-core",
|
||||
@@ -6472,7 +6604,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
"rustls-pki-types",
|
||||
@@ -6816,7 +6948,7 @@ dependencies = [
|
||||
"data-encoding",
|
||||
"http",
|
||||
"httparse",
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
"rand 0.9.1",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
@@ -6837,13 +6969,19 @@ version = "1.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
|
||||
|
||||
[[package]]
|
||||
name = "ucd-util"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "abd2fc5d32b590614af8b0a20d837f32eca055edd0bbead59a9cfe80858be003"
|
||||
|
||||
[[package]]
|
||||
name = "uds_windows"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9"
|
||||
dependencies = [
|
||||
"memoffset",
|
||||
"memoffset 0.9.1",
|
||||
"tempfile",
|
||||
"winapi",
|
||||
]
|
||||
@@ -6953,7 +7091,7 @@ version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d"
|
||||
dependencies = [
|
||||
"regex",
|
||||
"regex 1.11.1",
|
||||
"serde",
|
||||
"unic-ucd-ident",
|
||||
"url",
|
||||
@@ -6965,6 +7103,12 @@ version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||
|
||||
[[package]]
|
||||
name = "utf8-ranges"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fcfc827f90e53a02eaef5e535ee14266c1d569214c6aa70133a624d8a3164ba"
|
||||
|
||||
[[package]]
|
||||
name = "utf8-width"
|
||||
version = "0.1.7"
|
||||
@@ -7101,7 +7245,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
@@ -7848,6 +7992,15 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.55.0"
|
||||
@@ -7874,7 +8027,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e5ff8d0e60065f549fafd9d6cb626203ea64a798186c80d8e7df4f8af56baeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
"os_pipe",
|
||||
"rustix 0.38.44",
|
||||
"tempfile",
|
||||
@@ -7994,6 +8147,17 @@ dependencies = [
|
||||
"rustix 1.0.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yaak-api"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"log 0.4.29",
|
||||
"reqwest",
|
||||
"sysproxy",
|
||||
"thiserror 2.0.17",
|
||||
"yaak-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yaak-app"
|
||||
version = "0.0.0"
|
||||
@@ -8003,7 +8167,7 @@ dependencies = [
|
||||
"cookie",
|
||||
"eventsource-client",
|
||||
"http",
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
"md5 0.8.0",
|
||||
"mime_guess",
|
||||
"openssl-sys",
|
||||
@@ -8034,6 +8198,7 @@ dependencies = [
|
||||
"ts-rs",
|
||||
"url",
|
||||
"uuid",
|
||||
"yaak-api",
|
||||
"yaak-common",
|
||||
"yaak-core",
|
||||
"yaak-crypto",
|
||||
@@ -8060,7 +8225,7 @@ dependencies = [
|
||||
"clap",
|
||||
"dirs",
|
||||
"env_logger",
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"yaak-crypto",
|
||||
@@ -8075,6 +8240,7 @@ name = "yaak-common"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8092,7 +8258,7 @@ dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chacha20poly1305",
|
||||
"keyring",
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
"serde",
|
||||
"thiserror 2.0.17",
|
||||
"yaak-models",
|
||||
@@ -8116,13 +8282,15 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"git2",
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"ts-rs",
|
||||
"url",
|
||||
"yaak-common",
|
||||
"yaak-models",
|
||||
"yaak-sync",
|
||||
]
|
||||
@@ -8136,7 +8304,7 @@ dependencies = [
|
||||
"dunce",
|
||||
"hyper-rustls",
|
||||
"hyper-util",
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
"md5 0.7.0",
|
||||
"prost",
|
||||
"prost-reflect",
|
||||
@@ -8149,6 +8317,7 @@ dependencies = [
|
||||
"tonic",
|
||||
"tonic-reflection",
|
||||
"uuid",
|
||||
"yaak-common",
|
||||
"yaak-tls",
|
||||
]
|
||||
|
||||
@@ -8163,10 +8332,11 @@ dependencies = [
|
||||
"cookie",
|
||||
"flate2",
|
||||
"futures-util",
|
||||
"http-body",
|
||||
"hyper-util",
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
"mime_guess",
|
||||
"regex",
|
||||
"regex 1.11.1",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -8187,7 +8357,7 @@ name = "yaak-license"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -8195,9 +8365,9 @@ dependencies = [
|
||||
"tauri-plugin",
|
||||
"thiserror 2.0.17",
|
||||
"ts-rs",
|
||||
"yaak-api",
|
||||
"yaak-common",
|
||||
"yaak-models",
|
||||
"yaak-tauri-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8206,7 +8376,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"cocoa",
|
||||
"csscolorparser",
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
"objc",
|
||||
"rand 0.9.1",
|
||||
"tauri",
|
||||
@@ -8220,7 +8390,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"hex",
|
||||
"include_dir",
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
"nanoid",
|
||||
"r2d2",
|
||||
"r2d2_sqlite",
|
||||
@@ -8245,11 +8415,11 @@ dependencies = [
|
||||
"futures-util",
|
||||
"hex",
|
||||
"keyring",
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
"md5 0.7.0",
|
||||
"path-slash",
|
||||
"rand 0.9.1",
|
||||
"regex",
|
||||
"regex 1.11.1",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -8279,7 +8449,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"hex",
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
"notify",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -8296,12 +8466,8 @@ dependencies = [
|
||||
name = "yaak-tauri-utils"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"regex",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"regex 1.11.1",
|
||||
"tauri",
|
||||
"thiserror 2.0.17",
|
||||
"yaak-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8309,7 +8475,7 @@ name = "yaak-templates"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
"serde",
|
||||
"serde-wasm-bindgen",
|
||||
"serde_json",
|
||||
@@ -8323,7 +8489,7 @@ dependencies = [
|
||||
name = "yaak-tls"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
"p12",
|
||||
"rustls",
|
||||
"rustls-pemfile",
|
||||
@@ -8340,7 +8506,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"http",
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
"md5 0.8.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -8404,7 +8570,7 @@ dependencies = [
|
||||
"futures-core",
|
||||
"futures-lite",
|
||||
"hex",
|
||||
"nix",
|
||||
"nix 0.30.1",
|
||||
"ordered-stream",
|
||||
"serde",
|
||||
"serde_repr",
|
||||
@@ -8571,7 +8737,7 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aed5f10c571472911e37d8f7601a8dfba52b4f7f73a344015291b82ab292faf6"
|
||||
dependencies = [
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
"thiserror 2.0.17",
|
||||
"zip",
|
||||
]
|
||||
@@ -8590,7 +8756,7 @@ checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"crc32fast",
|
||||
"log",
|
||||
"log 0.4.29",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ members = [
|
||||
"crates/yaak-templates",
|
||||
"crates/yaak-tls",
|
||||
"crates/yaak-ws",
|
||||
"crates/yaak-api",
|
||||
# CLI crates
|
||||
"crates-cli/yaak-cli",
|
||||
# Tauri-specific crates
|
||||
@@ -58,6 +59,7 @@ yaak-sync = { path = "crates/yaak-sync" }
|
||||
yaak-templates = { path = "crates/yaak-templates" }
|
||||
yaak-tls = { path = "crates/yaak-tls" }
|
||||
yaak-ws = { path = "crates/yaak-ws" }
|
||||
yaak-api = { path = "crates/yaak-api" }
|
||||
|
||||
# Internal crates - Tauri-specific
|
||||
yaak-fonts = { path = "crates-tauri/yaak-fonts" }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<p align="center">
|
||||
<a href="https://github.com/JamesIves/github-sponsors-readme-action">
|
||||
<img width="200px" src="https://github.com/mountain-loop/yaak/raw/main/src-tauri/icons/icon.png">
|
||||
<img width="200px" src="https://github.com/mountain-loop/yaak/raw/main/crates-tauri/yaak-app/icons/icon.png">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<!-- sponsors-premium --><a href="https://github.com/MVST-Solutions"><img src="https://github.com/MVST-Solutions.png" width="80px" alt="User avatar: MVST-Solutions" /></a> <a href="https://github.com/dharsanb"><img src="https://github.com/dharsanb.png" width="80px" alt="User avatar: dharsanb" /></a> <a href="https://github.com/railwayapp"><img src="https://github.com/railwayapp.png" width="80px" alt="User avatar: railwayapp" /></a> <a href="https://github.com/caseyamcl"><img src="https://github.com/caseyamcl.png" width="80px" alt="User avatar: caseyamcl" /></a> <a href="https://github.com/bytebase"><img src="https://github.com/bytebase.png" width="80px" alt="User avatar: bytebase" /></a> <a href="https://github.com/"><img src="https://raw.githubusercontent.com/JamesIves/github-sponsors-readme-action/dev/.github/assets/placeholder.png" width="80px" alt="User avatar: " /></a> <!-- sponsors-premium -->
|
||||
</p>
|
||||
<p align="center">
|
||||
<!-- sponsors-base --><a href="https://github.com/seanwash"><img src="https://github.com/seanwash.png" width="50px" alt="User avatar: seanwash" /></a> <a href="https://github.com/jerath"><img src="https://github.com/jerath.png" width="50px" alt="User avatar: jerath" /></a> <a href="https://github.com/itsa-sh"><img src="https://github.com/itsa-sh.png" width="50px" alt="User avatar: itsa-sh" /></a> <a href="https://github.com/dmmulroy"><img src="https://github.com/dmmulroy.png" width="50px" alt="User avatar: dmmulroy" /></a> <a href="https://github.com/timcole"><img src="https://github.com/timcole.png" width="50px" alt="User avatar: timcole" /></a> <a href="https://github.com/VLZH"><img src="https://github.com/VLZH.png" width="50px" alt="User avatar: VLZH" /></a> <a href="https://github.com/terasaka2k"><img src="https://github.com/terasaka2k.png" width="50px" alt="User avatar: terasaka2k" /></a> <a href="https://github.com/andriyor"><img src="https://github.com/andriyor.png" width="50px" alt="User avatar: andriyor" /></a> <a href="https://github.com/majudhu"><img src="https://github.com/majudhu.png" width="50px" alt="User avatar: majudhu" /></a> <a href="https://github.com/axelrindle"><img src="https://github.com/axelrindle.png" width="50px" alt="User avatar: axelrindle" /></a> <a href="https://github.com/jirizverina"><img src="https://github.com/jirizverina.png" width="50px" alt="User avatar: jirizverina" /></a> <a href="https://github.com/chip-well"><img src="https://github.com/chip-well.png" width="50px" alt="User avatar: chip-well" /></a> <a href="https://github.com/GRAYAH"><img src="https://github.com/GRAYAH.png" width="50px" alt="User avatar: GRAYAH" /></a> <!-- sponsors-base -->
|
||||
<!-- sponsors-base --><a href="https://github.com/seanwash"><img src="https://github.com/seanwash.png" width="50px" alt="User avatar: seanwash" /></a> <a href="https://github.com/jerath"><img src="https://github.com/jerath.png" width="50px" alt="User avatar: jerath" /></a> <a href="https://github.com/itsa-sh"><img src="https://github.com/itsa-sh.png" width="50px" alt="User avatar: itsa-sh" /></a> <a href="https://github.com/dmmulroy"><img src="https://github.com/dmmulroy.png" width="50px" alt="User avatar: dmmulroy" /></a> <a href="https://github.com/timcole"><img src="https://github.com/timcole.png" width="50px" alt="User avatar: timcole" /></a> <a href="https://github.com/VLZH"><img src="https://github.com/VLZH.png" width="50px" alt="User avatar: VLZH" /></a> <a href="https://github.com/terasaka2k"><img src="https://github.com/terasaka2k.png" width="50px" alt="User avatar: terasaka2k" /></a> <a href="https://github.com/andriyor"><img src="https://github.com/andriyor.png" width="50px" alt="User avatar: andriyor" /></a> <a href="https://github.com/majudhu"><img src="https://github.com/majudhu.png" width="50px" alt="User avatar: majudhu" /></a> <a href="https://github.com/axelrindle"><img src="https://github.com/axelrindle.png" width="50px" alt="User avatar: axelrindle" /></a> <a href="https://github.com/jirizverina"><img src="https://github.com/jirizverina.png" width="50px" alt="User avatar: jirizverina" /></a> <a href="https://github.com/chip-well"><img src="https://github.com/chip-well.png" width="50px" alt="User avatar: chip-well" /></a> <a href="https://github.com/GRAYAH"><img src="https://github.com/GRAYAH.png" width="50px" alt="User avatar: GRAYAH" /></a> <a href="https://github.com/flashblaze"><img src="https://github.com/flashblaze.png" width="50px" alt="User avatar: flashblaze" /></a> <!-- sponsors-base -->
|
||||
</p>
|
||||
|
||||

|
||||
@@ -64,7 +64,7 @@ visit [`DEVELOPMENT.md`](DEVELOPMENT.md) for tips on setting up your environment
|
||||
## Useful Resources
|
||||
|
||||
- [Feedback and Bug Reports](https://feedback.yaak.app)
|
||||
- [Documentation](https://feedback.yaak.app/help)
|
||||
- [Documentation](https://yaak.app/docs)
|
||||
- [Yaak vs Postman](https://yaak.app/alternatives/postman)
|
||||
- [Yaak vs Bruno](https://yaak.app/alternatives/bruno)
|
||||
- [Yaak vs Insomnia](https://yaak.app/alternatives/insomnia)
|
||||
|
||||
@@ -15,7 +15,7 @@ use yaak_models::util::UpdateSource;
|
||||
use yaak_plugins::events::{PluginContext, RenderPurpose};
|
||||
use yaak_plugins::manager::PluginManager;
|
||||
use yaak_plugins::template_callback::PluginTemplateCallback;
|
||||
use yaak_templates::{parse_and_render, render_json_value_raw, RenderOptions};
|
||||
use yaak_templates::{RenderOptions, parse_and_render, render_json_value_raw};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "yaakcli")]
|
||||
@@ -149,14 +149,7 @@ async fn render_http_request(
|
||||
// Apply path placeholders (e.g., /users/:id -> /users/123)
|
||||
let (url, url_parameters) = apply_path_placeholders(&url, &url_parameters);
|
||||
|
||||
Ok(HttpRequest {
|
||||
url,
|
||||
url_parameters,
|
||||
headers,
|
||||
body,
|
||||
authentication,
|
||||
..r.to_owned()
|
||||
})
|
||||
Ok(HttpRequest { url, url_parameters, headers, body, authentication, ..r.to_owned() })
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
@@ -169,16 +162,10 @@ async fn main() {
|
||||
}
|
||||
|
||||
// Use the same app_id for both data directory and keyring
|
||||
let app_id = if cfg!(debug_assertions) {
|
||||
"app.yaak.desktop.dev"
|
||||
} else {
|
||||
"app.yaak.desktop"
|
||||
};
|
||||
let app_id = if cfg!(debug_assertions) { "app.yaak.desktop.dev" } else { "app.yaak.desktop" };
|
||||
|
||||
let data_dir = cli.data_dir.unwrap_or_else(|| {
|
||||
dirs::data_dir()
|
||||
.expect("Could not determine data directory")
|
||||
.join(app_id)
|
||||
dirs::data_dir().expect("Could not determine data directory").join(app_id)
|
||||
});
|
||||
|
||||
let db_path = data_dir.join("db.sqlite");
|
||||
@@ -191,9 +178,7 @@ async fn main() {
|
||||
|
||||
// Initialize encryption manager for secure() template function
|
||||
// Use the same app_id as the Tauri app for keyring access
|
||||
let encryption_manager = Arc::new(
|
||||
EncryptionManager::new(query_manager.clone(), app_id),
|
||||
);
|
||||
let encryption_manager = Arc::new(EncryptionManager::new(query_manager.clone(), app_id));
|
||||
|
||||
// Initialize plugin manager for template functions
|
||||
let vendored_plugin_dir = data_dir.join("vendored-plugins");
|
||||
@@ -203,9 +188,8 @@ async fn main() {
|
||||
let node_bin_path = PathBuf::from("node");
|
||||
|
||||
// Find the plugin runtime - check YAAK_PLUGIN_RUNTIME env var, then fallback to development path
|
||||
let plugin_runtime_main = std::env::var("YAAK_PLUGIN_RUNTIME")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|_| {
|
||||
let plugin_runtime_main =
|
||||
std::env::var("YAAK_PLUGIN_RUNTIME").map(PathBuf::from).unwrap_or_else(|_| {
|
||||
// Development fallback: look relative to crate root
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../crates-tauri/yaak-app/vendored/plugin-runtime/index.cjs")
|
||||
@@ -226,14 +210,10 @@ async fn main() {
|
||||
// Initialize plugins from database
|
||||
let plugins = db.list_plugins().unwrap_or_default();
|
||||
if !plugins.is_empty() {
|
||||
let errors = plugin_manager
|
||||
.initialize_all_plugins(plugins, &PluginContext::new_empty())
|
||||
.await;
|
||||
let errors =
|
||||
plugin_manager.initialize_all_plugins(plugins, &PluginContext::new_empty()).await;
|
||||
for (plugin_dir, error_msg) in errors {
|
||||
eprintln!(
|
||||
"Warning: Failed to initialize plugin '{}': {}",
|
||||
plugin_dir, error_msg
|
||||
);
|
||||
eprintln!("Warning: Failed to initialize plugin '{}': {}", plugin_dir, error_msg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,9 +229,7 @@ async fn main() {
|
||||
}
|
||||
}
|
||||
Commands::Requests { workspace_id } => {
|
||||
let requests = db
|
||||
.list_http_requests(&workspace_id)
|
||||
.expect("Failed to list requests");
|
||||
let requests = db.list_http_requests(&workspace_id).expect("Failed to list requests");
|
||||
if requests.is_empty() {
|
||||
println!("No requests found in workspace {}", workspace_id);
|
||||
} else {
|
||||
@@ -261,9 +239,7 @@ async fn main() {
|
||||
}
|
||||
}
|
||||
Commands::Send { request_id } => {
|
||||
let request = db
|
||||
.get_http_request(&request_id)
|
||||
.expect("Failed to get request");
|
||||
let request = db.get_http_request(&request_id).expect("Failed to get request");
|
||||
|
||||
// Resolve environment chain for variable substitution
|
||||
let environment_chain = db
|
||||
@@ -318,18 +294,13 @@ async fn main() {
|
||||
}))
|
||||
} else {
|
||||
// Drain events silently
|
||||
tokio::spawn(async move {
|
||||
while event_rx.recv().await.is_some() {}
|
||||
});
|
||||
tokio::spawn(async move { while event_rx.recv().await.is_some() {} });
|
||||
None
|
||||
};
|
||||
|
||||
// Send the request
|
||||
let sender = ReqwestSender::new().expect("Failed to create HTTP client");
|
||||
let response = sender
|
||||
.send(sendable, event_tx)
|
||||
.await
|
||||
.expect("Failed to send request");
|
||||
let response = sender.send(sendable, event_tx).await.expect("Failed to send request");
|
||||
|
||||
// Wait for event handler to finish
|
||||
if let Some(handle) = verbose_handle {
|
||||
@@ -383,18 +354,13 @@ async fn main() {
|
||||
}
|
||||
}))
|
||||
} else {
|
||||
tokio::spawn(async move {
|
||||
while event_rx.recv().await.is_some() {}
|
||||
});
|
||||
tokio::spawn(async move { while event_rx.recv().await.is_some() {} });
|
||||
None
|
||||
};
|
||||
|
||||
// Send the request
|
||||
let sender = ReqwestSender::new().expect("Failed to create HTTP client");
|
||||
let response = sender
|
||||
.send(sendable, event_tx)
|
||||
.await
|
||||
.expect("Failed to send request");
|
||||
let response = sender.send(sendable, event_tx).await.expect("Failed to send request");
|
||||
|
||||
if let Some(handle) = verbose_handle {
|
||||
let _ = handle.await;
|
||||
@@ -421,12 +387,7 @@ async fn main() {
|
||||
let (body, _stats) = response.text().await.expect("Failed to read response body");
|
||||
println!("{}", body);
|
||||
}
|
||||
Commands::Create {
|
||||
workspace_id,
|
||||
name,
|
||||
method,
|
||||
url,
|
||||
} => {
|
||||
Commands::Create { workspace_id, name, method, url } => {
|
||||
let request = HttpRequest {
|
||||
workspace_id,
|
||||
name,
|
||||
|
||||
@@ -57,6 +57,7 @@ url = "2"
|
||||
tokio-util = { version = "0.7", features = ["codec"] }
|
||||
ts-rs = { workspace = true }
|
||||
uuid = "1.12.1"
|
||||
yaak-api = { workspace = true }
|
||||
yaak-common = { workspace = true }
|
||||
yaak-tauri-utils = { workspace = true }
|
||||
yaak-core = { workspace = true }
|
||||
|
||||
@@ -2,14 +2,6 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<!-- Enable for NodeJS execution -->
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
|
||||
<!-- Allow loading 1Password's dylib (signed with different Team ID) -->
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
|
||||
<!-- Re-enable for sandboxing. Currently disabled because auto-updater doesn't work with sandboxing.-->
|
||||
<!-- <key>com.apple.security.app-sandbox</key> <true/>-->
|
||||
<!-- <key>com.apple.security.files.user-selected.read-write</key> <true/>-->
|
||||
|
||||
13
crates-tauri/yaak-app/macos/entitlements.yaaknode.plist
Normal file
13
crates-tauri/yaak-app/macos/entitlements.yaaknode.plist
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<!-- Enable for NodeJS/V8 JIT compiler -->
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
|
||||
<!-- Allow loading plugins signed with different Team IDs (e.g., 1Password) -->
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,9 +1,10 @@
|
||||
use crate::error::Result;
|
||||
use crate::PluginContextExt;
|
||||
use crate::error::Result;
|
||||
use std::sync::Arc;
|
||||
use tauri::{AppHandle, Manager, Runtime, State, WebviewWindow, command};
|
||||
use tauri_plugin_dialog::{DialogExt, MessageDialogKind};
|
||||
use yaak_crypto::manager::EncryptionManager;
|
||||
use yaak_models::models::HttpRequestHeader;
|
||||
use yaak_models::queries::workspaces::default_headers;
|
||||
use yaak_plugins::events::GetThemesResponse;
|
||||
use yaak_plugins::manager::PluginManager;
|
||||
use yaak_plugins::native_template_functions::{
|
||||
@@ -21,20 +22,6 @@ impl<'a, R: Runtime, M: Manager<R>> EncryptionManagerExt<'a, R> for M {
|
||||
}
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn cmd_show_workspace_key<R: Runtime>(
|
||||
window: WebviewWindow<R>,
|
||||
workspace_id: &str,
|
||||
) -> Result<()> {
|
||||
let key = window.crypto().reveal_workspace_key(workspace_id)?;
|
||||
window
|
||||
.dialog()
|
||||
.message(format!("Your workspace key is \n\n{}", key))
|
||||
.kind(MessageDialogKind::Info)
|
||||
.show(|_v| {});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn cmd_decrypt_template<R: Runtime>(
|
||||
window: WebviewWindow<R>,
|
||||
@@ -54,7 +41,12 @@ pub(crate) async fn cmd_secure_template<R: Runtime>(
|
||||
let plugin_manager = Arc::new((*app_handle.state::<PluginManager>()).clone());
|
||||
let encryption_manager = Arc::new((*app_handle.state::<EncryptionManager>()).clone());
|
||||
let plugin_context = window.plugin_context();
|
||||
Ok(encrypt_secure_template_function(plugin_manager, encryption_manager, &plugin_context, template)?)
|
||||
Ok(encrypt_secure_template_function(
|
||||
plugin_manager,
|
||||
encryption_manager,
|
||||
&plugin_context,
|
||||
template,
|
||||
)?)
|
||||
}
|
||||
|
||||
#[command]
|
||||
@@ -92,3 +84,17 @@ pub(crate) async fn cmd_set_workspace_key<R: Runtime>(
|
||||
window.crypto().set_human_key(workspace_id, key)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn cmd_disable_encryption<R: Runtime>(
|
||||
window: WebviewWindow<R>,
|
||||
workspace_id: &str,
|
||||
) -> Result<()> {
|
||||
window.crypto().disable_encryption(workspace_id)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) fn cmd_default_headers() -> Vec<HttpRequestHeader> {
|
||||
default_headers()
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ pub enum Error {
|
||||
PluginError(#[from] yaak_plugins::error::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
TauriUtilsError(#[from] yaak_tauri_utils::error::Error),
|
||||
ApiError(#[from] yaak_api::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
ClipboardError(#[from] tauri_plugin_clipboard_manager::Error),
|
||||
|
||||
@@ -6,33 +6,47 @@ use crate::error::Result;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tauri::command;
|
||||
use yaak_git::{
|
||||
GitCommit, GitRemote, GitStatusSummary, PullResult, PushResult,
|
||||
git_add, git_add_credential, git_add_remote, git_checkout_branch, git_commit,
|
||||
git_create_branch, git_delete_branch, git_fetch_all, git_init, git_log,
|
||||
git_merge_branch, git_pull, git_push, git_remotes, git_rm_remote, git_status,
|
||||
git_unstage,
|
||||
BranchDeleteResult, CloneResult, GitCommit, GitRemote, GitStatusSummary, PullResult,
|
||||
PushResult, git_add, git_add_credential, git_add_remote, git_checkout_branch, git_clone,
|
||||
git_commit, git_create_branch, git_delete_branch, git_delete_remote_branch, git_fetch_all,
|
||||
git_init, git_log, git_merge_branch, git_pull, git_pull_force_reset, git_pull_merge, git_push,
|
||||
git_remotes, git_rename_branch, git_reset_changes, git_rm_remote, git_status, git_unstage,
|
||||
};
|
||||
|
||||
// NOTE: All of these commands are async to prevent blocking work from locking up the UI
|
||||
|
||||
#[command]
|
||||
pub async fn cmd_git_checkout(dir: &Path, branch: &str, force: bool) -> Result<String> {
|
||||
Ok(git_checkout_branch(dir, branch, force)?)
|
||||
Ok(git_checkout_branch(dir, branch, force).await?)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn cmd_git_branch(dir: &Path, branch: &str) -> Result<()> {
|
||||
Ok(git_create_branch(dir, branch)?)
|
||||
pub async fn cmd_git_branch(dir: &Path, branch: &str, base: Option<&str>) -> Result<()> {
|
||||
Ok(git_create_branch(dir, branch, base).await?)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn cmd_git_delete_branch(dir: &Path, branch: &str) -> Result<()> {
|
||||
Ok(git_delete_branch(dir, branch)?)
|
||||
pub async fn cmd_git_delete_branch(
|
||||
dir: &Path,
|
||||
branch: &str,
|
||||
force: Option<bool>,
|
||||
) -> Result<BranchDeleteResult> {
|
||||
Ok(git_delete_branch(dir, branch, force.unwrap_or(false)).await?)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn cmd_git_merge_branch(dir: &Path, branch: &str, force: bool) -> Result<()> {
|
||||
Ok(git_merge_branch(dir, branch, force)?)
|
||||
pub async fn cmd_git_delete_remote_branch(dir: &Path, branch: &str) -> Result<()> {
|
||||
Ok(git_delete_remote_branch(dir, branch).await?)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn cmd_git_merge_branch(dir: &Path, branch: &str) -> Result<()> {
|
||||
Ok(git_merge_branch(dir, branch).await?)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn cmd_git_rename_branch(dir: &Path, old_name: &str, new_name: &str) -> Result<()> {
|
||||
Ok(git_rename_branch(dir, old_name, new_name).await?)
|
||||
}
|
||||
|
||||
#[command]
|
||||
@@ -50,24 +64,43 @@ pub async fn cmd_git_initialize(dir: &Path) -> Result<()> {
|
||||
Ok(git_init(dir)?)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn cmd_git_clone(url: &str, dir: &Path) -> Result<CloneResult> {
|
||||
Ok(git_clone(url, dir).await?)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn cmd_git_commit(dir: &Path, message: &str) -> Result<()> {
|
||||
Ok(git_commit(dir, message)?)
|
||||
Ok(git_commit(dir, message).await?)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn cmd_git_fetch_all(dir: &Path) -> Result<()> {
|
||||
Ok(git_fetch_all(dir)?)
|
||||
Ok(git_fetch_all(dir).await?)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn cmd_git_push(dir: &Path) -> Result<PushResult> {
|
||||
Ok(git_push(dir)?)
|
||||
Ok(git_push(dir).await?)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn cmd_git_pull(dir: &Path) -> Result<PullResult> {
|
||||
Ok(git_pull(dir)?)
|
||||
Ok(git_pull(dir).await?)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn cmd_git_pull_force_reset(
|
||||
dir: &Path,
|
||||
remote: &str,
|
||||
branch: &str,
|
||||
) -> Result<PullResult> {
|
||||
Ok(git_pull_force_reset(dir, remote, branch).await?)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn cmd_git_pull_merge(dir: &Path, remote: &str, branch: &str) -> Result<PullResult> {
|
||||
Ok(git_pull_merge(dir, remote, branch).await?)
|
||||
}
|
||||
|
||||
#[command]
|
||||
@@ -86,14 +119,18 @@ pub async fn cmd_git_unstage(dir: &Path, rela_paths: Vec<PathBuf>) -> Result<()>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn cmd_git_reset_changes(dir: &Path) -> Result<()> {
|
||||
Ok(git_reset_changes(dir).await?)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn cmd_git_add_credential(
|
||||
dir: &Path,
|
||||
remote_url: &str,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> Result<()> {
|
||||
Ok(git_add_credential(dir, remote_url, username, password).await?)
|
||||
Ok(git_add_credential(remote_url, username, password).await?)
|
||||
}
|
||||
|
||||
#[command]
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::PluginContextExt;
|
||||
use crate::error::Result;
|
||||
use crate::models_ext::QueryManagerExt;
|
||||
use KeyAndValueRef::{Ascii, Binary};
|
||||
use tauri::{Manager, Runtime, WebviewWindow};
|
||||
use yaak_grpc::{KeyAndValueRef, MetadataMap};
|
||||
use yaak_models::models::GrpcRequest;
|
||||
use crate::models_ext::QueryManagerExt;
|
||||
use yaak_plugins::events::{CallHttpAuthenticationRequest, HttpHeader};
|
||||
use yaak_plugins::manager::PluginManager;
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::models_ext::QueryManagerExt;
|
||||
use chrono::{NaiveDateTime, Utc};
|
||||
use log::debug;
|
||||
use std::sync::OnceLock;
|
||||
use tauri::{AppHandle, Runtime};
|
||||
use crate::models_ext::QueryManagerExt;
|
||||
use yaak_models::util::UpdateSource;
|
||||
|
||||
const NAMESPACE: &str = "analytics";
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
use crate::PluginContextExt;
|
||||
use crate::error::Error::GenericError;
|
||||
use crate::error::Result;
|
||||
use crate::models_ext::BlobManagerExt;
|
||||
use crate::models_ext::QueryManagerExt;
|
||||
use crate::render::render_http_request;
|
||||
use log::{debug, warn};
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicI32, Ordering};
|
||||
use std::time::{Duration, Instant};
|
||||
use tauri::{AppHandle, Manager, Runtime, WebviewWindow};
|
||||
use tokio::fs::{File, create_dir_all};
|
||||
@@ -15,22 +19,19 @@ use yaak_http::client::{
|
||||
HttpConnectionOptions, HttpConnectionProxySetting, HttpConnectionProxySettingAuth,
|
||||
};
|
||||
use yaak_http::cookies::CookieStore;
|
||||
use yaak_http::manager::HttpConnectionManager;
|
||||
use yaak_http::manager::{CachedClient, HttpConnectionManager};
|
||||
use yaak_http::sender::ReqwestSender;
|
||||
use yaak_http::tee_reader::TeeReader;
|
||||
use yaak_http::transaction::HttpTransaction;
|
||||
use yaak_http::types::{
|
||||
SendableBody, SendableHttpRequest, SendableHttpRequestOptions, append_query_params,
|
||||
};
|
||||
use crate::models_ext::BlobManagerExt;
|
||||
use yaak_models::blob_manager::BodyChunk;
|
||||
use yaak_models::models::{
|
||||
CookieJar, Environment, HttpRequest, HttpResponse, HttpResponseEvent, HttpResponseHeader,
|
||||
HttpResponseState, ProxySetting, ProxySettingAuth,
|
||||
};
|
||||
use crate::models_ext::QueryManagerExt;
|
||||
use yaak_models::util::UpdateSource;
|
||||
use crate::PluginContextExt;
|
||||
use yaak_plugins::events::{
|
||||
CallHttpAuthenticationRequest, HttpHeader, PluginContext, RenderPurpose,
|
||||
};
|
||||
@@ -173,10 +174,22 @@ async fn send_http_request_inner<R: Runtime>(
|
||||
let environment_id = environment.map(|e| e.id);
|
||||
let workspace = window.db().get_workspace(workspace_id)?;
|
||||
let (resolved, auth_context_id) = resolve_http_request(window, unrendered_request)?;
|
||||
let cb = PluginTemplateCallback::new(plugin_manager.clone(), encryption_manager.clone(), &plugin_context, RenderPurpose::Send);
|
||||
let cb = PluginTemplateCallback::new(
|
||||
plugin_manager.clone(),
|
||||
encryption_manager.clone(),
|
||||
&plugin_context,
|
||||
RenderPurpose::Send,
|
||||
);
|
||||
let env_chain =
|
||||
window.db().resolve_environments(&workspace.id, folder_id, environment_id.as_deref())?;
|
||||
let request = render_http_request(&resolved, env_chain, &cb, &RenderOptions::throw()).await?;
|
||||
let mut cancel_rx = cancelled_rx.clone();
|
||||
let render_options = RenderOptions::throw();
|
||||
let request = tokio::select! {
|
||||
result = render_http_request(&resolved, env_chain, &cb, &render_options) => result?,
|
||||
_ = cancel_rx.changed() => {
|
||||
return Err(GenericError("Request canceled".to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
// Build the sendable request using the new SendableHttpRequest type
|
||||
let options = SendableHttpRequestOptions {
|
||||
@@ -228,29 +241,36 @@ async fn send_http_request_inner<R: Runtime>(
|
||||
None => None,
|
||||
};
|
||||
|
||||
let client = connection_manager
|
||||
let cached_client = connection_manager
|
||||
.get_client(&HttpConnectionOptions {
|
||||
id: plugin_context.id.clone(),
|
||||
validate_certificates: workspace.setting_validate_certificates,
|
||||
proxy: proxy_setting,
|
||||
client_certificate,
|
||||
dns_overrides: workspace.setting_dns_overrides.clone(),
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Apply authentication to the request
|
||||
apply_authentication(
|
||||
&window,
|
||||
&mut sendable_request,
|
||||
&request,
|
||||
auth_context_id,
|
||||
&plugin_manager,
|
||||
plugin_context,
|
||||
)
|
||||
.await?;
|
||||
// Apply authentication to the request, racing against cancellation since
|
||||
// auth plugins (e.g. OAuth2) can block indefinitely waiting for user action.
|
||||
let mut cancel_rx = cancelled_rx.clone();
|
||||
tokio::select! {
|
||||
result = apply_authentication(
|
||||
&window,
|
||||
&mut sendable_request,
|
||||
&request,
|
||||
auth_context_id,
|
||||
&plugin_manager,
|
||||
plugin_context,
|
||||
) => result?,
|
||||
_ = cancel_rx.changed() => {
|
||||
return Err(GenericError("Request canceled".to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
let cookie_store = maybe_cookie_store.as_ref().map(|(cs, _)| cs.clone());
|
||||
let result = execute_transaction(
|
||||
client,
|
||||
cached_client,
|
||||
sendable_request,
|
||||
response_ctx,
|
||||
cancelled_rx.clone(),
|
||||
@@ -310,7 +330,7 @@ pub fn resolve_http_request<R: Runtime>(
|
||||
}
|
||||
|
||||
async fn execute_transaction<R: Runtime>(
|
||||
client: reqwest::Client,
|
||||
cached_client: CachedClient,
|
||||
mut sendable_request: SendableHttpRequest,
|
||||
response_ctx: &mut ResponseContext<R>,
|
||||
mut cancelled_rx: Receiver<bool>,
|
||||
@@ -321,7 +341,10 @@ async fn execute_transaction<R: Runtime>(
|
||||
let workspace_id = response_ctx.response().workspace_id.clone();
|
||||
let is_persisted = response_ctx.is_persisted();
|
||||
|
||||
let sender = ReqwestSender::with_client(client);
|
||||
// Keep a reference to the resolver for DNS timing events
|
||||
let resolver = cached_client.resolver.clone();
|
||||
|
||||
let sender = ReqwestSender::with_client(cached_client.client);
|
||||
let transaction = match cookie_store {
|
||||
Some(cs) => HttpTransaction::with_cookie_store(sender, cs),
|
||||
None => HttpTransaction::new(sender),
|
||||
@@ -346,21 +369,39 @@ async fn execute_transaction<R: Runtime>(
|
||||
let (event_tx, mut event_rx) =
|
||||
tokio::sync::mpsc::channel::<yaak_http::sender::HttpResponseEvent>(100);
|
||||
|
||||
// Set the event sender on the DNS resolver so it can emit DNS timing events
|
||||
resolver.set_event_sender(Some(event_tx.clone())).await;
|
||||
|
||||
// Shared state to capture DNS timing from the event processing task
|
||||
let dns_elapsed = Arc::new(AtomicI32::new(0));
|
||||
|
||||
// Write events to DB in a task (only for persisted responses)
|
||||
if is_persisted {
|
||||
let response_id = response_id.clone();
|
||||
let app_handle = app_handle.clone();
|
||||
let update_source = response_ctx.update_source.clone();
|
||||
let workspace_id = workspace_id.clone();
|
||||
let dns_elapsed = dns_elapsed.clone();
|
||||
tokio::spawn(async move {
|
||||
while let Some(event) = event_rx.recv().await {
|
||||
// Capture DNS timing when we see a DNS event
|
||||
if let yaak_http::sender::HttpResponseEvent::DnsResolved { duration, .. } = &event {
|
||||
dns_elapsed.store(*duration as i32, Ordering::SeqCst);
|
||||
}
|
||||
let db_event = HttpResponseEvent::new(&response_id, &workspace_id, event.into());
|
||||
let _ = app_handle.db().upsert_http_response_event(&db_event, &update_source);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// For ephemeral responses, just drain the events
|
||||
tokio::spawn(async move { while event_rx.recv().await.is_some() {} });
|
||||
// For ephemeral responses, just drain the events but still capture DNS timing
|
||||
let dns_elapsed = dns_elapsed.clone();
|
||||
tokio::spawn(async move {
|
||||
while let Some(event) = event_rx.recv().await {
|
||||
if let yaak_http::sender::HttpResponseEvent::DnsResolved { duration, .. } = &event {
|
||||
dns_elapsed.store(*duration as i32, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Capture request body as it's sent (only for persisted responses)
|
||||
@@ -373,7 +414,7 @@ async fn execute_transaction<R: Runtime>(
|
||||
sendable_request.body = Some(SendableBody::Bytes(bytes));
|
||||
None
|
||||
}
|
||||
Some(SendableBody::Stream(stream)) => {
|
||||
Some(SendableBody::Stream { data: stream, content_length }) => {
|
||||
// Wrap stream with TeeReader to capture data as it's read
|
||||
// Use unbounded channel to ensure all data is captured without blocking the HTTP request
|
||||
let (body_chunk_tx, body_chunk_rx) = tokio::sync::mpsc::unbounded_channel::<Vec<u8>>();
|
||||
@@ -407,7 +448,7 @@ async fn execute_transaction<R: Runtime>(
|
||||
None
|
||||
};
|
||||
|
||||
sendable_request.body = Some(SendableBody::Stream(pinned));
|
||||
sendable_request.body = Some(SendableBody::Stream { data: pinned, content_length });
|
||||
handle
|
||||
}
|
||||
None => {
|
||||
@@ -528,10 +569,14 @@ async fn execute_transaction<R: Runtime>(
|
||||
// Final update with closed state and accurate byte count
|
||||
response_ctx.update(|r| {
|
||||
r.elapsed = start.elapsed().as_millis() as i32;
|
||||
r.elapsed_dns = dns_elapsed.load(Ordering::SeqCst);
|
||||
r.content_length = Some(written_bytes as i32);
|
||||
r.state = HttpResponseState::Closed;
|
||||
})?;
|
||||
|
||||
// Clear the event sender from the resolver since this request is done
|
||||
resolver.set_event_sender(None).await;
|
||||
|
||||
Ok((response_ctx.response().clone(), maybe_blob_write_handle))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
use crate::PluginContextExt;
|
||||
use crate::error::Result;
|
||||
use crate::models_ext::QueryManagerExt;
|
||||
use crate::PluginContextExt;
|
||||
use log::info;
|
||||
use std::collections::BTreeMap;
|
||||
use std::fs::read_to_string;
|
||||
use tauri::{Manager, Runtime, WebviewWindow};
|
||||
use yaak_tauri_utils::window::WorkspaceWindowTrait;
|
||||
use yaak_core::WorkspaceContext;
|
||||
use yaak_models::models::{
|
||||
Environment, Folder, GrpcRequest, HttpRequest, WebsocketRequest, Workspace,
|
||||
};
|
||||
use yaak_models::util::{BatchUpsertResult, UpdateSource, maybe_gen_id, maybe_gen_id_opt};
|
||||
use yaak_plugins::manager::PluginManager;
|
||||
use yaak_tauri_utils::window::WorkspaceWindowTrait;
|
||||
|
||||
pub(crate) async fn import_data<R: Runtime>(
|
||||
window: &WebviewWindow<R>,
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::http_request::{resolve_http_request, send_http_request};
|
||||
use crate::import::import_data;
|
||||
use crate::models_ext::{BlobManagerExt, QueryManagerExt};
|
||||
use crate::notifications::YaakNotifier;
|
||||
use crate::render::{render_grpc_request, render_template};
|
||||
use crate::render::{render_grpc_request, render_json_value, render_template};
|
||||
use crate::updates::{UpdateMode, UpdateTrigger, YaakUpdater};
|
||||
use crate::uri_scheme::handle_deep_link;
|
||||
use error::Result as YaakResult;
|
||||
@@ -37,8 +37,8 @@ use yaak_grpc::{Code, ServiceDefinition, serialize_message};
|
||||
use yaak_mac_window::AppHandleMacWindowExt;
|
||||
use yaak_models::models::{
|
||||
AnyModel, CookieJar, Environment, GrpcConnection, GrpcConnectionState, GrpcEvent,
|
||||
GrpcEventType, GrpcRequest, HttpRequest, HttpResponse, HttpResponseEvent, HttpResponseState,
|
||||
Plugin, Workspace, WorkspaceMeta,
|
||||
GrpcEventType, HttpRequest, HttpResponse, HttpResponseEvent, HttpResponseState, Plugin,
|
||||
Workspace, WorkspaceMeta,
|
||||
};
|
||||
use yaak_models::util::{BatchUpsertResult, UpdateSource, get_workspace_export_resources};
|
||||
use yaak_plugins::events::{
|
||||
@@ -101,6 +101,7 @@ struct AppMetaData {
|
||||
app_data_dir: String,
|
||||
app_log_dir: String,
|
||||
vendored_plugin_dir: String,
|
||||
default_project_dir: String,
|
||||
feature_updater: bool,
|
||||
feature_license: bool,
|
||||
}
|
||||
@@ -111,6 +112,7 @@ async fn cmd_metadata(app_handle: AppHandle) -> YaakResult<AppMetaData> {
|
||||
let app_log_dir = app_handle.path().app_log_dir()?;
|
||||
let vendored_plugin_dir =
|
||||
app_handle.path().resolve("vendored/plugins", BaseDirectory::Resource)?;
|
||||
let default_project_dir = app_handle.path().home_dir()?.join("YaakProjects");
|
||||
Ok(AppMetaData {
|
||||
is_dev: is_dev(),
|
||||
version: app_handle.package_info().version.to_string(),
|
||||
@@ -118,6 +120,7 @@ async fn cmd_metadata(app_handle: AppHandle) -> YaakResult<AppMetaData> {
|
||||
app_data_dir: app_data_dir.to_string_lossy().to_string(),
|
||||
app_log_dir: app_log_dir.to_string_lossy().to_string(),
|
||||
vendored_plugin_dir: vendored_plugin_dir.to_string_lossy().to_string(),
|
||||
default_project_dir: default_project_dir.to_string_lossy().to_string(),
|
||||
feature_license: cfg!(feature = "license"),
|
||||
feature_updater: cfg!(feature = "updater"),
|
||||
})
|
||||
@@ -189,7 +192,6 @@ async fn cmd_grpc_reflect<R: Runtime>(
|
||||
request_id: &str,
|
||||
environment_id: Option<&str>,
|
||||
proto_files: Vec<String>,
|
||||
skip_cache: Option<bool>,
|
||||
window: WebviewWindow<R>,
|
||||
app_handle: AppHandle<R>,
|
||||
grpc_handle: State<'_, Mutex<GrpcHandle>>,
|
||||
@@ -224,18 +226,21 @@ async fn cmd_grpc_reflect<R: Runtime>(
|
||||
let settings = window.db().get_settings();
|
||||
let client_certificate =
|
||||
find_client_certificate(req.url.as_str(), &settings.client_certificates);
|
||||
let proto_files: Vec<PathBuf> =
|
||||
proto_files.iter().map(|p| PathBuf::from_str(p).unwrap()).collect();
|
||||
|
||||
Ok(grpc_handle
|
||||
.lock()
|
||||
.await
|
||||
// Always invalidate cached pool when this command is called, to force re-reflection
|
||||
let mut handle = grpc_handle.lock().await;
|
||||
handle.invalidate_pool(&req.id, &uri, &proto_files);
|
||||
|
||||
Ok(handle
|
||||
.services(
|
||||
&req.id,
|
||||
&uri,
|
||||
&proto_files.iter().map(|p| PathBuf::from_str(p).unwrap()).collect(),
|
||||
&proto_files,
|
||||
&metadata,
|
||||
workspace.setting_validate_certificates,
|
||||
client_certificate,
|
||||
skip_cache.unwrap_or(false),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| GenericError(e.to_string()))?)
|
||||
@@ -360,10 +365,8 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
|
||||
let cb = {
|
||||
let cancelled_rx = cancelled_rx.clone();
|
||||
let app_handle = app_handle.clone();
|
||||
let environment_chain = environment_chain.clone();
|
||||
let window = window.clone();
|
||||
let base_msg = base_msg.clone();
|
||||
let plugin_manager = plugin_manager.clone();
|
||||
let encryption_manager = encryption_manager.clone();
|
||||
|
||||
@@ -385,14 +388,12 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
match serde_json::from_str::<IncomingMsg>(ev.payload()) {
|
||||
Ok(IncomingMsg::Message(msg)) => {
|
||||
let window = window.clone();
|
||||
let app_handle = app_handle.clone();
|
||||
let base_msg = base_msg.clone();
|
||||
let environment_chain = environment_chain.clone();
|
||||
let plugin_manager = plugin_manager.clone();
|
||||
let encryption_manager = encryption_manager.clone();
|
||||
let msg = block_in_place(|| {
|
||||
tauri::async_runtime::block_on(async {
|
||||
render_template(
|
||||
let result = render_template(
|
||||
msg.as_str(),
|
||||
environment_chain,
|
||||
&PluginTemplateCallback::new(
|
||||
@@ -406,24 +407,11 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
),
|
||||
&RenderOptions { error_behavior: RenderErrorBehavior::Throw },
|
||||
)
|
||||
.await
|
||||
.expect("Failed to render template")
|
||||
.await;
|
||||
result.expect("Failed to render template")
|
||||
})
|
||||
});
|
||||
in_msg_tx.try_send(msg.clone()).unwrap();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
app_handle
|
||||
.db()
|
||||
.upsert_grpc_event(
|
||||
&GrpcEvent {
|
||||
content: msg,
|
||||
event_type: GrpcEventType::ClientMessage,
|
||||
..base_msg.clone()
|
||||
},
|
||||
&UpdateSource::from_window_label(window.label()),
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
Ok(IncomingMsg::Commit) => {
|
||||
maybe_in_msg_tx.take();
|
||||
@@ -470,12 +458,48 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
)?;
|
||||
|
||||
async move {
|
||||
// Create callback for streaming methods that handles both success and error
|
||||
let on_message = {
|
||||
let app_handle = app_handle.clone();
|
||||
let base_event = base_event.clone();
|
||||
let window_label = window.label().to_string();
|
||||
move |result: std::result::Result<String, String>| match result {
|
||||
Ok(msg) => {
|
||||
let _ = app_handle.db().upsert_grpc_event(
|
||||
&GrpcEvent {
|
||||
content: msg,
|
||||
event_type: GrpcEventType::ClientMessage,
|
||||
..base_event.clone()
|
||||
},
|
||||
&UpdateSource::from_window_label(&window_label),
|
||||
);
|
||||
}
|
||||
Err(error) => {
|
||||
let _ = app_handle.db().upsert_grpc_event(
|
||||
&GrpcEvent {
|
||||
content: format!("Failed to send message: {}", error),
|
||||
event_type: GrpcEventType::Error,
|
||||
..base_event.clone()
|
||||
},
|
||||
&UpdateSource::from_window_label(&window_label),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let (maybe_stream, maybe_msg) =
|
||||
match (method_desc.is_client_streaming(), method_desc.is_server_streaming()) {
|
||||
(true, true) => (
|
||||
Some(
|
||||
connection
|
||||
.streaming(&service, &method, in_msg_stream, &metadata, client_cert)
|
||||
.streaming(
|
||||
&service,
|
||||
&method,
|
||||
in_msg_stream,
|
||||
&metadata,
|
||||
client_cert,
|
||||
on_message.clone(),
|
||||
)
|
||||
.await,
|
||||
),
|
||||
None,
|
||||
@@ -490,6 +514,7 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
in_msg_stream,
|
||||
&metadata,
|
||||
client_cert,
|
||||
on_message.clone(),
|
||||
)
|
||||
.await,
|
||||
),
|
||||
@@ -1035,14 +1060,59 @@ async fn cmd_get_http_authentication_summaries<R: Runtime>(
|
||||
#[tauri::command]
|
||||
async fn cmd_get_http_authentication_config<R: Runtime>(
|
||||
window: WebviewWindow<R>,
|
||||
app_handle: AppHandle<R>,
|
||||
plugin_manager: State<'_, PluginManager>,
|
||||
encryption_manager: State<'_, EncryptionManager>,
|
||||
auth_name: &str,
|
||||
values: HashMap<String, JsonPrimitive>,
|
||||
model: AnyModel,
|
||||
_environment_id: Option<&str>,
|
||||
environment_id: Option<&str>,
|
||||
) -> YaakResult<GetHttpAuthenticationConfigResponse> {
|
||||
// Extract workspace_id and folder_id from the model to resolve the environment chain
|
||||
let (workspace_id, folder_id) = match &model {
|
||||
AnyModel::HttpRequest(r) => (r.workspace_id.clone(), r.folder_id.clone()),
|
||||
AnyModel::GrpcRequest(r) => (r.workspace_id.clone(), r.folder_id.clone()),
|
||||
AnyModel::WebsocketRequest(r) => (r.workspace_id.clone(), r.folder_id.clone()),
|
||||
AnyModel::Folder(f) => (f.workspace_id.clone(), f.folder_id.clone()),
|
||||
AnyModel::Workspace(w) => (w.id.clone(), None),
|
||||
_ => return Err(GenericError("Unsupported model type for authentication config".into())),
|
||||
};
|
||||
|
||||
// Resolve environment chain and render the values for token lookup
|
||||
let environment_chain = app_handle.db().resolve_environments(
|
||||
&workspace_id,
|
||||
folder_id.as_deref(),
|
||||
environment_id,
|
||||
)?;
|
||||
let plugin_manager_arc = Arc::new((*plugin_manager).clone());
|
||||
let encryption_manager_arc = Arc::new((*encryption_manager).clone());
|
||||
let cb = PluginTemplateCallback::new(
|
||||
plugin_manager_arc,
|
||||
encryption_manager_arc,
|
||||
&window.plugin_context(),
|
||||
RenderPurpose::Preview,
|
||||
);
|
||||
|
||||
// Convert HashMap<String, JsonPrimitive> to serde_json::Value for rendering
|
||||
let values_json: serde_json::Value = serde_json::to_value(&values)?;
|
||||
let rendered_json = render_json_value(
|
||||
values_json,
|
||||
environment_chain,
|
||||
&cb,
|
||||
&RenderOptions::return_empty(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Convert back to HashMap<String, JsonPrimitive>
|
||||
let rendered_values: HashMap<String, JsonPrimitive> = serde_json::from_value(rendered_json)?;
|
||||
|
||||
Ok(plugin_manager
|
||||
.get_http_authentication_config(&window.plugin_context(), auth_name, values, model.id())
|
||||
.get_http_authentication_config(
|
||||
&window.plugin_context(),
|
||||
auth_name,
|
||||
rendered_values,
|
||||
model.id(),
|
||||
)
|
||||
.await?)
|
||||
}
|
||||
|
||||
@@ -1089,19 +1159,54 @@ async fn cmd_call_grpc_request_action<R: Runtime>(
|
||||
#[tauri::command]
|
||||
async fn cmd_call_http_authentication_action<R: Runtime>(
|
||||
window: WebviewWindow<R>,
|
||||
app_handle: AppHandle<R>,
|
||||
plugin_manager: State<'_, PluginManager>,
|
||||
encryption_manager: State<'_, EncryptionManager>,
|
||||
auth_name: &str,
|
||||
action_index: i32,
|
||||
values: HashMap<String, JsonPrimitive>,
|
||||
model: AnyModel,
|
||||
_environment_id: Option<&str>,
|
||||
environment_id: Option<&str>,
|
||||
) -> YaakResult<()> {
|
||||
// Extract workspace_id and folder_id from the model to resolve the environment chain
|
||||
let (workspace_id, folder_id) = match &model {
|
||||
AnyModel::HttpRequest(r) => (r.workspace_id.clone(), r.folder_id.clone()),
|
||||
AnyModel::GrpcRequest(r) => (r.workspace_id.clone(), r.folder_id.clone()),
|
||||
AnyModel::WebsocketRequest(r) => (r.workspace_id.clone(), r.folder_id.clone()),
|
||||
AnyModel::Folder(f) => (f.workspace_id.clone(), f.folder_id.clone()),
|
||||
AnyModel::Workspace(w) => (w.id.clone(), None),
|
||||
_ => return Err(GenericError("Unsupported model type for authentication action".into())),
|
||||
};
|
||||
|
||||
// Resolve environment chain and render the values
|
||||
let environment_chain = app_handle.db().resolve_environments(
|
||||
&workspace_id,
|
||||
folder_id.as_deref(),
|
||||
environment_id,
|
||||
)?;
|
||||
let plugin_manager_arc = Arc::new((*plugin_manager).clone());
|
||||
let encryption_manager_arc = Arc::new((*encryption_manager).clone());
|
||||
let cb = PluginTemplateCallback::new(
|
||||
plugin_manager_arc,
|
||||
encryption_manager_arc,
|
||||
&window.plugin_context(),
|
||||
RenderPurpose::Send,
|
||||
);
|
||||
|
||||
// Convert HashMap<String, JsonPrimitive> to serde_json::Value for rendering
|
||||
let values_json: serde_json::Value = serde_json::to_value(&values)?;
|
||||
let rendered_json =
|
||||
render_json_value(values_json, environment_chain, &cb, &RenderOptions::throw()).await?;
|
||||
|
||||
// Convert back to HashMap<String, JsonPrimitive>
|
||||
let rendered_values: HashMap<String, JsonPrimitive> = serde_json::from_value(rendered_json)?;
|
||||
|
||||
Ok(plugin_manager
|
||||
.call_http_authentication_action(
|
||||
&window.plugin_context(),
|
||||
auth_name,
|
||||
action_index,
|
||||
values,
|
||||
rendered_values,
|
||||
&model.id(),
|
||||
)
|
||||
.await?)
|
||||
@@ -1171,35 +1276,6 @@ async fn cmd_save_response<R: Runtime>(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cmd_send_folder<R: Runtime>(
|
||||
app_handle: AppHandle<R>,
|
||||
window: WebviewWindow<R>,
|
||||
environment_id: Option<String>,
|
||||
cookie_jar_id: Option<String>,
|
||||
folder_id: &str,
|
||||
) -> YaakResult<()> {
|
||||
let requests = app_handle.db().list_http_requests_for_folder_recursive(folder_id)?;
|
||||
for request in requests {
|
||||
let app_handle = app_handle.clone();
|
||||
let window = window.clone();
|
||||
let environment_id = environment_id.clone();
|
||||
let cookie_jar_id = cookie_jar_id.clone();
|
||||
tokio::spawn(async move {
|
||||
let _ = cmd_send_http_request(
|
||||
app_handle,
|
||||
window,
|
||||
environment_id.as_deref(),
|
||||
cookie_jar_id.as_deref(),
|
||||
request,
|
||||
)
|
||||
.await;
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cmd_send_http_request<R: Runtime>(
|
||||
app_handle: AppHandle<R>,
|
||||
@@ -1296,27 +1372,6 @@ async fn cmd_install_plugin<R: Runtime>(
|
||||
Ok(plugin)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cmd_create_grpc_request<R: Runtime>(
|
||||
workspace_id: &str,
|
||||
name: &str,
|
||||
sort_priority: f64,
|
||||
folder_id: Option<&str>,
|
||||
app_handle: AppHandle<R>,
|
||||
window: WebviewWindow<R>,
|
||||
) -> YaakResult<GrpcRequest> {
|
||||
Ok(app_handle.db().upsert_grpc_request(
|
||||
&GrpcRequest {
|
||||
workspace_id: workspace_id.to_string(),
|
||||
name: name.to_string(),
|
||||
folder_id: folder_id.map(|s| s.to_string()),
|
||||
sort_priority,
|
||||
..Default::default()
|
||||
},
|
||||
&UpdateSource::from_window_label(window.label()),
|
||||
)?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cmd_reload_plugins<R: Runtime>(
|
||||
app_handle: AppHandle<R>,
|
||||
@@ -1579,7 +1634,6 @@ pub fn run() {
|
||||
cmd_call_folder_action,
|
||||
cmd_call_grpc_request_action,
|
||||
cmd_check_for_updates,
|
||||
cmd_create_grpc_request,
|
||||
cmd_curl_to_request,
|
||||
cmd_delete_all_grpc_connections,
|
||||
cmd_delete_all_http_responses,
|
||||
@@ -1613,7 +1667,6 @@ pub fn run() {
|
||||
cmd_save_response,
|
||||
cmd_send_ephemeral_request,
|
||||
cmd_send_http_request,
|
||||
cmd_send_folder,
|
||||
cmd_template_function_config,
|
||||
cmd_template_function_summaries,
|
||||
cmd_template_tokens_to_string,
|
||||
@@ -1621,12 +1674,13 @@ pub fn run() {
|
||||
//
|
||||
// Migrated commands
|
||||
crate::commands::cmd_decrypt_template,
|
||||
crate::commands::cmd_default_headers,
|
||||
crate::commands::cmd_disable_encryption,
|
||||
crate::commands::cmd_enable_encryption,
|
||||
crate::commands::cmd_get_themes,
|
||||
crate::commands::cmd_reveal_workspace_key,
|
||||
crate::commands::cmd_secure_template,
|
||||
crate::commands::cmd_set_workspace_key,
|
||||
crate::commands::cmd_show_workspace_key,
|
||||
//
|
||||
// Models commands
|
||||
models_ext::models_delete,
|
||||
@@ -1649,30 +1703,36 @@ pub fn run() {
|
||||
git_ext::cmd_git_checkout,
|
||||
git_ext::cmd_git_branch,
|
||||
git_ext::cmd_git_delete_branch,
|
||||
git_ext::cmd_git_delete_remote_branch,
|
||||
git_ext::cmd_git_merge_branch,
|
||||
git_ext::cmd_git_rename_branch,
|
||||
git_ext::cmd_git_status,
|
||||
git_ext::cmd_git_log,
|
||||
git_ext::cmd_git_initialize,
|
||||
git_ext::cmd_git_clone,
|
||||
git_ext::cmd_git_commit,
|
||||
git_ext::cmd_git_fetch_all,
|
||||
git_ext::cmd_git_push,
|
||||
git_ext::cmd_git_pull,
|
||||
git_ext::cmd_git_pull_force_reset,
|
||||
git_ext::cmd_git_pull_merge,
|
||||
git_ext::cmd_git_add,
|
||||
git_ext::cmd_git_unstage,
|
||||
git_ext::cmd_git_reset_changes,
|
||||
git_ext::cmd_git_add_credential,
|
||||
git_ext::cmd_git_remotes,
|
||||
git_ext::cmd_git_add_remote,
|
||||
git_ext::cmd_git_rm_remote,
|
||||
//
|
||||
// Plugin commands
|
||||
plugins_ext::cmd_plugins_search,
|
||||
plugins_ext::cmd_plugins_install,
|
||||
plugins_ext::cmd_plugins_uninstall,
|
||||
plugins_ext::cmd_plugins_updates,
|
||||
plugins_ext::cmd_plugins_update_all,
|
||||
//
|
||||
// WebSocket commands
|
||||
ws_ext::cmd_ws_upsert_request,
|
||||
ws_ext::cmd_ws_duplicate_request,
|
||||
ws_ext::cmd_ws_delete_request,
|
||||
ws_ext::cmd_ws_delete_connection,
|
||||
ws_ext::cmd_ws_delete_connections,
|
||||
ws_ext::cmd_ws_list_events,
|
||||
ws_ext::cmd_ws_list_requests,
|
||||
ws_ext::cmd_ws_list_connections,
|
||||
ws_ext::cmd_ws_send,
|
||||
ws_ext::cmd_ws_close,
|
||||
ws_ext::cmd_ws_connect,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::error::Result;
|
||||
use crate::history::get_or_upsert_launch_info;
|
||||
use crate::models_ext::QueryManagerExt;
|
||||
use chrono::{DateTime, Utc};
|
||||
use log::{debug, info};
|
||||
use reqwest::Method;
|
||||
@@ -8,9 +9,8 @@ use std::time::Instant;
|
||||
use tauri::{AppHandle, Emitter, Manager, Runtime, WebviewWindow};
|
||||
use ts_rs::TS;
|
||||
use yaak_common::platform::get_os_str;
|
||||
use yaak_tauri_utils::api_client::yaak_api_client;
|
||||
use crate::models_ext::QueryManagerExt;
|
||||
use yaak_models::util::UpdateSource;
|
||||
use yaak_api::yaak_api_client;
|
||||
|
||||
// Check for updates every hour
|
||||
const MAX_UPDATE_CHECK_SECONDS: u64 = 60 * 60;
|
||||
@@ -101,7 +101,8 @@ impl YaakNotifier {
|
||||
let license_check = "disabled".to_string();
|
||||
|
||||
let launch_info = get_or_upsert_launch_info(app_handle);
|
||||
let req = yaak_api_client(app_handle)?
|
||||
let app_version = app_handle.package_info().version.to_string();
|
||||
let req = yaak_api_client(&app_version)?
|
||||
.request(Method::GET, "https://notify.yaak.app/notifications")
|
||||
.query(&[
|
||||
("version", &launch_info.current_version),
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use crate::error::Result;
|
||||
use crate::http_request::send_http_request_with_context;
|
||||
use crate::models_ext::BlobManagerExt;
|
||||
use crate::models_ext::QueryManagerExt;
|
||||
use crate::render::{render_grpc_request, render_http_request, render_json_value};
|
||||
use crate::window::{CreateWindowConfig, create_window};
|
||||
use crate::{
|
||||
@@ -10,15 +12,12 @@ use chrono::Utc;
|
||||
use cookie::Cookie;
|
||||
use log::error;
|
||||
use std::sync::Arc;
|
||||
use tauri::{AppHandle, Emitter, Manager, Runtime};
|
||||
use tauri::{AppHandle, Emitter, Listener, Manager, Runtime};
|
||||
use tauri_plugin_clipboard_manager::ClipboardExt;
|
||||
use tauri_plugin_opener::OpenerExt;
|
||||
use yaak_crypto::manager::EncryptionManager;
|
||||
use yaak_tauri_utils::window::WorkspaceWindowTrait;
|
||||
use crate::models_ext::BlobManagerExt;
|
||||
use yaak_models::models::{AnyModel, HttpResponse, Plugin};
|
||||
use yaak_models::queries::any_request::AnyRequest;
|
||||
use crate::models_ext::QueryManagerExt;
|
||||
use yaak_models::util::UpdateSource;
|
||||
use yaak_plugins::error::Error::PluginErr;
|
||||
use yaak_plugins::events::{
|
||||
@@ -32,6 +31,7 @@ use yaak_plugins::events::{
|
||||
use yaak_plugins::manager::PluginManager;
|
||||
use yaak_plugins::plugin_handle::PluginHandle;
|
||||
use yaak_plugins::template_callback::PluginTemplateCallback;
|
||||
use yaak_tauri_utils::window::WorkspaceWindowTrait;
|
||||
use yaak_templates::{RenderErrorBehavior, RenderOptions};
|
||||
|
||||
pub(crate) async fn handle_plugin_event<R: Runtime>(
|
||||
@@ -57,6 +57,58 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
|
||||
let window = get_window_from_plugin_context(app_handle, &plugin_context)?;
|
||||
Ok(call_frontend(&window, event).await)
|
||||
}
|
||||
InternalEventPayload::PromptFormRequest(_) => {
|
||||
let window = get_window_from_plugin_context(app_handle, &plugin_context)?;
|
||||
if event.reply_id.is_some() {
|
||||
// Follow-up update from plugin runtime with resolved inputs — forward to frontend
|
||||
window.emit_to(window.label(), "plugin_event", event.clone())?;
|
||||
Ok(None)
|
||||
} else {
|
||||
// Initial request — set up bidirectional communication
|
||||
window.emit_to(window.label(), "plugin_event", event.clone()).unwrap();
|
||||
|
||||
let event_id = event.id.clone();
|
||||
let plugin_handle = plugin_handle.clone();
|
||||
let plugin_context = plugin_context.clone();
|
||||
let window = window.clone();
|
||||
|
||||
// Spawn async task to handle bidirectional form communication
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let (tx, mut rx) = tokio::sync::mpsc::channel::<InternalEvent>(128);
|
||||
|
||||
// Listen for replies from the frontend
|
||||
let listener_id = window.listen(event_id, move |ev: tauri::Event| {
|
||||
let resp: InternalEvent = serde_json::from_str(ev.payload()).unwrap();
|
||||
let _ = tx.try_send(resp);
|
||||
});
|
||||
|
||||
// Forward each reply to the plugin runtime
|
||||
while let Some(resp) = rx.recv().await {
|
||||
let is_done = matches!(
|
||||
&resp.payload,
|
||||
InternalEventPayload::PromptFormResponse(r) if r.done.unwrap_or(false)
|
||||
);
|
||||
|
||||
let event_to_send = plugin_handle.build_event_to_send(
|
||||
&plugin_context,
|
||||
&resp.payload,
|
||||
Some(resp.reply_id.unwrap_or_default()),
|
||||
);
|
||||
if let Err(e) = plugin_handle.send(&event_to_send).await {
|
||||
log::warn!("Failed to forward form response to plugin: {:?}", e);
|
||||
}
|
||||
|
||||
if is_done {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
window.unlisten(listener_id);
|
||||
});
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
InternalEventPayload::FindHttpResponsesRequest(req) => {
|
||||
let http_responses = app_handle
|
||||
.db()
|
||||
@@ -166,7 +218,12 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
|
||||
)?;
|
||||
let plugin_manager = Arc::new((*app_handle.state::<PluginManager>()).clone());
|
||||
let encryption_manager = Arc::new((*app_handle.state::<EncryptionManager>()).clone());
|
||||
let cb = PluginTemplateCallback::new(plugin_manager, encryption_manager, &plugin_context, req.purpose);
|
||||
let cb = PluginTemplateCallback::new(
|
||||
plugin_manager,
|
||||
encryption_manager,
|
||||
&plugin_context,
|
||||
req.purpose,
|
||||
);
|
||||
let opt = RenderOptions { error_behavior: RenderErrorBehavior::Throw };
|
||||
let grpc_request =
|
||||
render_grpc_request(&req.grpc_request, environment_chain, &cb, &opt).await?;
|
||||
@@ -187,7 +244,12 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
|
||||
)?;
|
||||
let plugin_manager = Arc::new((*app_handle.state::<PluginManager>()).clone());
|
||||
let encryption_manager = Arc::new((*app_handle.state::<EncryptionManager>()).clone());
|
||||
let cb = PluginTemplateCallback::new(plugin_manager, encryption_manager, &plugin_context, req.purpose);
|
||||
let cb = PluginTemplateCallback::new(
|
||||
plugin_manager,
|
||||
encryption_manager,
|
||||
&plugin_context,
|
||||
req.purpose,
|
||||
);
|
||||
let opt = &RenderOptions { error_behavior: RenderErrorBehavior::Throw };
|
||||
let http_request =
|
||||
render_http_request(&req.http_request, environment_chain, &cb, &opt).await?;
|
||||
@@ -218,7 +280,12 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
|
||||
)?;
|
||||
let plugin_manager = Arc::new((*app_handle.state::<PluginManager>()).clone());
|
||||
let encryption_manager = Arc::new((*app_handle.state::<EncryptionManager>()).clone());
|
||||
let cb = PluginTemplateCallback::new(plugin_manager, encryption_manager, &plugin_context, req.purpose);
|
||||
let cb = PluginTemplateCallback::new(
|
||||
plugin_manager,
|
||||
encryption_manager,
|
||||
&plugin_context,
|
||||
req.purpose,
|
||||
);
|
||||
let opt = RenderOptions { error_behavior: RenderErrorBehavior::Throw };
|
||||
let data = render_json_value(req.data, environment_chain, &cb, &opt).await?;
|
||||
Ok(Some(InternalEventPayload::TemplateRenderResponse(TemplateRenderResponse { data })))
|
||||
|
||||
@@ -17,7 +17,7 @@ use tauri::path::BaseDirectory;
|
||||
use tauri::plugin::{Builder, TauriPlugin};
|
||||
use tauri::{
|
||||
AppHandle, Emitter, Manager, RunEvent, Runtime, State, WebviewWindow, WindowEvent, command,
|
||||
generate_handler, is_dev,
|
||||
is_dev,
|
||||
};
|
||||
use tokio::sync::Mutex;
|
||||
use ts_rs::TS;
|
||||
@@ -31,7 +31,7 @@ use yaak_plugins::events::{Color, Icon, PluginContext, ShowToastRequest};
|
||||
use yaak_plugins::install::{delete_and_uninstall, download_and_install};
|
||||
use yaak_plugins::manager::PluginManager;
|
||||
use yaak_plugins::plugin_meta::get_plugin_meta;
|
||||
use yaak_tauri_utils::api_client::yaak_api_client;
|
||||
use yaak_api::yaak_api_client;
|
||||
|
||||
static EXITING: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
@@ -72,7 +72,8 @@ impl PluginUpdater {
|
||||
|
||||
info!("Checking for plugin updates");
|
||||
|
||||
let http_client = yaak_api_client(window.app_handle())?;
|
||||
let app_version = window.app_handle().package_info().version.to_string();
|
||||
let http_client = yaak_api_client(&app_version)?;
|
||||
let plugins = window.app_handle().db().list_plugins()?;
|
||||
let updates = check_plugin_updates(&http_client, plugins.clone()).await?;
|
||||
|
||||
@@ -132,22 +133,24 @@ impl PluginUpdater {
|
||||
// ============================================================================
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn cmd_plugins_search<R: Runtime>(
|
||||
pub async fn cmd_plugins_search<R: Runtime>(
|
||||
app_handle: AppHandle<R>,
|
||||
query: &str,
|
||||
) -> Result<PluginSearchResponse> {
|
||||
let http_client = yaak_api_client(&app_handle)?;
|
||||
let app_version = app_handle.package_info().version.to_string();
|
||||
let http_client = yaak_api_client(&app_version)?;
|
||||
Ok(search_plugins(&http_client, query).await?)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn cmd_plugins_install<R: Runtime>(
|
||||
pub async fn cmd_plugins_install<R: Runtime>(
|
||||
window: WebviewWindow<R>,
|
||||
name: &str,
|
||||
version: Option<String>,
|
||||
) -> Result<()> {
|
||||
let plugin_manager = Arc::new((*window.state::<PluginManager>()).clone());
|
||||
let http_client = yaak_api_client(window.app_handle())?;
|
||||
let app_version = window.app_handle().package_info().version.to_string();
|
||||
let http_client = yaak_api_client(&app_version)?;
|
||||
let query_manager = window.state::<yaak_models::query_manager::QueryManager>();
|
||||
let plugin_context = window.plugin_context();
|
||||
download_and_install(
|
||||
@@ -163,7 +166,7 @@ pub(crate) async fn cmd_plugins_install<R: Runtime>(
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn cmd_plugins_uninstall<R: Runtime>(
|
||||
pub async fn cmd_plugins_uninstall<R: Runtime>(
|
||||
plugin_id: &str,
|
||||
window: WebviewWindow<R>,
|
||||
) -> Result<Plugin> {
|
||||
@@ -174,19 +177,21 @@ pub(crate) async fn cmd_plugins_uninstall<R: Runtime>(
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn cmd_plugins_updates<R: Runtime>(
|
||||
pub async fn cmd_plugins_updates<R: Runtime>(
|
||||
app_handle: AppHandle<R>,
|
||||
) -> Result<PluginUpdatesResponse> {
|
||||
let http_client = yaak_api_client(&app_handle)?;
|
||||
let app_version = app_handle.package_info().version.to_string();
|
||||
let http_client = yaak_api_client(&app_version)?;
|
||||
let plugins = app_handle.db().list_plugins()?;
|
||||
Ok(check_plugin_updates(&http_client, plugins).await?)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn cmd_plugins_update_all<R: Runtime>(
|
||||
pub async fn cmd_plugins_update_all<R: Runtime>(
|
||||
window: WebviewWindow<R>,
|
||||
) -> Result<Vec<PluginNameVersion>> {
|
||||
let http_client = yaak_api_client(window.app_handle())?;
|
||||
let app_version = window.app_handle().package_info().version.to_string();
|
||||
let http_client = yaak_api_client(&app_version)?;
|
||||
let plugins = window.db().list_plugins()?;
|
||||
|
||||
// Get list of available updates (already filtered to only registry plugins)
|
||||
@@ -233,13 +238,6 @@ pub(crate) async fn cmd_plugins_update_all<R: Runtime>(
|
||||
|
||||
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
Builder::new("yaak-plugins")
|
||||
.invoke_handler(generate_handler![
|
||||
cmd_plugins_search,
|
||||
cmd_plugins_install,
|
||||
cmd_plugins_uninstall,
|
||||
cmd_plugins_updates,
|
||||
cmd_plugins_update_all
|
||||
])
|
||||
.setup(|app_handle, _| {
|
||||
// Resolve paths for plugin manager
|
||||
let vendored_plugin_dir = app_handle
|
||||
|
||||
@@ -38,6 +38,9 @@ pub async fn render_grpc_request<T: TemplateCallback>(
|
||||
|
||||
let mut metadata = Vec::new();
|
||||
for p in r.metadata.clone() {
|
||||
if !p.enabled {
|
||||
continue;
|
||||
}
|
||||
metadata.push(HttpRequestHeader {
|
||||
enabled: p.enabled,
|
||||
name: parse_and_render(p.name.as_str(), vars, cb, &opt).await?,
|
||||
@@ -119,6 +122,7 @@ pub async fn render_http_request<T: TemplateCallback>(
|
||||
|
||||
let mut body = BTreeMap::new();
|
||||
for (k, v) in r.body.clone() {
|
||||
let v = if k == "form" { strip_disabled_form_entries(v) } else { v };
|
||||
body.insert(k, render_json_value_raw(v, vars, cb, &opt).await?);
|
||||
}
|
||||
|
||||
@@ -161,3 +165,71 @@ pub async fn render_http_request<T: TemplateCallback>(
|
||||
|
||||
Ok(HttpRequest { url, url_parameters, headers, body, authentication, ..r.to_owned() })
|
||||
}
|
||||
|
||||
/// Strip disabled entries from a JSON array of form objects.
|
||||
fn strip_disabled_form_entries(v: Value) -> Value {
|
||||
match v {
|
||||
Value::Array(items) => Value::Array(
|
||||
items
|
||||
.into_iter()
|
||||
.filter(|item| item.get("enabled").and_then(|e| e.as_bool()).unwrap_or(true))
|
||||
.collect(),
|
||||
),
|
||||
v => v,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn test_strip_disabled_form_entries() {
|
||||
let input = json!([
|
||||
{"enabled": true, "name": "foo", "value": "bar"},
|
||||
{"enabled": false, "name": "disabled", "value": "gone"},
|
||||
{"enabled": true, "name": "baz", "value": "qux"},
|
||||
]);
|
||||
let result = strip_disabled_form_entries(input);
|
||||
assert_eq!(
|
||||
result,
|
||||
json!([
|
||||
{"enabled": true, "name": "foo", "value": "bar"},
|
||||
{"enabled": true, "name": "baz", "value": "qux"},
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_strip_disabled_form_entries_all_disabled() {
|
||||
let input = json!([
|
||||
{"enabled": false, "name": "a", "value": "b"},
|
||||
{"enabled": false, "name": "c", "value": "d"},
|
||||
]);
|
||||
let result = strip_disabled_form_entries(input);
|
||||
assert_eq!(result, json!([]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_strip_disabled_form_entries_missing_enabled_defaults_to_kept() {
|
||||
let input = json!([
|
||||
{"name": "no_enabled_field", "value": "kept"},
|
||||
{"enabled": false, "name": "disabled", "value": "gone"},
|
||||
]);
|
||||
let result = strip_disabled_form_entries(input);
|
||||
assert_eq!(
|
||||
result,
|
||||
json!([
|
||||
{"name": "no_enabled_field", "value": "kept"},
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_strip_disabled_form_entries_non_array_passthrough() {
|
||||
let input = json!("just a string");
|
||||
let result = strip_disabled_form_entries(input.clone());
|
||||
assert_eq!(result, input);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::path::PathBuf;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::models_ext::QueryManagerExt;
|
||||
use log::{debug, error, info, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{Emitter, Listener, Manager, Runtime, WebviewWindow};
|
||||
@@ -11,10 +12,12 @@ use tauri_plugin_updater::{Update, UpdaterExt};
|
||||
use tokio::task::block_in_place;
|
||||
use tokio::time::sleep;
|
||||
use ts_rs::TS;
|
||||
use crate::models_ext::QueryManagerExt;
|
||||
use yaak_models::util::generate_id;
|
||||
use yaak_plugins::manager::PluginManager;
|
||||
|
||||
use url::Url;
|
||||
use yaak_api::get_system_proxy_url;
|
||||
|
||||
use crate::error::Error::GenericError;
|
||||
use crate::is_dev;
|
||||
|
||||
@@ -87,8 +90,13 @@ impl YaakUpdater {
|
||||
info!("Checking for updates mode={} autodl={}", mode, auto_download);
|
||||
|
||||
let w = window.clone();
|
||||
let update_check_result = w
|
||||
.updater_builder()
|
||||
let mut updater_builder = w.updater_builder();
|
||||
if let Some(proxy_url) = get_system_proxy_url() {
|
||||
if let Ok(url) = Url::parse(&proxy_url) {
|
||||
updater_builder = updater_builder.proxy(url);
|
||||
}
|
||||
}
|
||||
let update_check_result = updater_builder
|
||||
.on_before_exit(move || {
|
||||
// Kill plugin manager before exit or NSIS installer will fail to replace sidecar
|
||||
// while it's running.
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
use crate::PluginContextExt;
|
||||
use crate::error::Result;
|
||||
use crate::import::import_data;
|
||||
use crate::models_ext::QueryManagerExt;
|
||||
use crate::PluginContextExt;
|
||||
use log::{info, warn};
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::sync::Arc;
|
||||
use tauri::{AppHandle, Emitter, Manager, Runtime, Url};
|
||||
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogKind};
|
||||
use yaak_tauri_utils::api_client::yaak_api_client;
|
||||
use yaak_models::util::generate_id;
|
||||
use yaak_plugins::events::{Color, ShowToastRequest};
|
||||
use yaak_plugins::install::download_and_install;
|
||||
use yaak_plugins::manager::PluginManager;
|
||||
use yaak_api::yaak_api_client;
|
||||
|
||||
pub(crate) async fn handle_deep_link<R: Runtime>(
|
||||
app_handle: &AppHandle<R>,
|
||||
@@ -46,7 +46,8 @@ pub(crate) async fn handle_deep_link<R: Runtime>(
|
||||
|
||||
let plugin_manager = Arc::new((*window.state::<PluginManager>()).clone());
|
||||
let query_manager = app_handle.db_manager();
|
||||
let http_client = yaak_api_client(app_handle)?;
|
||||
let app_version = app_handle.package_info().version.to_string();
|
||||
let http_client = yaak_api_client(&app_version)?;
|
||||
let plugin_context = window.plugin_context();
|
||||
let pv = download_and_install(
|
||||
plugin_manager,
|
||||
@@ -55,7 +56,8 @@ pub(crate) async fn handle_deep_link<R: Runtime>(
|
||||
&plugin_context,
|
||||
name,
|
||||
version,
|
||||
).await?;
|
||||
)
|
||||
.await?;
|
||||
app_handle.emit(
|
||||
"show_toast",
|
||||
ShowToastRequest {
|
||||
@@ -85,7 +87,8 @@ pub(crate) async fn handle_deep_link<R: Runtime>(
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let resp = yaak_api_client(app_handle)?.get(file_url).send().await?;
|
||||
let app_version = app_handle.package_info().version.to_string();
|
||||
let resp = yaak_api_client(&app_version)?.get(file_url).send().await?;
|
||||
let json = resp.bytes().await?;
|
||||
let p = app_handle
|
||||
.path()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::error::Result;
|
||||
use crate::models_ext::QueryManagerExt;
|
||||
use crate::window_menu::app_menu;
|
||||
use log::{info, warn};
|
||||
use rand::random;
|
||||
@@ -8,7 +9,6 @@ use tauri::{
|
||||
};
|
||||
use tauri_plugin_opener::OpenerExt;
|
||||
use tokio::sync::mpsc;
|
||||
use crate::models_ext::QueryManagerExt;
|
||||
|
||||
const DEFAULT_WINDOW_WIDTH: f64 = 1100.0;
|
||||
const DEFAULT_WINDOW_HEIGHT: f64 = 600.0;
|
||||
@@ -162,11 +162,16 @@ pub(crate) fn create_window<R: Runtime>(
|
||||
"dev.reset_size" => webview_window
|
||||
.set_size(LogicalSize::new(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT))
|
||||
.unwrap(),
|
||||
"dev.reset_size_record" => {
|
||||
"dev.reset_size_16x9" => {
|
||||
let width = webview_window.outer_size().unwrap().width;
|
||||
let height = width * 9 / 16;
|
||||
webview_window.set_size(PhysicalSize::new(width, height)).unwrap()
|
||||
}
|
||||
"dev.reset_size_16x10" => {
|
||||
let width = webview_window.outer_size().unwrap().width;
|
||||
let height = width * 10 / 16;
|
||||
webview_window.set_size(PhysicalSize::new(width, height)).unwrap()
|
||||
}
|
||||
"dev.refresh" => webview_window.eval("location.reload()").unwrap(),
|
||||
"dev.generate_theme_css" => {
|
||||
w.emit("generate_theme_css", true).unwrap();
|
||||
|
||||
@@ -154,8 +154,13 @@ pub fn app_menu<R: Runtime>(app_handle: &AppHandle<R>) -> tauri::Result<Menu<R>>
|
||||
&MenuItemBuilder::with_id("dev.reset_size".to_string(), "Reset Size")
|
||||
.build(app_handle)?,
|
||||
&MenuItemBuilder::with_id(
|
||||
"dev.reset_size_record".to_string(),
|
||||
"Reset Size 16x9",
|
||||
"dev.reset_size_16x9".to_string(),
|
||||
"Resize to 16x9",
|
||||
)
|
||||
.build(app_handle)?,
|
||||
&MenuItemBuilder::with_id(
|
||||
"dev.reset_size_16x10".to_string(),
|
||||
"Resize to 16x10",
|
||||
)
|
||||
.build(app_handle)?,
|
||||
&MenuItemBuilder::with_id(
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//! WebSocket Tauri command wrappers
|
||||
//! These wrap the core yaak-ws functionality for Tauri IPC.
|
||||
|
||||
use crate::PluginContextExt;
|
||||
use crate::error::Result;
|
||||
use crate::models_ext::QueryManagerExt;
|
||||
use crate::PluginContextExt;
|
||||
use http::HeaderMap;
|
||||
use log::{debug, info, warn};
|
||||
use std::str::FromStr;
|
||||
@@ -28,53 +28,6 @@ use yaak_templates::{RenderErrorBehavior, RenderOptions};
|
||||
use yaak_tls::find_client_certificate;
|
||||
use yaak_ws::{WebsocketManager, render_websocket_request};
|
||||
|
||||
#[command]
|
||||
pub async fn cmd_ws_upsert_request<R: Runtime>(
|
||||
request: WebsocketRequest,
|
||||
app_handle: AppHandle<R>,
|
||||
window: WebviewWindow<R>,
|
||||
) -> Result<WebsocketRequest> {
|
||||
Ok(app_handle
|
||||
.db()
|
||||
.upsert_websocket_request(&request, &UpdateSource::from_window_label(window.label()))?)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn cmd_ws_duplicate_request<R: Runtime>(
|
||||
request_id: &str,
|
||||
app_handle: AppHandle<R>,
|
||||
window: WebviewWindow<R>,
|
||||
) -> Result<WebsocketRequest> {
|
||||
let db = app_handle.db();
|
||||
let request = db.get_websocket_request(request_id)?;
|
||||
Ok(db.duplicate_websocket_request(&request, &UpdateSource::from_window_label(window.label()))?)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn cmd_ws_delete_request<R: Runtime>(
|
||||
request_id: &str,
|
||||
app_handle: AppHandle<R>,
|
||||
window: WebviewWindow<R>,
|
||||
) -> Result<WebsocketRequest> {
|
||||
Ok(app_handle
|
||||
.db()
|
||||
.delete_websocket_request_by_id(request_id, &UpdateSource::from_window_label(window.label()))?)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn cmd_ws_delete_connection<R: Runtime>(
|
||||
connection_id: &str,
|
||||
app_handle: AppHandle<R>,
|
||||
window: WebviewWindow<R>,
|
||||
) -> Result<WebsocketConnection> {
|
||||
Ok(app_handle
|
||||
.db()
|
||||
.delete_websocket_connection_by_id(
|
||||
connection_id,
|
||||
&UpdateSource::from_window_label(window.label()),
|
||||
)?)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn cmd_ws_delete_connections<R: Runtime>(
|
||||
request_id: &str,
|
||||
@@ -87,30 +40,6 @@ pub async fn cmd_ws_delete_connections<R: Runtime>(
|
||||
)?)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn cmd_ws_list_events<R: Runtime>(
|
||||
connection_id: &str,
|
||||
app_handle: AppHandle<R>,
|
||||
) -> Result<Vec<WebsocketEvent>> {
|
||||
Ok(app_handle.db().list_websocket_events(connection_id)?)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn cmd_ws_list_requests<R: Runtime>(
|
||||
workspace_id: &str,
|
||||
app_handle: AppHandle<R>,
|
||||
) -> Result<Vec<WebsocketRequest>> {
|
||||
Ok(app_handle.db().list_websocket_requests(workspace_id)?)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn cmd_ws_list_connections<R: Runtime>(
|
||||
workspace_id: &str,
|
||||
app_handle: AppHandle<R>,
|
||||
) -> Result<Vec<WebsocketConnection>> {
|
||||
Ok(app_handle.db().list_websocket_connections(workspace_id)?)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn cmd_ws_send<R: Runtime>(
|
||||
connection_id: &str,
|
||||
@@ -296,8 +225,10 @@ pub async fn cmd_ws_connect<R: Runtime>(
|
||||
)
|
||||
.await?;
|
||||
for header in plugin_result.set_headers.unwrap_or_default() {
|
||||
match (http::HeaderName::from_str(&header.name), HeaderValue::from_str(&header.value))
|
||||
{
|
||||
match (
|
||||
http::HeaderName::from_str(&header.name),
|
||||
HeaderValue::from_str(&header.value),
|
||||
) {
|
||||
(Ok(name), Ok(value)) => {
|
||||
headers.insert(name, value);
|
||||
}
|
||||
|
||||
@@ -44,8 +44,8 @@
|
||||
"vendored/protoc/include",
|
||||
"vendored/plugins",
|
||||
"vendored/plugin-runtime",
|
||||
"vendored/node/yaaknode",
|
||||
"vendored/protoc/yaakprotoc"
|
||||
"vendored/node/yaaknode*",
|
||||
"vendored/protoc/yaakprotoc*"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
{
|
||||
"build": {
|
||||
"features": [
|
||||
"updater",
|
||||
"license"
|
||||
]
|
||||
"features": ["updater", "license"]
|
||||
},
|
||||
"app": {
|
||||
"security": {
|
||||
@@ -11,12 +8,8 @@
|
||||
"default",
|
||||
{
|
||||
"identifier": "release",
|
||||
"windows": [
|
||||
"*"
|
||||
],
|
||||
"permissions": [
|
||||
"yaak-license:default"
|
||||
]
|
||||
"windows": ["*"],
|
||||
"permissions": ["yaak-license:default"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -39,14 +32,7 @@
|
||||
"createUpdaterArtifacts": true,
|
||||
"longDescription": "A cross-platform desktop app for interacting with REST, GraphQL, and gRPC",
|
||||
"shortDescription": "Play with APIs, intuitively",
|
||||
"targets": [
|
||||
"app",
|
||||
"appimage",
|
||||
"deb",
|
||||
"dmg",
|
||||
"nsis",
|
||||
"rpm"
|
||||
],
|
||||
"targets": ["app", "appimage", "deb", "dmg", "nsis", "rpm"],
|
||||
"macOS": {
|
||||
"minimumSystemVersion": "13.0",
|
||||
"exceptionDomain": "",
|
||||
@@ -58,10 +44,16 @@
|
||||
},
|
||||
"linux": {
|
||||
"deb": {
|
||||
"desktopTemplate": "./template.desktop"
|
||||
"desktopTemplate": "./template.desktop",
|
||||
"files": {
|
||||
"/usr/share/metainfo/app.yaak.Yaak.metainfo.xml": "../../flatpak/app.yaak.Yaak.metainfo.xml"
|
||||
}
|
||||
},
|
||||
"rpm": {
|
||||
"desktopTemplate": "./template.desktop"
|
||||
"desktopTemplate": "./template.desktop",
|
||||
"files": {
|
||||
"/usr/share/metainfo/app.yaak.Yaak.metainfo.xml": "../../flatpak/app.yaak.Yaak.metainfo.xml"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ thiserror = { workspace = true }
|
||||
ts-rs = { workspace = true }
|
||||
yaak-common = { workspace = true }
|
||||
yaak-models = { workspace = true }
|
||||
yaak-tauri-utils = { workspace = true }
|
||||
yaak-api = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
tauri-plugin = { workspace = true, features = ["build"] }
|
||||
|
||||
@@ -16,7 +16,7 @@ pub enum Error {
|
||||
ModelError(#[from] yaak_models::error::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
TauriUtilsError(#[from] yaak_tauri_utils::error::Error),
|
||||
ApiError(#[from] yaak_api::Error),
|
||||
|
||||
#[error("Internal server error")]
|
||||
ServerError,
|
||||
|
||||
@@ -8,10 +8,10 @@ use std::time::Duration;
|
||||
use tauri::{AppHandle, Emitter, Manager, Runtime, WebviewWindow, is_dev};
|
||||
use ts_rs::TS;
|
||||
use yaak_common::platform::get_os_str;
|
||||
use yaak_tauri_utils::api_client::yaak_api_client;
|
||||
use yaak_models::db_context::DbContext;
|
||||
use yaak_models::query_manager::QueryManager;
|
||||
use yaak_models::util::UpdateSource;
|
||||
use yaak_api::yaak_api_client;
|
||||
|
||||
/// Extension trait for accessing the QueryManager from Tauri Manager types.
|
||||
/// This is needed temporarily until all crates are refactored to not use Tauri.
|
||||
@@ -118,11 +118,12 @@ pub async fn activate_license<R: Runtime>(
|
||||
license_key: &str,
|
||||
) -> Result<()> {
|
||||
info!("Activating license {}", license_key);
|
||||
let client = reqwest::Client::new();
|
||||
let app_version = window.app_handle().package_info().version.to_string();
|
||||
let client = yaak_api_client(&app_version)?;
|
||||
let payload = ActivateLicenseRequestPayload {
|
||||
license_key: license_key.to_string(),
|
||||
app_platform: get_os_str().to_string(),
|
||||
app_version: window.app_handle().package_info().version.to_string(),
|
||||
app_version,
|
||||
};
|
||||
let response = client.post(build_url("/licenses/activate")).json(&payload).send().await?;
|
||||
|
||||
@@ -155,11 +156,12 @@ pub async fn deactivate_license<R: Runtime>(window: &WebviewWindow<R>) -> Result
|
||||
let app_handle = window.app_handle();
|
||||
let activation_id = get_activation_id(app_handle).await;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let app_version = window.app_handle().package_info().version.to_string();
|
||||
let client = yaak_api_client(&app_version)?;
|
||||
let path = format!("/licenses/activations/{}/deactivate", activation_id);
|
||||
let payload = DeactivateLicenseRequestPayload {
|
||||
app_platform: get_os_str().to_string(),
|
||||
app_version: window.app_handle().package_info().version.to_string(),
|
||||
app_version,
|
||||
};
|
||||
let response = client.post(build_url(&path)).json(&payload).send().await?;
|
||||
|
||||
@@ -186,9 +188,10 @@ pub async fn deactivate_license<R: Runtime>(window: &WebviewWindow<R>) -> Result
|
||||
}
|
||||
|
||||
pub async fn check_license<R: Runtime>(window: &WebviewWindow<R>) -> Result<LicenseCheckStatus> {
|
||||
let app_version = window.app_handle().package_info().version.to_string();
|
||||
let payload = CheckActivationRequestPayload {
|
||||
app_platform: get_os_str().to_string(),
|
||||
app_version: window.package_info().version.to_string(),
|
||||
app_version,
|
||||
};
|
||||
let activation_id = get_activation_id(window.app_handle()).await;
|
||||
|
||||
@@ -204,7 +207,7 @@ pub async fn check_license<R: Runtime>(window: &WebviewWindow<R>) -> Result<Lice
|
||||
(true, _) => {
|
||||
info!("Checking license activation");
|
||||
// A license has been activated, so let's check the license server
|
||||
let client = yaak_api_client(window.app_handle())?;
|
||||
let client = yaak_api_client(&payload.app_version)?;
|
||||
let path = format!("/licenses/activations/{activation_id}/check-v2");
|
||||
let response = client.post(build_url(&path)).json(&payload).send().await?;
|
||||
|
||||
|
||||
@@ -6,8 +6,4 @@ publish = false
|
||||
|
||||
[dependencies]
|
||||
tauri = { workspace = true }
|
||||
reqwest = { workspace = true, features = ["gzip"] }
|
||||
thiserror = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
regex = "1.11.0"
|
||||
yaak-common = { workspace = true }
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
use crate::error::Result;
|
||||
use reqwest::Client;
|
||||
use std::time::Duration;
|
||||
use tauri::http::{HeaderMap, HeaderValue};
|
||||
use tauri::{AppHandle, Runtime};
|
||||
use yaak_common::platform::{get_ua_arch, get_ua_platform};
|
||||
|
||||
pub fn yaak_api_client<R: Runtime>(app_handle: &AppHandle<R>) -> Result<Client> {
|
||||
let platform = get_ua_platform();
|
||||
let version = app_handle.package_info().version.clone();
|
||||
let arch = get_ua_arch();
|
||||
let ua = format!("Yaak/{version} ({platform}; {arch})");
|
||||
let mut default_headers = HeaderMap::new();
|
||||
default_headers.insert("Accept", HeaderValue::from_str("application/json").unwrap());
|
||||
|
||||
let client = reqwest::ClientBuilder::new()
|
||||
.timeout(Duration::from_secs(20))
|
||||
.default_headers(default_headers)
|
||||
.gzip(true)
|
||||
.user_agent(ua)
|
||||
.build()?;
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
use serde::{Serialize, Serializer};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
ReqwestError(#[from] reqwest::Error),
|
||||
}
|
||||
|
||||
impl Serialize for Error {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.to_string().as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
@@ -1,3 +1 @@
|
||||
pub mod api_client;
|
||||
pub mod error;
|
||||
pub mod window;
|
||||
|
||||
12
crates/yaak-api/Cargo.toml
Normal file
12
crates/yaak-api/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "yaak-api"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
log = { workspace = true }
|
||||
reqwest = { workspace = true, features = ["gzip"] }
|
||||
sysproxy = "0.3"
|
||||
thiserror = { workspace = true }
|
||||
yaak-common = { workspace = true }
|
||||
9
crates/yaak-api/src/error.rs
Normal file
9
crates/yaak-api/src/error.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
ReqwestError(#[from] reqwest::Error),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
70
crates/yaak-api/src/lib.rs
Normal file
70
crates/yaak-api/src/lib.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
mod error;
|
||||
|
||||
pub use error::{Error, Result};
|
||||
|
||||
use log::{debug, warn};
|
||||
use reqwest::Client;
|
||||
use reqwest::header::{HeaderMap, HeaderValue};
|
||||
use std::time::Duration;
|
||||
use yaak_common::platform::{get_ua_arch, get_ua_platform};
|
||||
|
||||
/// Build a reqwest Client configured for Yaak's own API calls.
|
||||
///
|
||||
/// Includes a custom User-Agent, JSON accept header, 20s timeout, gzip,
|
||||
/// and automatic OS-level proxy detection via sysproxy.
|
||||
pub fn yaak_api_client(version: &str) -> Result<Client> {
|
||||
let platform = get_ua_platform();
|
||||
let arch = get_ua_arch();
|
||||
let ua = format!("Yaak/{version} ({platform}; {arch})");
|
||||
|
||||
let mut default_headers = HeaderMap::new();
|
||||
default_headers.insert("Accept", HeaderValue::from_str("application/json").unwrap());
|
||||
|
||||
let mut builder = reqwest::ClientBuilder::new()
|
||||
.timeout(Duration::from_secs(20))
|
||||
.default_headers(default_headers)
|
||||
.gzip(true)
|
||||
.user_agent(ua);
|
||||
|
||||
if let Some(sys) = get_enabled_system_proxy() {
|
||||
let proxy_url = format!("http://{}:{}", sys.host, sys.port);
|
||||
match reqwest::Proxy::all(&proxy_url) {
|
||||
Ok(p) => {
|
||||
let p = if !sys.bypass.is_empty() {
|
||||
p.no_proxy(reqwest::NoProxy::from_string(&sys.bypass))
|
||||
} else {
|
||||
p
|
||||
};
|
||||
builder = builder.proxy(p);
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to configure system proxy: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(builder.build()?)
|
||||
}
|
||||
|
||||
/// Returns the system proxy URL if one is enabled, e.g. `http://host:port`.
|
||||
pub fn get_system_proxy_url() -> Option<String> {
|
||||
let sys = get_enabled_system_proxy()?;
|
||||
Some(format!("http://{}:{}", sys.host, sys.port))
|
||||
}
|
||||
|
||||
fn get_enabled_system_proxy() -> Option<sysproxy::Sysproxy> {
|
||||
match sysproxy::Sysproxy::get_system_proxy() {
|
||||
Ok(sys) if sys.enable => {
|
||||
debug!("Detected system proxy: http://{}:{}", sys.host, sys.port);
|
||||
Some(sys)
|
||||
}
|
||||
Ok(_) => {
|
||||
debug!("System proxy detected but not enabled");
|
||||
None
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("Could not detect system proxy: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,3 +6,4 @@ publish = false
|
||||
|
||||
[dependencies]
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true, features = ["process"] }
|
||||
|
||||
16
crates/yaak-common/src/command.rs
Normal file
16
crates/yaak-common/src/command.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use std::ffi::OsStr;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
|
||||
|
||||
/// Creates a new `tokio::process::Command` that won't spawn a console window on Windows.
|
||||
pub fn new_xplatform_command<S: AsRef<OsStr>>(program: S) -> tokio::process::Command {
|
||||
#[allow(unused_mut)]
|
||||
let mut cmd = tokio::process::Command::new(program);
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
cmd.creation_flags(CREATE_NO_WINDOW);
|
||||
}
|
||||
cmd
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod command;
|
||||
pub mod platform;
|
||||
pub mod serde;
|
||||
|
||||
@@ -11,3 +11,7 @@ export function revealWorkspaceKey(workspaceId: string) {
|
||||
export function setWorkspaceKey(args: { workspaceId: string; key: string }) {
|
||||
return invoke<void>('cmd_set_workspace_key', args);
|
||||
}
|
||||
|
||||
export function disableEncryption(workspaceId: string) {
|
||||
return invoke<void>('cmd_disable_encryption', { workspaceId });
|
||||
}
|
||||
|
||||
@@ -115,6 +115,35 @@ impl EncryptionManager {
|
||||
self.set_workspace_key(workspace_id, &wkey)
|
||||
}
|
||||
|
||||
pub fn disable_encryption(&self, workspace_id: &str) -> Result<()> {
|
||||
info!("Disabling encryption for {workspace_id}");
|
||||
|
||||
self.query_manager.with_tx::<(), Error>(|tx| {
|
||||
let workspace = tx.get_workspace(workspace_id)?;
|
||||
let workspace_meta = tx.get_or_create_workspace_meta(workspace_id)?;
|
||||
|
||||
// Clear encryption challenge on workspace
|
||||
tx.upsert_workspace(
|
||||
&Workspace { encryption_key_challenge: None, ..workspace },
|
||||
&UpdateSource::Background,
|
||||
)?;
|
||||
|
||||
// Clear encryption key on workspace meta
|
||||
tx.upsert_workspace_meta(
|
||||
&WorkspaceMeta { encryption_key: None, ..workspace_meta },
|
||||
&UpdateSource::Background,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// Remove from cache
|
||||
let mut cache = self.cached_workspace_keys.lock().unwrap();
|
||||
cache.remove(workspace_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_workspace_key(&self, workspace_id: &str) -> Result<WorkspaceKey> {
|
||||
{
|
||||
let cache = self.cached_workspace_keys.lock().unwrap();
|
||||
|
||||
@@ -6,13 +6,15 @@ publish = false
|
||||
|
||||
[dependencies]
|
||||
chrono = { workspace = true, features = ["serde"] }
|
||||
git2 = { version = "0.20.0", features = ["vendored-libgit2", "vendored-openssl"] }
|
||||
git2 = { version = "0.20.4", features = ["vendored-libgit2", "vendored-openssl"] }
|
||||
log = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
serde_yaml = "0.9.34"
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true, features = ["io-util"] }
|
||||
ts-rs = { workspace = true, features = ["chrono-impl", "serde-json-impl"] }
|
||||
url = "2"
|
||||
yaak-common = { workspace = true }
|
||||
yaak-models = { workspace = true }
|
||||
yaak-sync = { workspace = true }
|
||||
|
||||
8
crates/yaak-git/bindings/gen_git.ts
generated
8
crates/yaak-git/bindings/gen_git.ts
generated
@@ -1,6 +1,10 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { SyncModel } from "./gen_models";
|
||||
|
||||
export type BranchDeleteResult = { "type": "success", message: string, } | { "type": "not_fully_merged" };
|
||||
|
||||
export type CloneResult = { "type": "success" } | { "type": "cancelled" } | { "type": "needs_credentials", url: string, error: string | null, };
|
||||
|
||||
export type GitAuthor = { name: string | null, email: string | null, };
|
||||
|
||||
export type GitCommit = { author: GitAuthor, when: string, message: string | null, };
|
||||
@@ -11,8 +15,8 @@ export type GitStatus = "untracked" | "conflict" | "current" | "modified" | "rem
|
||||
|
||||
export type GitStatusEntry = { relaPath: string, status: GitStatus, staged: boolean, prev: SyncModel | null, next: SyncModel | null, };
|
||||
|
||||
export type GitStatusSummary = { path: string, headRef: string | null, headRefShorthand: string | null, entries: Array<GitStatusEntry>, origins: Array<string>, localBranches: Array<string>, remoteBranches: Array<string>, };
|
||||
export type GitStatusSummary = { path: string, headRef: string | null, headRefShorthand: string | null, entries: Array<GitStatusEntry>, origins: Array<string>, localBranches: Array<string>, remoteBranches: Array<string>, ahead: number, behind: number, };
|
||||
|
||||
export type PullResult = { "type": "success", message: string, } | { "type": "up_to_date" } | { "type": "needs_credentials", url: string, error: string | null, };
|
||||
export type PullResult = { "type": "success", message: string, } | { "type": "up_to_date" } | { "type": "needs_credentials", url: string, error: string | null, } | { "type": "diverged", remote: string, branch: string, } | { "type": "uncommitted_changes" };
|
||||
|
||||
export type PushResult = { "type": "success", message: string, } | { "type": "up_to_date" } | { "type": "needs_credentials", url: string, error: string | null, };
|
||||
|
||||
4
crates/yaak-git/bindings/gen_models.ts
generated
4
crates/yaak-git/bindings/gen_models.ts
generated
@@ -1,5 +1,7 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type DnsOverride = { hostname: string, ipv4: Array<string>, ipv6: Array<string>, enabled?: boolean, };
|
||||
|
||||
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, parentModel: string, parentId: string | null, variables: Array<EnvironmentVariable>, color: string | null, sortPriority: number, };
|
||||
|
||||
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||
@@ -18,4 +20,4 @@ export type SyncModel = { "type": "workspace" } & Workspace | { "type": "environ
|
||||
|
||||
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
|
||||
|
||||
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };
|
||||
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, settingDnsOverrides: Array<DnsOverride>, };
|
||||
|
||||
@@ -3,40 +3,59 @@ import { invoke } from '@tauri-apps/api/core';
|
||||
import { createFastMutation } from '@yaakapp/app/hooks/useFastMutation';
|
||||
import { queryClient } from '@yaakapp/app/lib/queryClient';
|
||||
import { useMemo } from 'react';
|
||||
import { GitCommit, GitRemote, GitStatusSummary, PullResult, PushResult } from './bindings/gen_git';
|
||||
import { BranchDeleteResult, CloneResult, GitCommit, GitRemote, GitStatusSummary, PullResult, PushResult } from './bindings/gen_git';
|
||||
import { showToast } from '@yaakapp/app/lib/toast';
|
||||
|
||||
export * from './bindings/gen_git';
|
||||
export * from './bindings/gen_models';
|
||||
|
||||
export interface GitCredentials {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export type DivergedStrategy = 'force_reset' | 'merge' | 'cancel';
|
||||
|
||||
export type UncommittedChangesStrategy = 'reset' | 'cancel';
|
||||
|
||||
export interface GitCallbacks {
|
||||
addRemote: () => Promise<GitRemote | null>;
|
||||
promptCredentials: (
|
||||
result: Extract<PushResult, { type: 'needs_credentials' }>,
|
||||
) => Promise<GitCredentials | null>;
|
||||
promptDiverged: (
|
||||
result: Extract<PullResult, { type: 'diverged' }>,
|
||||
) => Promise<DivergedStrategy>;
|
||||
promptUncommittedChanges: () => Promise<UncommittedChangesStrategy>;
|
||||
forceSync: () => Promise<void>;
|
||||
}
|
||||
|
||||
const onSuccess = () => queryClient.invalidateQueries({ queryKey: ['git'] });
|
||||
|
||||
export function useGit(dir: string, callbacks: GitCallbacks) {
|
||||
export function useGit(dir: string, callbacks: GitCallbacks, refreshKey?: string) {
|
||||
const mutations = useMemo(() => gitMutations(dir, callbacks), [dir, callbacks]);
|
||||
const fetchAll = useQuery<void, string>({
|
||||
queryKey: ['git', 'fetch_all', dir, refreshKey],
|
||||
queryFn: () => invoke('cmd_git_fetch_all', { dir }),
|
||||
refetchInterval: 10 * 60_000,
|
||||
});
|
||||
return [
|
||||
{
|
||||
remotes: useQuery<GitRemote[], string>({
|
||||
queryKey: ['git', 'remotes', dir],
|
||||
queryKey: ['git', 'remotes', dir, refreshKey],
|
||||
queryFn: () => getRemotes(dir),
|
||||
placeholderData: (prev) => prev,
|
||||
}),
|
||||
log: useQuery<GitCommit[], string>({
|
||||
queryKey: ['git', 'log', dir],
|
||||
queryKey: ['git', 'log', dir, refreshKey],
|
||||
queryFn: () => invoke('cmd_git_log', { dir }),
|
||||
placeholderData: (prev) => prev,
|
||||
}),
|
||||
status: useQuery<GitStatusSummary, string>({
|
||||
refetchOnMount: true,
|
||||
queryKey: ['git', 'status', dir],
|
||||
queryKey: ['git', 'status', dir, refreshKey, fetchAll.dataUpdatedAt],
|
||||
queryFn: () => invoke('cmd_git_status', { dir }),
|
||||
placeholderData: (prev) => prev,
|
||||
}),
|
||||
},
|
||||
mutations,
|
||||
@@ -59,7 +78,6 @@ export const gitMutations = (dir: string, callbacks: GitCallbacks) => {
|
||||
if (creds == null) throw new Error('Canceled');
|
||||
|
||||
await invoke('cmd_git_add_credential', {
|
||||
dir,
|
||||
remoteUrl: result.url,
|
||||
username: creds.username,
|
||||
password: creds.password,
|
||||
@@ -69,6 +87,15 @@ export const gitMutations = (dir: string, callbacks: GitCallbacks) => {
|
||||
return invoke<PushResult>('cmd_git_push', { dir });
|
||||
};
|
||||
|
||||
const handleError = (err: unknown) => {
|
||||
showToast({
|
||||
id: `${err}`,
|
||||
message: `${err}`,
|
||||
color: 'danger',
|
||||
timeout: 5000,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
init: createFastMutation<void, string, void>({
|
||||
mutationKey: ['git', 'init'],
|
||||
@@ -90,21 +117,31 @@ export const gitMutations = (dir: string, callbacks: GitCallbacks) => {
|
||||
mutationFn: (args) => invoke('cmd_git_rm_remote', { dir, ...args }),
|
||||
onSuccess,
|
||||
}),
|
||||
branch: createFastMutation<void, string, { branch: string }>({
|
||||
createBranch: createFastMutation<void, string, { branch: string; base?: string }>({
|
||||
mutationKey: ['git', 'branch', dir],
|
||||
mutationFn: (args) => invoke('cmd_git_branch', { dir, ...args }),
|
||||
onSuccess,
|
||||
}),
|
||||
mergeBranch: createFastMutation<void, string, { branch: string; force: boolean }>({
|
||||
mergeBranch: createFastMutation<void, string, { branch: string }>({
|
||||
mutationKey: ['git', 'merge', dir],
|
||||
mutationFn: (args) => invoke('cmd_git_merge_branch', { dir, ...args }),
|
||||
onSuccess,
|
||||
}),
|
||||
deleteBranch: createFastMutation<void, string, { branch: string }>({
|
||||
deleteBranch: createFastMutation<BranchDeleteResult, string, { branch: string, force?: boolean }>({
|
||||
mutationKey: ['git', 'delete-branch', dir],
|
||||
mutationFn: (args) => invoke('cmd_git_delete_branch', { dir, ...args }),
|
||||
onSuccess,
|
||||
}),
|
||||
deleteRemoteBranch: createFastMutation<void, string, { branch: string }>({
|
||||
mutationKey: ['git', 'delete-remote-branch', dir],
|
||||
mutationFn: (args) => invoke('cmd_git_delete_remote_branch', { dir, ...args }),
|
||||
onSuccess,
|
||||
}),
|
||||
renameBranch: createFastMutation<void, string, { oldName: string, newName: string }>({
|
||||
mutationKey: ['git', 'rename-branch', dir],
|
||||
mutationFn: (args) => invoke('cmd_git_rename_branch', { dir, ...args }),
|
||||
onSuccess,
|
||||
}),
|
||||
checkout: createFastMutation<string, string, { branch: string; force: boolean }>({
|
||||
mutationKey: ['git', 'checkout', dir],
|
||||
mutationFn: (args) => invoke('cmd_git_checkout', { dir, ...args }),
|
||||
@@ -123,11 +160,7 @@ export const gitMutations = (dir: string, callbacks: GitCallbacks) => {
|
||||
},
|
||||
onSuccess,
|
||||
}),
|
||||
fetchAll: createFastMutation<string, string, void>({
|
||||
mutationKey: ['git', 'checkout', dir],
|
||||
mutationFn: () => invoke('cmd_git_fetch_all', { dir }),
|
||||
onSuccess,
|
||||
}),
|
||||
|
||||
push: createFastMutation<PushResult, string, void>({
|
||||
mutationKey: ['git', 'push', dir],
|
||||
mutationFn: push,
|
||||
@@ -137,21 +170,51 @@ export const gitMutations = (dir: string, callbacks: GitCallbacks) => {
|
||||
mutationKey: ['git', 'pull', dir],
|
||||
async mutationFn() {
|
||||
const result = await invoke<PullResult>('cmd_git_pull', { dir });
|
||||
if (result.type !== 'needs_credentials') return result;
|
||||
|
||||
// Needs credentials, prompt for them
|
||||
const creds = await callbacks.promptCredentials(result);
|
||||
if (creds == null) throw new Error('Canceled');
|
||||
if (result.type === 'needs_credentials') {
|
||||
const creds = await callbacks.promptCredentials(result);
|
||||
if (creds == null) throw new Error('Canceled');
|
||||
|
||||
await invoke('cmd_git_add_credential', {
|
||||
dir,
|
||||
remoteUrl: result.url,
|
||||
username: creds.username,
|
||||
password: creds.password,
|
||||
});
|
||||
await invoke('cmd_git_add_credential', {
|
||||
remoteUrl: result.url,
|
||||
username: creds.username,
|
||||
password: creds.password,
|
||||
});
|
||||
|
||||
// Pull again
|
||||
return invoke<PullResult>('cmd_git_pull', { dir });
|
||||
// Pull again after credentials
|
||||
return invoke<PullResult>('cmd_git_pull', { dir });
|
||||
}
|
||||
|
||||
if (result.type === 'uncommitted_changes') {
|
||||
callbacks.promptUncommittedChanges().then(async (strategy) => {
|
||||
if (strategy === 'cancel') return;
|
||||
|
||||
await invoke('cmd_git_reset_changes', { dir });
|
||||
return invoke<PullResult>('cmd_git_pull', { dir });
|
||||
}).then(async () => { onSuccess(); await callbacks.forceSync(); }, handleError);
|
||||
}
|
||||
|
||||
if (result.type === 'diverged') {
|
||||
callbacks.promptDiverged(result).then((strategy) => {
|
||||
if (strategy === 'cancel') return;
|
||||
|
||||
if (strategy === 'force_reset') {
|
||||
return invoke<PullResult>('cmd_git_pull_force_reset', {
|
||||
dir,
|
||||
remote: result.remote,
|
||||
branch: result.branch,
|
||||
});
|
||||
}
|
||||
|
||||
return invoke<PullResult>('cmd_git_pull_merge', {
|
||||
dir,
|
||||
remote: result.remote,
|
||||
branch: result.branch,
|
||||
});
|
||||
}).then(async () => { onSuccess(); await callbacks.forceSync(); }, handleError);
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
onSuccess,
|
||||
}),
|
||||
@@ -160,9 +223,39 @@ export const gitMutations = (dir: string, callbacks: GitCallbacks) => {
|
||||
mutationFn: (args) => invoke('cmd_git_unstage', { dir, ...args }),
|
||||
onSuccess,
|
||||
}),
|
||||
resetChanges: createFastMutation<void, string, void>({
|
||||
mutationKey: ['git', 'reset-changes', dir],
|
||||
mutationFn: () => invoke('cmd_git_reset_changes', { dir }),
|
||||
onSuccess,
|
||||
}),
|
||||
} as const;
|
||||
};
|
||||
|
||||
async function getRemotes(dir: string) {
|
||||
return invoke<GitRemote[]>('cmd_git_remotes', { dir });
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone a git repository, prompting for credentials if needed.
|
||||
*/
|
||||
export async function gitClone(
|
||||
url: string,
|
||||
dir: string,
|
||||
promptCredentials: (args: { url: string; error: string | null }) => Promise<GitCredentials | null>,
|
||||
): Promise<CloneResult> {
|
||||
const result = await invoke<CloneResult>('cmd_git_clone', { url, dir });
|
||||
if (result.type !== 'needs_credentials') return result;
|
||||
|
||||
// Prompt for credentials
|
||||
const creds = await promptCredentials({ url: result.url, error: result.error });
|
||||
if (creds == null) return {type: 'cancelled'};
|
||||
|
||||
// Store credentials and retry
|
||||
await invoke('cmd_git_add_credential', {
|
||||
remoteUrl: result.url,
|
||||
username: creds.username,
|
||||
password: creds.password,
|
||||
});
|
||||
|
||||
return invoke<CloneResult>('cmd_git_clone', { url, dir });
|
||||
}
|
||||
|
||||
@@ -1,38 +1,30 @@
|
||||
use crate::error::Error::GitNotFound;
|
||||
use crate::error::Result;
|
||||
use std::path::Path;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::process::Stdio;
|
||||
use tokio::process::Command;
|
||||
use yaak_common::command::new_xplatform_command;
|
||||
|
||||
use crate::error::Error::GitNotFound;
|
||||
#[cfg(target_os = "windows")]
|
||||
use std::os::windows::process::CommandExt;
|
||||
/// Create a git command that runs in the specified directory
|
||||
pub(crate) async fn new_binary_command(dir: &Path) -> Result<Command> {
|
||||
let mut cmd = new_binary_command_global().await?;
|
||||
cmd.arg("-C").arg(dir);
|
||||
Ok(cmd)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
|
||||
|
||||
pub(crate) fn new_binary_command(dir: &Path) -> Result<Command> {
|
||||
/// Create a git command without a specific directory (for global operations)
|
||||
pub(crate) async fn new_binary_command_global() -> Result<Command> {
|
||||
// 1. Probe that `git` exists and is runnable
|
||||
let mut probe = Command::new("git");
|
||||
let mut probe = new_xplatform_command("git");
|
||||
probe.arg("--version").stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null());
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
probe.creation_flags(CREATE_NO_WINDOW);
|
||||
}
|
||||
|
||||
let status = probe.status().map_err(|_| GitNotFound)?;
|
||||
let status = probe.status().await.map_err(|_| GitNotFound)?;
|
||||
|
||||
if !status.success() {
|
||||
return Err(GitNotFound);
|
||||
}
|
||||
|
||||
// 2. Build the reusable git command
|
||||
let mut cmd = Command::new("git");
|
||||
cmd.arg("-C").arg(dir);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
cmd.creation_flags(CREATE_NO_WINDOW);
|
||||
}
|
||||
|
||||
let cmd = new_xplatform_command("git");
|
||||
Ok(cmd)
|
||||
}
|
||||
|
||||
@@ -1,99 +1,153 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::binary::new_binary_command;
|
||||
use crate::error::Error::GenericError;
|
||||
use crate::error::Result;
|
||||
use crate::merge::do_merge;
|
||||
use crate::repository::open_repo;
|
||||
use crate::util::{bytes_to_string, get_branch_by_name, get_current_branch};
|
||||
use git2::BranchType;
|
||||
use git2::build::CheckoutBuilder;
|
||||
use log::info;
|
||||
use std::path::Path;
|
||||
|
||||
pub fn git_checkout_branch(dir: &Path, branch_name: &str, force: bool) -> Result<String> {
|
||||
if branch_name.starts_with("origin/") {
|
||||
return git_checkout_remote_branch(dir, branch_name, force);
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "snake_case", tag = "type")]
|
||||
#[ts(export, export_to = "gen_git.ts")]
|
||||
pub enum BranchDeleteResult {
|
||||
Success { message: String },
|
||||
NotFullyMerged,
|
||||
}
|
||||
|
||||
let repo = open_repo(dir)?;
|
||||
let branch = get_branch_by_name(&repo, branch_name)?;
|
||||
let branch_ref = branch.into_reference();
|
||||
let branch_tree = branch_ref.peel_to_tree()?;
|
||||
pub async fn git_checkout_branch(dir: &Path, branch_name: &str, force: bool) -> Result<String> {
|
||||
let branch_name = branch_name.trim_start_matches("origin/");
|
||||
|
||||
let mut options = CheckoutBuilder::default();
|
||||
let mut args = vec!["checkout"];
|
||||
if force {
|
||||
options.force();
|
||||
args.push("--force");
|
||||
}
|
||||
args.push(branch_name);
|
||||
|
||||
repo.checkout_tree(branch_tree.as_object(), Some(&mut options))?;
|
||||
repo.set_head(branch_ref.name().unwrap())?;
|
||||
let out = new_binary_command(dir)
|
||||
.await?
|
||||
.args(&args)
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| GenericError(format!("failed to run git checkout: {e}")))?;
|
||||
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
let combined = format!("{}{}", stdout, stderr);
|
||||
|
||||
if !out.status.success() {
|
||||
return Err(GenericError(format!("Failed to checkout: {}", combined.trim())));
|
||||
}
|
||||
|
||||
Ok(branch_name.to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn git_checkout_remote_branch(
|
||||
dir: &Path,
|
||||
branch_name: &str,
|
||||
force: bool,
|
||||
) -> Result<String> {
|
||||
let branch_name = branch_name.trim_start_matches("origin/");
|
||||
let repo = open_repo(dir)?;
|
||||
|
||||
let refname = format!("refs/remotes/origin/{}", branch_name);
|
||||
let remote_ref = repo.find_reference(&refname)?;
|
||||
let commit = remote_ref.peel_to_commit()?;
|
||||
|
||||
let mut new_branch = repo.branch(branch_name, &commit, false)?;
|
||||
let upstream_name = format!("origin/{}", branch_name);
|
||||
new_branch.set_upstream(Some(&upstream_name))?;
|
||||
|
||||
git_checkout_branch(dir, branch_name, force)
|
||||
}
|
||||
|
||||
pub fn git_create_branch(dir: &Path, name: &str) -> Result<()> {
|
||||
let repo = open_repo(dir)?;
|
||||
let head = match repo.head() {
|
||||
Ok(h) => h,
|
||||
Err(e) if e.code() == git2::ErrorCode::UnbornBranch => {
|
||||
let msg = "Cannot create branch when there are no commits";
|
||||
return Err(GenericError(msg.into()));
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
let head = head.peel_to_commit()?;
|
||||
|
||||
repo.branch(name, &head, false)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn git_delete_branch(dir: &Path, name: &str) -> Result<()> {
|
||||
let repo = open_repo(dir)?;
|
||||
let mut branch = get_branch_by_name(&repo, name)?;
|
||||
|
||||
if branch.is_head() {
|
||||
info!("Deleting head branch");
|
||||
let branches = repo.branches(Some(BranchType::Local))?;
|
||||
let other_branch = branches.into_iter().filter_map(|b| b.ok()).find(|b| !b.0.is_head());
|
||||
let other_branch = match other_branch {
|
||||
None => return Err(GenericError("Cannot delete only branch".into())),
|
||||
Some(b) => bytes_to_string(b.0.name_bytes()?)?,
|
||||
};
|
||||
|
||||
git_checkout_branch(dir, &other_branch, true)?;
|
||||
pub async fn git_create_branch(dir: &Path, name: &str, base: Option<&str>) -> Result<()> {
|
||||
let mut cmd = new_binary_command(dir).await?;
|
||||
cmd.arg("branch").arg(name);
|
||||
if let Some(base_branch) = base {
|
||||
cmd.arg(base_branch);
|
||||
}
|
||||
|
||||
branch.delete()?;
|
||||
let out =
|
||||
cmd.output().await.map_err(|e| GenericError(format!("failed to run git branch: {e}")))?;
|
||||
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
let combined = format!("{}{}", stdout, stderr);
|
||||
|
||||
if !out.status.success() {
|
||||
return Err(GenericError(format!("Failed to create branch: {}", combined.trim())));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn git_merge_branch(dir: &Path, name: &str, _force: bool) -> Result<()> {
|
||||
let repo = open_repo(dir)?;
|
||||
let local_branch = get_current_branch(&repo)?.unwrap();
|
||||
pub async fn git_delete_branch(dir: &Path, name: &str, force: bool) -> Result<BranchDeleteResult> {
|
||||
let mut cmd = new_binary_command(dir).await?;
|
||||
|
||||
let commit_to_merge = get_branch_by_name(&repo, name)?.into_reference();
|
||||
let commit_to_merge = repo.reference_to_annotated_commit(&commit_to_merge)?;
|
||||
let out =
|
||||
if force { cmd.args(["branch", "-D", name]) } else { cmd.args(["branch", "-d", name]) }
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| GenericError(format!("failed to run git branch -d: {e}")))?;
|
||||
|
||||
do_merge(&repo, &local_branch, &commit_to_merge)?;
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
let combined = format!("{}{}", stdout, stderr);
|
||||
|
||||
if !out.status.success() && stderr.to_lowercase().contains("not fully merged") {
|
||||
return Ok(BranchDeleteResult::NotFullyMerged);
|
||||
}
|
||||
|
||||
if !out.status.success() {
|
||||
return Err(GenericError(format!("Failed to delete branch: {}", combined.trim())));
|
||||
}
|
||||
|
||||
Ok(BranchDeleteResult::Success { message: combined })
|
||||
}
|
||||
|
||||
pub async fn git_merge_branch(dir: &Path, name: &str) -> Result<()> {
|
||||
let out = new_binary_command(dir)
|
||||
.await?
|
||||
.args(["merge", name])
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| GenericError(format!("failed to run git merge: {e}")))?;
|
||||
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
let combined = format!("{}{}", stdout, stderr);
|
||||
|
||||
if !out.status.success() {
|
||||
// Check for merge conflicts
|
||||
if combined.to_lowercase().contains("conflict") {
|
||||
return Err(GenericError(
|
||||
"Merge conflicts detected. Please resolve them manually.".to_string(),
|
||||
));
|
||||
}
|
||||
return Err(GenericError(format!("Failed to merge: {}", combined.trim())));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn git_delete_remote_branch(dir: &Path, name: &str) -> Result<()> {
|
||||
// Remote branch names come in as "origin/branch-name", extract the branch name
|
||||
let branch_name = name.trim_start_matches("origin/");
|
||||
|
||||
let out = new_binary_command(dir)
|
||||
.await?
|
||||
.args(["push", "origin", "--delete", branch_name])
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| GenericError(format!("failed to run git push --delete: {e}")))?;
|
||||
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
let combined = format!("{}{}", stdout, stderr);
|
||||
|
||||
if !out.status.success() {
|
||||
return Err(GenericError(format!("Failed to delete remote branch: {}", combined.trim())));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn git_rename_branch(dir: &Path, old_name: &str, new_name: &str) -> Result<()> {
|
||||
let out = new_binary_command(dir)
|
||||
.await?
|
||||
.args(["branch", "-m", old_name, new_name])
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| GenericError(format!("failed to run git branch -m: {e}")))?;
|
||||
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
let combined = format!("{}{}", stdout, stderr);
|
||||
|
||||
if !out.status.success() {
|
||||
return Err(GenericError(format!("Failed to rename branch: {}", combined.trim())));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
53
crates/yaak-git/src/clone.rs
Normal file
53
crates/yaak-git/src/clone.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use crate::binary::new_binary_command;
|
||||
use crate::error::Error::GenericError;
|
||||
use crate::error::Result;
|
||||
use log::info;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use ts_rs::TS;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "snake_case", tag = "type")]
|
||||
#[ts(export, export_to = "gen_git.ts")]
|
||||
pub enum CloneResult {
|
||||
Success,
|
||||
Cancelled,
|
||||
NeedsCredentials { url: String, error: Option<String> },
|
||||
}
|
||||
|
||||
pub async fn git_clone(url: &str, dir: &Path) -> Result<CloneResult> {
|
||||
let parent = dir.parent().ok_or_else(|| GenericError("Invalid clone directory".to_string()))?;
|
||||
fs::create_dir_all(parent)
|
||||
.map_err(|e| GenericError(format!("Failed to create directory: {e}")))?;
|
||||
let mut cmd = new_binary_command(parent).await?;
|
||||
cmd.args(["clone", url]).arg(dir).env("GIT_TERMINAL_PROMPT", "0");
|
||||
|
||||
let out =
|
||||
cmd.output().await.map_err(|e| GenericError(format!("failed to run git clone: {e}")))?;
|
||||
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
let combined = format!("{}{}", stdout, stderr);
|
||||
let combined_lower = combined.to_lowercase();
|
||||
|
||||
info!("Cloned status={}: {combined}", out.status);
|
||||
|
||||
if !out.status.success() {
|
||||
// Check for credentials error
|
||||
if combined_lower.contains("could not read") {
|
||||
return Ok(CloneResult::NeedsCredentials { url: url.to_string(), error: None });
|
||||
}
|
||||
if combined_lower.contains("unable to access")
|
||||
|| combined_lower.contains("authentication failed")
|
||||
{
|
||||
return Ok(CloneResult::NeedsCredentials {
|
||||
url: url.to_string(),
|
||||
error: Some(combined.to_string()),
|
||||
});
|
||||
}
|
||||
return Err(GenericError(format!("Failed to clone: {}", combined.trim())));
|
||||
}
|
||||
|
||||
Ok(CloneResult::Success)
|
||||
}
|
||||
@@ -3,8 +3,9 @@ use crate::error::Error::GenericError;
|
||||
use log::info;
|
||||
use std::path::Path;
|
||||
|
||||
pub fn git_commit(dir: &Path, message: &str) -> crate::error::Result<()> {
|
||||
let out = new_binary_command(dir)?.args(["commit", "--message", message]).output()?;
|
||||
pub async fn git_commit(dir: &Path, message: &str) -> crate::error::Result<()> {
|
||||
let out =
|
||||
new_binary_command(dir).await?.args(["commit", "--message", message]).output().await?;
|
||||
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
|
||||
@@ -1,24 +1,19 @@
|
||||
use crate::binary::new_binary_command;
|
||||
use crate::binary::new_binary_command_global;
|
||||
use crate::error::Error::GenericError;
|
||||
use crate::error::Result;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use std::process::Stdio;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use url::Url;
|
||||
|
||||
pub async fn git_add_credential(
|
||||
dir: &Path,
|
||||
remote_url: &str,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> Result<()> {
|
||||
pub async fn git_add_credential(remote_url: &str, username: &str, password: &str) -> Result<()> {
|
||||
let url = Url::parse(remote_url)
|
||||
.map_err(|e| GenericError(format!("Failed to parse remote url {remote_url}: {e:?}")))?;
|
||||
let protocol = url.scheme();
|
||||
let host = url.host_str().unwrap();
|
||||
let path = Some(url.path());
|
||||
|
||||
let mut child = new_binary_command(dir)?
|
||||
let mut child = new_binary_command_global()
|
||||
.await?
|
||||
.args(["credential", "approve"])
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::null())
|
||||
@@ -26,19 +21,21 @@ pub async fn git_add_credential(
|
||||
|
||||
{
|
||||
let stdin = child.stdin.as_mut().unwrap();
|
||||
writeln!(stdin, "protocol={}", protocol)?;
|
||||
writeln!(stdin, "host={}", host)?;
|
||||
stdin.write_all(format!("protocol={}\n", protocol).as_bytes()).await?;
|
||||
stdin.write_all(format!("host={}\n", host).as_bytes()).await?;
|
||||
if let Some(path) = path {
|
||||
if !path.is_empty() {
|
||||
writeln!(stdin, "path={}", path.trim_start_matches('/'))?;
|
||||
stdin
|
||||
.write_all(format!("path={}\n", path.trim_start_matches('/')).as_bytes())
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
writeln!(stdin, "username={}", username)?;
|
||||
writeln!(stdin, "password={}", password)?;
|
||||
writeln!(stdin)?; // blank line terminator
|
||||
stdin.write_all(format!("username={}\n", username).as_bytes()).await?;
|
||||
stdin.write_all(format!("password={}\n", password).as_bytes()).await?;
|
||||
stdin.write_all(b"\n").await?; // blank line terminator
|
||||
}
|
||||
|
||||
let status = child.wait()?;
|
||||
let status = child.wait().await?;
|
||||
if !status.success() {
|
||||
return Err(GenericError("Failed to approve git credential".to_string()));
|
||||
}
|
||||
|
||||
@@ -3,10 +3,12 @@ use crate::error::Error::GenericError;
|
||||
use crate::error::Result;
|
||||
use std::path::Path;
|
||||
|
||||
pub fn git_fetch_all(dir: &Path) -> Result<()> {
|
||||
let out = new_binary_command(dir)?
|
||||
pub async fn git_fetch_all(dir: &Path) -> Result<()> {
|
||||
let out = new_binary_command(dir)
|
||||
.await?
|
||||
.args(["fetch", "--all", "--prune", "--tags"])
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| GenericError(format!("failed to run git pull: {e}")))?;
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
|
||||
@@ -1,31 +1,38 @@
|
||||
mod add;
|
||||
mod binary;
|
||||
mod branch;
|
||||
mod clone;
|
||||
mod commit;
|
||||
mod credential;
|
||||
pub mod error;
|
||||
mod fetch;
|
||||
mod init;
|
||||
mod log;
|
||||
mod merge;
|
||||
|
||||
mod pull;
|
||||
mod push;
|
||||
mod remotes;
|
||||
mod repository;
|
||||
mod reset;
|
||||
mod status;
|
||||
mod unstage;
|
||||
mod util;
|
||||
|
||||
// Re-export all git functions for external use
|
||||
pub use add::git_add;
|
||||
pub use branch::{git_checkout_branch, git_create_branch, git_delete_branch, git_merge_branch};
|
||||
pub use branch::{
|
||||
BranchDeleteResult, git_checkout_branch, git_create_branch, git_delete_branch,
|
||||
git_delete_remote_branch, git_merge_branch, git_rename_branch,
|
||||
};
|
||||
pub use clone::{CloneResult, git_clone};
|
||||
pub use commit::git_commit;
|
||||
pub use credential::git_add_credential;
|
||||
pub use fetch::git_fetch_all;
|
||||
pub use init::git_init;
|
||||
pub use log::{GitCommit, git_log};
|
||||
pub use pull::{PullResult, git_pull};
|
||||
pub use pull::{PullResult, git_pull, git_pull_force_reset, git_pull_merge};
|
||||
pub use push::{PushResult, git_push};
|
||||
pub use remotes::{GitRemote, git_add_remote, git_remotes, git_rm_remote};
|
||||
pub use reset::git_reset_changes;
|
||||
pub use status::{GitStatusSummary, git_status};
|
||||
pub use unstage::git_unstage;
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
use crate::error::Error::MergeConflicts;
|
||||
use crate::util::bytes_to_string;
|
||||
use git2::{AnnotatedCommit, Branch, IndexEntry, Reference, Repository};
|
||||
use log::{debug, info};
|
||||
|
||||
pub(crate) fn do_merge(
|
||||
repo: &Repository,
|
||||
local_branch: &Branch,
|
||||
commit_to_merge: &AnnotatedCommit,
|
||||
) -> crate::error::Result<()> {
|
||||
debug!("Merging remote branches");
|
||||
let analysis = repo.merge_analysis(&[&commit_to_merge])?;
|
||||
|
||||
if analysis.0.is_fast_forward() {
|
||||
let refname = bytes_to_string(local_branch.get().name_bytes())?;
|
||||
match repo.find_reference(&refname) {
|
||||
Ok(mut r) => {
|
||||
merge_fast_forward(repo, &mut r, &commit_to_merge)?;
|
||||
}
|
||||
Err(_) => {
|
||||
// The branch doesn't exist, so set the reference to the commit directly. Usually
|
||||
// this is because you are pulling into an empty repository.
|
||||
repo.reference(
|
||||
&refname,
|
||||
commit_to_merge.id(),
|
||||
true,
|
||||
&format!("Setting {} to {}", refname, commit_to_merge.id()),
|
||||
)?;
|
||||
repo.set_head(&refname)?;
|
||||
repo.checkout_head(Some(
|
||||
git2::build::CheckoutBuilder::default()
|
||||
.allow_conflicts(true)
|
||||
.conflict_style_merge(true)
|
||||
.force(),
|
||||
))?;
|
||||
}
|
||||
};
|
||||
} else if analysis.0.is_normal() {
|
||||
let head_commit = repo.reference_to_annotated_commit(&repo.head()?)?;
|
||||
merge_normal(repo, &head_commit, commit_to_merge)?;
|
||||
} else {
|
||||
debug!("Skipping merge. Nothing to do")
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn merge_fast_forward(
|
||||
repo: &Repository,
|
||||
local_reference: &mut Reference,
|
||||
remote_commit: &AnnotatedCommit,
|
||||
) -> crate::error::Result<()> {
|
||||
info!("Performing fast forward");
|
||||
let name = match local_reference.name() {
|
||||
Some(s) => s.to_string(),
|
||||
None => String::from_utf8_lossy(local_reference.name_bytes()).to_string(),
|
||||
};
|
||||
let msg = format!("Fast-Forward: Setting {} to id: {}", name, remote_commit.id());
|
||||
local_reference.set_target(remote_commit.id(), &msg)?;
|
||||
repo.set_head(&name)?;
|
||||
repo.checkout_head(Some(
|
||||
git2::build::CheckoutBuilder::default()
|
||||
// For some reason, the force is required to make the working directory actually get
|
||||
// updated I suspect we should be adding some logic to handle dirty working directory
|
||||
// states, but this is just an example so maybe not.
|
||||
.force(),
|
||||
))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn merge_normal(
|
||||
repo: &Repository,
|
||||
local: &AnnotatedCommit,
|
||||
remote: &AnnotatedCommit,
|
||||
) -> crate::error::Result<()> {
|
||||
info!("Performing normal merge");
|
||||
let local_tree = repo.find_commit(local.id())?.tree()?;
|
||||
let remote_tree = repo.find_commit(remote.id())?.tree()?;
|
||||
let ancestor = repo.find_commit(repo.merge_base(local.id(), remote.id())?)?.tree()?;
|
||||
|
||||
let mut idx = repo.merge_trees(&ancestor, &local_tree, &remote_tree, None)?;
|
||||
|
||||
if idx.has_conflicts() {
|
||||
let conflicts = idx.conflicts()?;
|
||||
for conflict in conflicts {
|
||||
if let Ok(conflict) = conflict {
|
||||
print_conflict(&conflict);
|
||||
}
|
||||
}
|
||||
return Err(MergeConflicts);
|
||||
}
|
||||
|
||||
let result_tree = repo.find_tree(idx.write_tree_to(repo)?)?;
|
||||
// now create the merge commit
|
||||
let msg = format!("Merge: {} into {}", remote.id(), local.id());
|
||||
let sig = repo.signature()?;
|
||||
let local_commit = repo.find_commit(local.id())?;
|
||||
let remote_commit = repo.find_commit(remote.id())?;
|
||||
|
||||
// Do our merge commit and set current branch head to that commit.
|
||||
let _merge_commit = repo.commit(
|
||||
Some("HEAD"),
|
||||
&sig,
|
||||
&sig,
|
||||
&msg,
|
||||
&result_tree,
|
||||
&[&local_commit, &remote_commit],
|
||||
)?;
|
||||
|
||||
// Set working tree to match head.
|
||||
repo.checkout_head(None)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_conflict(conflict: &git2::IndexConflict) {
|
||||
let ancestor = conflict.ancestor.as_ref().map(path_from_index_entry);
|
||||
let ours = conflict.our.as_ref().map(path_from_index_entry);
|
||||
let theirs = conflict.their.as_ref().map(path_from_index_entry);
|
||||
|
||||
println!("Conflict detected:");
|
||||
if let Some(path) = ancestor {
|
||||
println!(" Common ancestor: {:?}", path);
|
||||
}
|
||||
if let Some(path) = ours {
|
||||
println!(" Ours: {:?}", path);
|
||||
}
|
||||
if let Some(path) = theirs {
|
||||
println!(" Theirs: {:?}", path);
|
||||
}
|
||||
}
|
||||
|
||||
fn path_from_index_entry(entry: &IndexEntry) -> String {
|
||||
String::from_utf8_lossy(entry.path.as_slice()).into_owned()
|
||||
}
|
||||
@@ -15,49 +15,159 @@ pub enum PullResult {
|
||||
Success { message: String },
|
||||
UpToDate,
|
||||
NeedsCredentials { url: String, error: Option<String> },
|
||||
Diverged { remote: String, branch: String },
|
||||
UncommittedChanges,
|
||||
}
|
||||
|
||||
pub fn git_pull(dir: &Path) -> Result<PullResult> {
|
||||
fn has_uncommitted_changes(dir: &Path) -> Result<bool> {
|
||||
let repo = open_repo(dir)?;
|
||||
let branch_name = get_current_branch_name(&repo)?;
|
||||
let remote = get_default_remote_in_repo(&repo)?;
|
||||
let remote_name = remote.name().ok_or(GenericError("Failed to get remote name".to_string()))?;
|
||||
let remote_url = remote.url().ok_or(GenericError("Failed to get remote url".to_string()))?;
|
||||
let mut opts = git2::StatusOptions::new();
|
||||
opts.include_ignored(false).include_untracked(false);
|
||||
let statuses = repo.statuses(Some(&mut opts))?;
|
||||
Ok(statuses.iter().any(|e| e.status() != git2::Status::CURRENT))
|
||||
}
|
||||
|
||||
let out = new_binary_command(dir)?
|
||||
.args(["pull", &remote_name, &branch_name])
|
||||
pub async fn git_pull(dir: &Path) -> Result<PullResult> {
|
||||
if has_uncommitted_changes(dir)? {
|
||||
return Ok(PullResult::UncommittedChanges);
|
||||
}
|
||||
|
||||
// Extract all git2 data before any await points (git2 types are not Send)
|
||||
let (branch_name, remote_name, remote_url) = {
|
||||
let repo = open_repo(dir)?;
|
||||
let branch_name = get_current_branch_name(&repo)?;
|
||||
let remote = get_default_remote_in_repo(&repo)?;
|
||||
let remote_name =
|
||||
remote.name().ok_or(GenericError("Failed to get remote name".to_string()))?.to_string();
|
||||
let remote_url =
|
||||
remote.url().ok_or(GenericError("Failed to get remote url".to_string()))?.to_string();
|
||||
(branch_name, remote_name, remote_url)
|
||||
};
|
||||
|
||||
// Step 1: fetch the specific branch
|
||||
// NOTE: We use fetch + merge instead of `git pull` to avoid conflicts with
|
||||
// global git config (e.g. pull.ff=only) and the background fetch --all.
|
||||
let fetch_out = new_binary_command(dir)
|
||||
.await?
|
||||
.args(["fetch", &remote_name, &branch_name])
|
||||
.env("GIT_TERMINAL_PROMPT", "0")
|
||||
.output()
|
||||
.map_err(|e| GenericError(format!("failed to run git pull: {e}")))?;
|
||||
.await
|
||||
.map_err(|e| GenericError(format!("failed to run git fetch: {e}")))?;
|
||||
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
let combined = stdout + stderr;
|
||||
let fetch_stdout = String::from_utf8_lossy(&fetch_out.stdout);
|
||||
let fetch_stderr = String::from_utf8_lossy(&fetch_out.stderr);
|
||||
let fetch_combined = format!("{fetch_stdout}{fetch_stderr}");
|
||||
|
||||
info!("Pulled status={} {combined}", out.status);
|
||||
info!("Fetched status={} {fetch_combined}", fetch_out.status);
|
||||
|
||||
if combined.to_lowercase().contains("could not read") {
|
||||
if fetch_combined.to_lowercase().contains("could not read") {
|
||||
return Ok(PullResult::NeedsCredentials { url: remote_url.to_string(), error: None });
|
||||
}
|
||||
|
||||
if combined.to_lowercase().contains("unable to access") {
|
||||
if fetch_combined.to_lowercase().contains("unable to access") {
|
||||
return Ok(PullResult::NeedsCredentials {
|
||||
url: remote_url.to_string(),
|
||||
error: Some(combined.to_string()),
|
||||
error: Some(fetch_combined.to_string()),
|
||||
});
|
||||
}
|
||||
|
||||
if !out.status.success() {
|
||||
return Err(GenericError(format!("Failed to pull {combined}")));
|
||||
if !fetch_out.status.success() {
|
||||
return Err(GenericError(format!("Failed to fetch: {fetch_combined}")));
|
||||
}
|
||||
|
||||
if combined.to_lowercase().contains("up to date") {
|
||||
// Step 2: merge the fetched branch
|
||||
let ref_name = format!("{}/{}", remote_name, branch_name);
|
||||
let merge_out = new_binary_command(dir)
|
||||
.await?
|
||||
.args(["merge", "--ff-only", &ref_name])
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| GenericError(format!("failed to run git merge: {e}")))?;
|
||||
|
||||
let merge_stdout = String::from_utf8_lossy(&merge_out.stdout);
|
||||
let merge_stderr = String::from_utf8_lossy(&merge_out.stderr);
|
||||
let merge_combined = format!("{merge_stdout}{merge_stderr}");
|
||||
|
||||
info!("Merged status={} {merge_combined}", merge_out.status);
|
||||
|
||||
if !merge_out.status.success() {
|
||||
let merge_lower = merge_combined.to_lowercase();
|
||||
if merge_lower.contains("cannot fast-forward")
|
||||
|| merge_lower.contains("not possible to fast-forward")
|
||||
|| merge_lower.contains("diverged")
|
||||
{
|
||||
return Ok(PullResult::Diverged { remote: remote_name, branch: branch_name });
|
||||
}
|
||||
return Err(GenericError(format!("Failed to merge: {merge_combined}")));
|
||||
}
|
||||
|
||||
if merge_combined.to_lowercase().contains("up to date") {
|
||||
return Ok(PullResult::UpToDate);
|
||||
}
|
||||
|
||||
Ok(PullResult::Success { message: format!("Pulled from {}/{}", remote_name, branch_name) })
|
||||
}
|
||||
|
||||
pub async fn git_pull_force_reset(dir: &Path, remote: &str, branch: &str) -> Result<PullResult> {
|
||||
// Step 1: fetch the remote
|
||||
let fetch_out = new_binary_command(dir)
|
||||
.await?
|
||||
.args(["fetch", remote])
|
||||
.env("GIT_TERMINAL_PROMPT", "0")
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| GenericError(format!("failed to run git fetch: {e}")))?;
|
||||
|
||||
if !fetch_out.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&fetch_out.stderr);
|
||||
return Err(GenericError(format!("Failed to fetch: {stderr}")));
|
||||
}
|
||||
|
||||
// Step 2: reset --hard to remote/branch
|
||||
let ref_name = format!("{}/{}", remote, branch);
|
||||
let reset_out = new_binary_command(dir)
|
||||
.await?
|
||||
.args(["reset", "--hard", &ref_name])
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| GenericError(format!("failed to run git reset: {e}")))?;
|
||||
|
||||
if !reset_out.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&reset_out.stderr);
|
||||
return Err(GenericError(format!("Failed to reset: {}", stderr.trim())));
|
||||
}
|
||||
|
||||
Ok(PullResult::Success { message: format!("Reset to {}/{}", remote, branch) })
|
||||
}
|
||||
|
||||
pub async fn git_pull_merge(dir: &Path, remote: &str, branch: &str) -> Result<PullResult> {
|
||||
let out = new_binary_command(dir)
|
||||
.await?
|
||||
.args(["pull", "--no-rebase", remote, branch])
|
||||
.env("GIT_TERMINAL_PROMPT", "0")
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| GenericError(format!("failed to run git pull --no-rebase: {e}")))?;
|
||||
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
let combined = format!("{}{}", stdout, stderr);
|
||||
|
||||
info!("Pull merge status={} {combined}", out.status);
|
||||
|
||||
if !out.status.success() {
|
||||
if combined.to_lowercase().contains("conflict") {
|
||||
return Err(GenericError(
|
||||
"Merge conflicts detected. Please resolve them manually.".to_string(),
|
||||
));
|
||||
}
|
||||
return Err(GenericError(format!("Failed to merge pull: {}", combined.trim())));
|
||||
}
|
||||
|
||||
Ok(PullResult::Success { message: format!("Merged from {}/{}", remote, branch) })
|
||||
}
|
||||
|
||||
// pub(crate) fn git_pull_old(dir: &Path) -> Result<PullResult> {
|
||||
// let repo = open_repo(dir)?;
|
||||
//
|
||||
|
||||
@@ -17,17 +17,25 @@ pub enum PushResult {
|
||||
NeedsCredentials { url: String, error: Option<String> },
|
||||
}
|
||||
|
||||
pub fn git_push(dir: &Path) -> Result<PushResult> {
|
||||
let repo = open_repo(dir)?;
|
||||
let branch_name = get_current_branch_name(&repo)?;
|
||||
let remote = get_default_remote_for_push_in_repo(&repo)?;
|
||||
let remote_name = remote.name().ok_or(GenericError("Failed to get remote name".to_string()))?;
|
||||
let remote_url = remote.url().ok_or(GenericError("Failed to get remote url".to_string()))?;
|
||||
pub async fn git_push(dir: &Path) -> Result<PushResult> {
|
||||
// Extract all git2 data before any await points (git2 types are not Send)
|
||||
let (branch_name, remote_name, remote_url) = {
|
||||
let repo = open_repo(dir)?;
|
||||
let branch_name = get_current_branch_name(&repo)?;
|
||||
let remote = get_default_remote_for_push_in_repo(&repo)?;
|
||||
let remote_name =
|
||||
remote.name().ok_or(GenericError("Failed to get remote name".to_string()))?.to_string();
|
||||
let remote_url =
|
||||
remote.url().ok_or(GenericError("Failed to get remote url".to_string()))?.to_string();
|
||||
(branch_name, remote_name, remote_url)
|
||||
};
|
||||
|
||||
let out = new_binary_command(dir)?
|
||||
let out = new_binary_command(dir)
|
||||
.await?
|
||||
.args(["push", &remote_name, &branch_name])
|
||||
.env("GIT_TERMINAL_PROMPT", "0")
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| GenericError(format!("failed to run git push: {e}")))?;
|
||||
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
|
||||
20
crates/yaak-git/src/reset.rs
Normal file
20
crates/yaak-git/src/reset.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
use crate::binary::new_binary_command;
|
||||
use crate::error::Error::GenericError;
|
||||
use crate::error::Result;
|
||||
use std::path::Path;
|
||||
|
||||
pub async fn git_reset_changes(dir: &Path) -> Result<()> {
|
||||
let out = new_binary_command(dir)
|
||||
.await?
|
||||
.args(["reset", "--hard", "HEAD"])
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| GenericError(format!("failed to run git reset: {e}")))?;
|
||||
|
||||
if !out.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
return Err(GenericError(format!("Failed to reset: {}", stderr.trim())));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -18,6 +18,8 @@ pub struct GitStatusSummary {
|
||||
pub origins: Vec<String>,
|
||||
pub local_branches: Vec<String>,
|
||||
pub remote_branches: Vec<String>,
|
||||
pub ahead: u32,
|
||||
pub behind: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
|
||||
@@ -160,6 +162,18 @@ pub fn git_status(dir: &Path) -> crate::error::Result<GitStatusSummary> {
|
||||
let local_branches = local_branch_names(&repo)?;
|
||||
let remote_branches = remote_branch_names(&repo)?;
|
||||
|
||||
// Compute ahead/behind relative to remote tracking branch
|
||||
let (ahead, behind) = (|| -> Option<(usize, usize)> {
|
||||
let head = repo.head().ok()?;
|
||||
let local_oid = head.target()?;
|
||||
let branch_name = head.shorthand()?;
|
||||
let upstream_ref =
|
||||
repo.find_branch(&format!("origin/{branch_name}"), git2::BranchType::Remote).ok()?;
|
||||
let upstream_oid = upstream_ref.get().target()?;
|
||||
repo.graph_ahead_behind(local_oid, upstream_oid).ok()
|
||||
})()
|
||||
.unwrap_or((0, 0));
|
||||
|
||||
Ok(GitStatusSummary {
|
||||
entries,
|
||||
origins,
|
||||
@@ -168,5 +182,7 @@ pub fn git_status(dir: &Path) -> crate::error::Result<GitStatusSummary> {
|
||||
head_ref_shorthand,
|
||||
local_branches,
|
||||
remote_branches,
|
||||
ahead: ahead as u32,
|
||||
behind: behind as u32,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -47,10 +47,6 @@ pub(crate) fn remote_branch_names(repo: &Repository) -> Result<Vec<String>> {
|
||||
Ok(branches)
|
||||
}
|
||||
|
||||
pub(crate) fn get_branch_by_name<'s>(repo: &'s Repository, name: &str) -> Result<Branch<'s>> {
|
||||
Ok(repo.find_branch(name, BranchType::Local)?)
|
||||
}
|
||||
|
||||
pub(crate) fn bytes_to_string(bytes: &[u8]) -> Result<String> {
|
||||
Ok(String::from_utf8(bytes.to_vec())?)
|
||||
}
|
||||
|
||||
@@ -22,5 +22,6 @@ tokio-stream = "0.1.14"
|
||||
tonic = { version = "0.12.3", default-features = false, features = ["transport"] }
|
||||
tonic-reflection = "0.12.3"
|
||||
uuid = { version = "1.7.0", features = ["v4"] }
|
||||
yaak-common = { workspace = true }
|
||||
yaak-tls = { workspace = true }
|
||||
thiserror = "2.0.17"
|
||||
|
||||
@@ -115,14 +115,18 @@ impl GrpcConnection {
|
||||
Ok(client.unary(req, path, codec).await?)
|
||||
}
|
||||
|
||||
pub async fn streaming(
|
||||
pub async fn streaming<F>(
|
||||
&self,
|
||||
service: &str,
|
||||
method: &str,
|
||||
stream: ReceiverStream<String>,
|
||||
metadata: &BTreeMap<String, String>,
|
||||
client_cert: Option<ClientCertificateConfig>,
|
||||
) -> Result<Response<Streaming<DynamicMessage>>> {
|
||||
on_message: F,
|
||||
) -> Result<Response<Streaming<DynamicMessage>>>
|
||||
where
|
||||
F: Fn(std::result::Result<String, String>) + Send + Sync + Clone + 'static,
|
||||
{
|
||||
let method = &self.method(&service, &method).await?;
|
||||
let mapped_stream = {
|
||||
let input_message = method.input();
|
||||
@@ -131,31 +135,39 @@ impl GrpcConnection {
|
||||
let md = metadata.clone();
|
||||
let use_reflection = self.use_reflection.clone();
|
||||
let client_cert = client_cert.clone();
|
||||
stream.filter_map(move |json| {
|
||||
let pool = pool.clone();
|
||||
let uri = uri.clone();
|
||||
let input_message = input_message.clone();
|
||||
let md = md.clone();
|
||||
let use_reflection = use_reflection.clone();
|
||||
let client_cert = client_cert.clone();
|
||||
tokio::runtime::Handle::current().block_on(async move {
|
||||
if use_reflection {
|
||||
if let Err(e) =
|
||||
reflect_types_for_message(pool, &uri, &json, &md, client_cert).await
|
||||
{
|
||||
warn!("Failed to resolve Any types: {e}");
|
||||
stream
|
||||
.then(move |json| {
|
||||
let pool = pool.clone();
|
||||
let uri = uri.clone();
|
||||
let input_message = input_message.clone();
|
||||
let md = md.clone();
|
||||
let use_reflection = use_reflection.clone();
|
||||
let client_cert = client_cert.clone();
|
||||
let on_message = on_message.clone();
|
||||
let json_clone = json.clone();
|
||||
async move {
|
||||
if use_reflection {
|
||||
if let Err(e) =
|
||||
reflect_types_for_message(pool, &uri, &json, &md, client_cert).await
|
||||
{
|
||||
warn!("Failed to resolve Any types: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut de = Deserializer::from_str(&json);
|
||||
match DynamicMessage::deserialize(input_message, &mut de) {
|
||||
Ok(m) => Some(m),
|
||||
Err(e) => {
|
||||
warn!("Failed to deserialize message: {e}");
|
||||
None
|
||||
let mut de = Deserializer::from_str(&json);
|
||||
match DynamicMessage::deserialize(input_message, &mut de) {
|
||||
Ok(m) => {
|
||||
on_message(Ok(json_clone));
|
||||
Some(m)
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to deserialize message: {e}");
|
||||
on_message(Err(e.to_string()));
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
.filter_map(|x| x)
|
||||
};
|
||||
|
||||
let mut client = tonic::client::Grpc::with_origin(self.conn.clone(), self.uri.clone());
|
||||
@@ -169,14 +181,18 @@ impl GrpcConnection {
|
||||
Ok(client.streaming(req, path, codec).await?)
|
||||
}
|
||||
|
||||
pub async fn client_streaming(
|
||||
pub async fn client_streaming<F>(
|
||||
&self,
|
||||
service: &str,
|
||||
method: &str,
|
||||
stream: ReceiverStream<String>,
|
||||
metadata: &BTreeMap<String, String>,
|
||||
client_cert: Option<ClientCertificateConfig>,
|
||||
) -> Result<Response<DynamicMessage>> {
|
||||
on_message: F,
|
||||
) -> Result<Response<DynamicMessage>>
|
||||
where
|
||||
F: Fn(std::result::Result<String, String>) + Send + Sync + Clone + 'static,
|
||||
{
|
||||
let method = &self.method(&service, &method).await?;
|
||||
let mapped_stream = {
|
||||
let input_message = method.input();
|
||||
@@ -185,31 +201,39 @@ impl GrpcConnection {
|
||||
let md = metadata.clone();
|
||||
let use_reflection = self.use_reflection.clone();
|
||||
let client_cert = client_cert.clone();
|
||||
stream.filter_map(move |json| {
|
||||
let pool = pool.clone();
|
||||
let uri = uri.clone();
|
||||
let input_message = input_message.clone();
|
||||
let md = md.clone();
|
||||
let use_reflection = use_reflection.clone();
|
||||
let client_cert = client_cert.clone();
|
||||
tokio::runtime::Handle::current().block_on(async move {
|
||||
if use_reflection {
|
||||
if let Err(e) =
|
||||
reflect_types_for_message(pool, &uri, &json, &md, client_cert).await
|
||||
{
|
||||
warn!("Failed to resolve Any types: {e}");
|
||||
stream
|
||||
.then(move |json| {
|
||||
let pool = pool.clone();
|
||||
let uri = uri.clone();
|
||||
let input_message = input_message.clone();
|
||||
let md = md.clone();
|
||||
let use_reflection = use_reflection.clone();
|
||||
let client_cert = client_cert.clone();
|
||||
let on_message = on_message.clone();
|
||||
let json_clone = json.clone();
|
||||
async move {
|
||||
if use_reflection {
|
||||
if let Err(e) =
|
||||
reflect_types_for_message(pool, &uri, &json, &md, client_cert).await
|
||||
{
|
||||
warn!("Failed to resolve Any types: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut de = Deserializer::from_str(&json);
|
||||
match DynamicMessage::deserialize(input_message, &mut de) {
|
||||
Ok(m) => Some(m),
|
||||
Err(e) => {
|
||||
warn!("Failed to deserialize message: {e}");
|
||||
None
|
||||
let mut de = Deserializer::from_str(&json);
|
||||
match DynamicMessage::deserialize(input_message, &mut de) {
|
||||
Ok(m) => {
|
||||
on_message(Ok(json_clone));
|
||||
Some(m)
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to deserialize message: {e}");
|
||||
on_message(Err(e.to_string()));
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
.filter_map(|x| x)
|
||||
};
|
||||
|
||||
let mut client = tonic::client::Grpc::with_origin(self.conn.clone(), self.uri.clone());
|
||||
@@ -316,10 +340,9 @@ impl GrpcHandle {
|
||||
metadata: &BTreeMap<String, String>,
|
||||
validate_certificates: bool,
|
||||
client_cert: Option<ClientCertificateConfig>,
|
||||
skip_cache: bool,
|
||||
) -> Result<Vec<ServiceDefinition>> {
|
||||
// Ensure we have a pool; reflect only if missing
|
||||
if skip_cache || self.get_pool(id, uri, proto_files).is_none() {
|
||||
if self.get_pool(id, uri, proto_files).is_none() {
|
||||
info!("Reflecting gRPC services for {} at {}", id, uri);
|
||||
self.reflect(id, uri, proto_files, metadata, validate_certificates, client_cert)
|
||||
.await?;
|
||||
|
||||
@@ -16,12 +16,12 @@ use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use tokio::fs;
|
||||
use tokio::process::Command;
|
||||
use tokio::sync::RwLock;
|
||||
use tonic::codegen::http::uri::PathAndQuery;
|
||||
use tonic::transport::Uri;
|
||||
use tonic_reflection::pb::v1::server_reflection_request::MessageRequest;
|
||||
use tonic_reflection::pb::v1::server_reflection_response::MessageResponse;
|
||||
use yaak_common::command::new_xplatform_command;
|
||||
use yaak_tls::ClientCertificateConfig;
|
||||
|
||||
pub async fn fill_pool_from_files(
|
||||
@@ -91,11 +91,11 @@ pub async fn fill_pool_from_files(
|
||||
|
||||
info!("Invoking protoc with {}", args.join(" "));
|
||||
|
||||
let out = Command::new(&config.protoc_bin_path)
|
||||
.args(&args)
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| GenericError(format!("Failed to run protoc: {}", e)))?;
|
||||
let mut cmd = new_xplatform_command(&config.protoc_bin_path);
|
||||
cmd.args(&args);
|
||||
|
||||
let out =
|
||||
cmd.output().await.map_err(|e| GenericError(format!("Failed to run protoc: {}", e)))?;
|
||||
|
||||
if !out.status.success() {
|
||||
return Err(GenericError(format!(
|
||||
|
||||
@@ -8,10 +8,11 @@ publish = false
|
||||
async-compression = { version = "0.4", features = ["tokio", "gzip", "deflate", "brotli", "zstd"] }
|
||||
async-trait = "0.1"
|
||||
brotli = "7"
|
||||
bytes = "1.5.0"
|
||||
bytes = "1.11.1"
|
||||
cookie = "0.18.1"
|
||||
flate2 = "1"
|
||||
futures-util = "0.3"
|
||||
http-body = "1"
|
||||
url = "2"
|
||||
zstd = "0.13"
|
||||
hyper-util = { version = "0.1.17", default-features = false, features = ["client-legacy"] }
|
||||
|
||||
@@ -2,6 +2,8 @@ use crate::dns::LocalhostResolver;
|
||||
use crate::error::Result;
|
||||
use log::{debug, info, warn};
|
||||
use reqwest::{Client, Proxy, redirect};
|
||||
use std::sync::Arc;
|
||||
use yaak_models::models::DnsOverride;
|
||||
use yaak_tls::{ClientCertificateConfig, get_tls_config};
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -28,10 +30,14 @@ pub struct HttpConnectionOptions {
|
||||
pub validate_certificates: bool,
|
||||
pub proxy: HttpConnectionProxySetting,
|
||||
pub client_certificate: Option<ClientCertificateConfig>,
|
||||
pub dns_overrides: Vec<DnsOverride>,
|
||||
}
|
||||
|
||||
impl HttpConnectionOptions {
|
||||
pub(crate) fn build_client(&self) -> Result<Client> {
|
||||
/// Build a reqwest Client and return it along with the DNS resolver.
|
||||
/// The resolver is returned separately so it can be configured per-request
|
||||
/// to emit DNS timing events to the appropriate channel.
|
||||
pub(crate) fn build_client(&self) -> Result<(Client, Arc<LocalhostResolver>)> {
|
||||
let mut client = Client::builder()
|
||||
.connection_verbose(true)
|
||||
.redirect(redirect::Policy::none())
|
||||
@@ -40,15 +46,19 @@ impl HttpConnectionOptions {
|
||||
.no_brotli()
|
||||
.no_deflate()
|
||||
.referer(false)
|
||||
.tls_info(true);
|
||||
.tls_info(true)
|
||||
// Disable connection pooling to ensure DNS resolution happens on each request
|
||||
// This is needed so we can emit DNS timing events for each request
|
||||
.pool_max_idle_per_host(0);
|
||||
|
||||
// Configure TLS with optional client certificate
|
||||
let config =
|
||||
get_tls_config(self.validate_certificates, true, self.client_certificate.clone())?;
|
||||
client = client.use_preconfigured_tls(config);
|
||||
|
||||
// Configure DNS resolver
|
||||
client = client.dns_resolver(LocalhostResolver::new());
|
||||
// Configure DNS resolver - keep a reference to configure per-request
|
||||
let resolver = LocalhostResolver::new(self.dns_overrides.clone());
|
||||
client = client.dns_resolver(resolver.clone());
|
||||
|
||||
// Configure proxy
|
||||
match self.proxy.clone() {
|
||||
@@ -69,7 +79,7 @@ impl HttpConnectionOptions {
|
||||
self.client_certificate.is_some()
|
||||
);
|
||||
|
||||
Ok(client.build()?)
|
||||
Ok((client.build()?, resolver))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,53 +1,185 @@
|
||||
use crate::sender::HttpResponseEvent;
|
||||
use hyper_util::client::legacy::connect::dns::{
|
||||
GaiResolver as HyperGaiResolver, Name as HyperName,
|
||||
};
|
||||
use log::info;
|
||||
use reqwest::dns::{Addrs, Name, Resolve, Resolving};
|
||||
use std::collections::HashMap;
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
use tokio::sync::{RwLock, mpsc};
|
||||
use tower_service::Service;
|
||||
use yaak_models::models::DnsOverride;
|
||||
|
||||
/// Stores resolved addresses for a hostname override
|
||||
#[derive(Clone)]
|
||||
pub struct ResolvedOverride {
|
||||
pub ipv4: Vec<Ipv4Addr>,
|
||||
pub ipv6: Vec<Ipv6Addr>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct LocalhostResolver {
|
||||
fallback: HyperGaiResolver,
|
||||
event_tx: Arc<RwLock<Option<mpsc::Sender<HttpResponseEvent>>>>,
|
||||
overrides: Arc<HashMap<String, ResolvedOverride>>,
|
||||
}
|
||||
|
||||
impl LocalhostResolver {
|
||||
pub fn new() -> Arc<Self> {
|
||||
pub fn new(dns_overrides: Vec<DnsOverride>) -> Arc<Self> {
|
||||
let resolver = HyperGaiResolver::new();
|
||||
Arc::new(Self { fallback: resolver })
|
||||
|
||||
// Pre-parse DNS overrides into a lookup map
|
||||
let mut overrides = HashMap::new();
|
||||
for o in dns_overrides {
|
||||
if !o.enabled {
|
||||
continue;
|
||||
}
|
||||
let hostname = o.hostname.to_lowercase();
|
||||
|
||||
let ipv4: Vec<Ipv4Addr> =
|
||||
o.ipv4.iter().filter_map(|s| s.parse::<Ipv4Addr>().ok()).collect();
|
||||
|
||||
let ipv6: Vec<Ipv6Addr> =
|
||||
o.ipv6.iter().filter_map(|s| s.parse::<Ipv6Addr>().ok()).collect();
|
||||
|
||||
// Only add if at least one address is valid
|
||||
if !ipv4.is_empty() || !ipv6.is_empty() {
|
||||
overrides.insert(hostname, ResolvedOverride { ipv4, ipv6 });
|
||||
}
|
||||
}
|
||||
|
||||
Arc::new(Self {
|
||||
fallback: resolver,
|
||||
event_tx: Arc::new(RwLock::new(None)),
|
||||
overrides: Arc::new(overrides),
|
||||
})
|
||||
}
|
||||
|
||||
/// Set the event sender for the current request.
|
||||
/// This should be called before each request to direct DNS events
|
||||
/// to the appropriate channel.
|
||||
pub async fn set_event_sender(&self, tx: Option<mpsc::Sender<HttpResponseEvent>>) {
|
||||
let mut guard = self.event_tx.write().await;
|
||||
*guard = tx;
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve for LocalhostResolver {
|
||||
fn resolve(&self, name: Name) -> Resolving {
|
||||
let host = name.as_str().to_lowercase();
|
||||
let event_tx = self.event_tx.clone();
|
||||
let overrides = self.overrides.clone();
|
||||
|
||||
info!("DNS resolve called for: {}", host);
|
||||
|
||||
// Check for DNS override first
|
||||
if let Some(resolved) = overrides.get(&host) {
|
||||
log::debug!("DNS override found for: {}", host);
|
||||
let hostname = host.clone();
|
||||
let mut addrs: Vec<SocketAddr> = Vec::new();
|
||||
|
||||
// Add IPv4 addresses
|
||||
for ip in &resolved.ipv4 {
|
||||
addrs.push(SocketAddr::new(IpAddr::V4(*ip), 0));
|
||||
}
|
||||
|
||||
// Add IPv6 addresses
|
||||
for ip in &resolved.ipv6 {
|
||||
addrs.push(SocketAddr::new(IpAddr::V6(*ip), 0));
|
||||
}
|
||||
|
||||
let addresses: Vec<String> = addrs.iter().map(|a| a.ip().to_string()).collect();
|
||||
|
||||
return Box::pin(async move {
|
||||
// Emit DNS event for override
|
||||
let guard = event_tx.read().await;
|
||||
if let Some(tx) = guard.as_ref() {
|
||||
let _ = tx
|
||||
.send(HttpResponseEvent::DnsResolved {
|
||||
hostname,
|
||||
addresses,
|
||||
duration: 0,
|
||||
overridden: true,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok::<Addrs, Box<dyn std::error::Error + Send + Sync>>(Box::new(addrs.into_iter()))
|
||||
});
|
||||
}
|
||||
|
||||
// Check for .localhost suffix
|
||||
let is_localhost = host.ends_with(".localhost");
|
||||
if is_localhost {
|
||||
let hostname = host.clone();
|
||||
// Port 0 is fine; reqwest replaces it with the URL's explicit
|
||||
// port or the scheme’s default (80/443, etc.).
|
||||
// (See docs note below.)
|
||||
// port or the scheme's default (80/443, etc.).
|
||||
let addrs: Vec<SocketAddr> = vec![
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
|
||||
SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 0),
|
||||
];
|
||||
|
||||
let addresses: Vec<String> = addrs.iter().map(|a| a.ip().to_string()).collect();
|
||||
|
||||
return Box::pin(async move {
|
||||
// Emit DNS event for localhost resolution
|
||||
let guard = event_tx.read().await;
|
||||
if let Some(tx) = guard.as_ref() {
|
||||
let _ = tx
|
||||
.send(HttpResponseEvent::DnsResolved {
|
||||
hostname,
|
||||
addresses,
|
||||
duration: 0,
|
||||
overridden: false,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok::<Addrs, Box<dyn std::error::Error + Send + Sync>>(Box::new(addrs.into_iter()))
|
||||
});
|
||||
}
|
||||
|
||||
// Fall back to system DNS
|
||||
let mut fallback = self.fallback.clone();
|
||||
let name_str = name.as_str().to_string();
|
||||
let hostname = host.clone();
|
||||
|
||||
Box::pin(async move {
|
||||
match HyperName::from_str(&name_str) {
|
||||
Ok(n) => fallback
|
||||
.call(n)
|
||||
.await
|
||||
.map(|addrs| Box::new(addrs) as Addrs)
|
||||
.map_err(|err| Box::new(err) as Box<dyn std::error::Error + Send + Sync>),
|
||||
Err(e) => Err(Box::new(e) as Box<dyn std::error::Error + Send + Sync>),
|
||||
let start = Instant::now();
|
||||
|
||||
let result = match HyperName::from_str(&name_str) {
|
||||
Ok(n) => fallback.call(n).await,
|
||||
Err(e) => return Err(Box::new(e) as Box<dyn std::error::Error + Send + Sync>),
|
||||
};
|
||||
|
||||
let duration = start.elapsed().as_millis() as u64;
|
||||
|
||||
match result {
|
||||
Ok(addrs) => {
|
||||
// Collect addresses for event emission
|
||||
let addr_vec: Vec<SocketAddr> = addrs.collect();
|
||||
let addresses: Vec<String> =
|
||||
addr_vec.iter().map(|a| a.ip().to_string()).collect();
|
||||
|
||||
// Emit DNS event
|
||||
let guard = event_tx.read().await;
|
||||
if let Some(tx) = guard.as_ref() {
|
||||
let _ = tx
|
||||
.send(HttpResponseEvent::DnsResolved {
|
||||
hostname,
|
||||
addresses,
|
||||
duration,
|
||||
overridden: false,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(Box::new(addr_vec.into_iter()) as Addrs)
|
||||
}
|
||||
Err(err) => Err(Box::new(err) as Box<dyn std::error::Error + Send + Sync>),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::client::HttpConnectionOptions;
|
||||
use crate::dns::LocalhostResolver;
|
||||
use crate::error::Result;
|
||||
use log::info;
|
||||
use reqwest::Client;
|
||||
@@ -7,8 +8,15 @@ use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
/// A cached HTTP client along with its DNS resolver.
|
||||
/// The resolver is needed to set the event sender per-request.
|
||||
pub struct CachedClient {
|
||||
pub client: Client,
|
||||
pub resolver: Arc<LocalhostResolver>,
|
||||
}
|
||||
|
||||
pub struct HttpConnectionManager {
|
||||
connections: Arc<RwLock<BTreeMap<String, (Client, Instant)>>>,
|
||||
connections: Arc<RwLock<BTreeMap<String, (CachedClient, Instant)>>>,
|
||||
ttl: Duration,
|
||||
}
|
||||
|
||||
@@ -20,21 +28,26 @@ impl HttpConnectionManager {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_client(&self, opt: &HttpConnectionOptions) -> Result<Client> {
|
||||
pub async fn get_client(&self, opt: &HttpConnectionOptions) -> Result<CachedClient> {
|
||||
let mut connections = self.connections.write().await;
|
||||
let id = opt.id.clone();
|
||||
|
||||
// Clean old connections
|
||||
connections.retain(|_, (_, last_used)| last_used.elapsed() <= self.ttl);
|
||||
|
||||
if let Some((c, last_used)) = connections.get_mut(&id) {
|
||||
if let Some((cached, last_used)) = connections.get_mut(&id) {
|
||||
info!("Re-using HTTP client {id}");
|
||||
*last_used = Instant::now();
|
||||
return Ok(c.clone());
|
||||
return Ok(CachedClient {
|
||||
client: cached.client.clone(),
|
||||
resolver: cached.resolver.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
let c = opt.build_client()?;
|
||||
connections.insert(id.into(), (c.clone(), Instant::now()));
|
||||
Ok(c)
|
||||
let (client, resolver) = opt.build_client()?;
|
||||
let cached = CachedClient { client: client.clone(), resolver: resolver.clone() };
|
||||
connections.insert(id.into(), (cached, Instant::now()));
|
||||
|
||||
Ok(CachedClient { client, resolver })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ use crate::decompress::{ContentEncoding, streaming_decoder};
|
||||
use crate::error::{Error, Result};
|
||||
use crate::types::{SendableBody, SendableHttpRequest};
|
||||
use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
use futures_util::StreamExt;
|
||||
use http_body::{Body as HttpBody, Frame, SizeHint};
|
||||
use reqwest::{Client, Method, Version};
|
||||
use std::fmt::Display;
|
||||
use std::pin::Pin;
|
||||
@@ -31,7 +33,14 @@ pub enum HttpResponseEvent {
|
||||
},
|
||||
SendUrl {
|
||||
method: String,
|
||||
scheme: String,
|
||||
username: String,
|
||||
password: String,
|
||||
host: String,
|
||||
port: u16,
|
||||
path: String,
|
||||
query: String,
|
||||
fragment: String,
|
||||
},
|
||||
ReceiveUrl {
|
||||
version: Version,
|
||||
@@ -45,6 +54,12 @@ pub enum HttpResponseEvent {
|
||||
ChunkReceived {
|
||||
bytes: usize,
|
||||
},
|
||||
DnsResolved {
|
||||
hostname: String,
|
||||
addresses: Vec<String>,
|
||||
duration: u64,
|
||||
overridden: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl Display for HttpResponseEvent {
|
||||
@@ -59,7 +74,16 @@ impl Display for HttpResponseEvent {
|
||||
};
|
||||
write!(f, "* Redirect {} -> {} ({})", status, url, behavior_str)
|
||||
}
|
||||
HttpResponseEvent::SendUrl { method, path } => write!(f, "> {} {}", method, path),
|
||||
HttpResponseEvent::SendUrl { method, scheme, username, password, host, port, path, query, fragment } => {
|
||||
let auth_str = if username.is_empty() && password.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("{}:{}@", username, password)
|
||||
};
|
||||
let query_str = if query.is_empty() { String::new() } else { format!("?{}", query) };
|
||||
let fragment_str = if fragment.is_empty() { String::new() } else { format!("#{}", fragment) };
|
||||
write!(f, "> {} {}://{}{}:{}{}{}{}", method, scheme, auth_str, host, port, path, query_str, fragment_str)
|
||||
}
|
||||
HttpResponseEvent::ReceiveUrl { version, status } => {
|
||||
write!(f, "< {} {}", version_to_str(version), status)
|
||||
}
|
||||
@@ -67,6 +91,19 @@ impl Display for HttpResponseEvent {
|
||||
HttpResponseEvent::HeaderDown(name, value) => write!(f, "< {}: {}", name, value),
|
||||
HttpResponseEvent::ChunkSent { bytes } => write!(f, "> [{} bytes sent]", bytes),
|
||||
HttpResponseEvent::ChunkReceived { bytes } => write!(f, "< [{} bytes received]", bytes),
|
||||
HttpResponseEvent::DnsResolved { hostname, addresses, duration, overridden } => {
|
||||
if *overridden {
|
||||
write!(f, "* DNS override {} -> {}", hostname, addresses.join(", "))
|
||||
} else {
|
||||
write!(
|
||||
f,
|
||||
"* DNS resolved {} to {} ({}ms)",
|
||||
hostname,
|
||||
addresses.join(", "),
|
||||
duration
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,7 +122,9 @@ impl From<HttpResponseEvent> for yaak_models::models::HttpResponseEventData {
|
||||
RedirectBehavior::DropBody => "drop_body".to_string(),
|
||||
},
|
||||
},
|
||||
HttpResponseEvent::SendUrl { method, path } => D::SendUrl { method, path },
|
||||
HttpResponseEvent::SendUrl { method, scheme, username, password, host, port, path, query, fragment } => {
|
||||
D::SendUrl { method, scheme, username, password, host, port, path, query, fragment }
|
||||
}
|
||||
HttpResponseEvent::ReceiveUrl { version, status } => {
|
||||
D::ReceiveUrl { version: format!("{:?}", version), status }
|
||||
}
|
||||
@@ -93,6 +132,9 @@ impl From<HttpResponseEvent> for yaak_models::models::HttpResponseEventData {
|
||||
HttpResponseEvent::HeaderDown(name, value) => D::HeaderDown { name, value },
|
||||
HttpResponseEvent::ChunkSent { bytes } => D::ChunkSent { bytes },
|
||||
HttpResponseEvent::ChunkReceived { bytes } => D::ChunkReceived { bytes },
|
||||
HttpResponseEvent::DnsResolved { hostname, addresses, duration, overridden } => {
|
||||
D::DnsResolved { hostname, addresses, duration, overridden }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -354,6 +396,9 @@ impl HttpSender for ReqwestSender {
|
||||
|
||||
// Add headers
|
||||
for header in request.headers {
|
||||
if header.0.is_empty() {
|
||||
continue;
|
||||
}
|
||||
req_builder = req_builder.header(&header.0, &header.1);
|
||||
}
|
||||
|
||||
@@ -370,10 +415,16 @@ impl HttpSender for ReqwestSender {
|
||||
Some(SendableBody::Bytes(bytes)) => {
|
||||
req_builder = req_builder.body(bytes);
|
||||
}
|
||||
Some(SendableBody::Stream(stream)) => {
|
||||
// Convert AsyncRead stream to reqwest Body
|
||||
let stream = tokio_util::io::ReaderStream::new(stream);
|
||||
let body = reqwest::Body::wrap_stream(stream);
|
||||
Some(SendableBody::Stream { data, content_length }) => {
|
||||
// Convert AsyncRead stream to reqwest Body. If content length is
|
||||
// known, wrap with a SizedBody so hyper can set Content-Length
|
||||
// automatically (for both HTTP/1.1 and HTTP/2).
|
||||
let stream = tokio_util::io::ReaderStream::new(data);
|
||||
let body = if let Some(len) = content_length {
|
||||
reqwest::Body::wrap(SizedBody::new(stream, len))
|
||||
} else {
|
||||
reqwest::Body::wrap_stream(stream)
|
||||
};
|
||||
req_builder = req_builder.body(body);
|
||||
}
|
||||
}
|
||||
@@ -390,8 +441,15 @@ impl HttpSender for ReqwestSender {
|
||||
));
|
||||
|
||||
send_event(HttpResponseEvent::SendUrl {
|
||||
path: sendable_req.url().path().to_string(),
|
||||
method: sendable_req.method().to_string(),
|
||||
scheme: sendable_req.url().scheme().to_string(),
|
||||
username: sendable_req.url().username().to_string(),
|
||||
password: sendable_req.url().password().unwrap_or_default().to_string(),
|
||||
host: sendable_req.url().host_str().unwrap_or_default().to_string(),
|
||||
port: sendable_req.url().port_or_known_default().unwrap_or(0),
|
||||
path: sendable_req.url().path().to_string(),
|
||||
query: sendable_req.url().query().unwrap_or_default().to_string(),
|
||||
fragment: sendable_req.url().fragment().unwrap_or_default().to_string(),
|
||||
});
|
||||
|
||||
let mut request_headers = Vec::new();
|
||||
@@ -470,6 +528,51 @@ impl HttpSender for ReqwestSender {
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper around a byte stream that reports a known content length via
|
||||
/// `size_hint()`. This lets hyper set the `Content-Length` header
|
||||
/// automatically based on the body size, without us having to add it as an
|
||||
/// explicit header — which can cause duplicate `Content-Length` headers and
|
||||
/// break HTTP/2.
|
||||
struct SizedBody<S> {
|
||||
stream: std::sync::Mutex<S>,
|
||||
remaining: u64,
|
||||
}
|
||||
|
||||
impl<S> SizedBody<S> {
|
||||
fn new(stream: S, content_length: u64) -> Self {
|
||||
Self { stream: std::sync::Mutex::new(stream), remaining: content_length }
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> HttpBody for SizedBody<S>
|
||||
where
|
||||
S: futures_util::Stream<Item = std::result::Result<Bytes, std::io::Error>> + Send + Unpin + 'static,
|
||||
{
|
||||
type Data = Bytes;
|
||||
type Error = std::io::Error;
|
||||
|
||||
fn poll_frame(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<std::result::Result<Frame<Self::Data>, Self::Error>>> {
|
||||
let this = self.get_mut();
|
||||
let mut stream = this.stream.lock().unwrap();
|
||||
match stream.poll_next_unpin(cx) {
|
||||
Poll::Ready(Some(Ok(chunk))) => {
|
||||
this.remaining = this.remaining.saturating_sub(chunk.len() as u64);
|
||||
Poll::Ready(Some(Ok(Frame::data(chunk))))
|
||||
}
|
||||
Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(e))),
|
||||
Poll::Ready(None) => Poll::Ready(None),
|
||||
Poll::Pending => Poll::Pending,
|
||||
}
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> SizeHint {
|
||||
SizeHint::with_exact(self.remaining)
|
||||
}
|
||||
}
|
||||
|
||||
fn version_to_str(version: &Version) -> String {
|
||||
match *version {
|
||||
Version::HTTP_09 => "HTTP/0.9".to_string(),
|
||||
|
||||
@@ -168,6 +168,7 @@ impl<S: HttpSender> HttpTransaction<S> {
|
||||
response.drain().await?;
|
||||
|
||||
// Update the request URL
|
||||
let previous_url = current_url.clone();
|
||||
current_url = if location.starts_with("http://") || location.starts_with("https://") {
|
||||
// Absolute URL
|
||||
location
|
||||
@@ -181,6 +182,8 @@ impl<S: HttpSender> HttpTransaction<S> {
|
||||
format!("{}/{}", base_path, location)
|
||||
};
|
||||
|
||||
Self::remove_sensitive_headers(&mut current_headers, &previous_url, ¤t_url);
|
||||
|
||||
// Determine redirect behavior based on status code and method
|
||||
let behavior = if status == 303 {
|
||||
// 303 See Other always changes to GET
|
||||
@@ -220,6 +223,33 @@ impl<S: HttpSender> HttpTransaction<S> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove sensitive headers when redirecting to a different host.
|
||||
/// This matches reqwest's `remove_sensitive_headers()` behavior and prevents
|
||||
/// credentials from being forwarded to third-party servers (e.g., an
|
||||
/// Authorization header sent from an API redirect to an S3 bucket).
|
||||
fn remove_sensitive_headers(
|
||||
headers: &mut Vec<(String, String)>,
|
||||
previous_url: &str,
|
||||
next_url: &str,
|
||||
) {
|
||||
let previous_host = Url::parse(previous_url).ok().and_then(|u| {
|
||||
u.host_str().map(|h| format!("{}:{}", h, u.port_or_known_default().unwrap_or(0)))
|
||||
});
|
||||
let next_host = Url::parse(next_url).ok().and_then(|u| {
|
||||
u.host_str().map(|h| format!("{}:{}", h, u.port_or_known_default().unwrap_or(0)))
|
||||
});
|
||||
if previous_host != next_host {
|
||||
headers.retain(|h| {
|
||||
let name_lower = h.0.to_lowercase();
|
||||
name_lower != "authorization"
|
||||
&& name_lower != "cookie"
|
||||
&& name_lower != "cookie2"
|
||||
&& name_lower != "proxy-authorization"
|
||||
&& name_lower != "www-authenticate"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a status code indicates a redirect
|
||||
fn is_redirect(status: u16) -> bool {
|
||||
matches!(status, 301 | 302 | 303 | 307 | 308)
|
||||
@@ -269,9 +299,20 @@ mod tests {
|
||||
use tokio::io::AsyncRead;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
/// Captured request metadata for test assertions
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(dead_code)]
|
||||
struct CapturedRequest {
|
||||
url: String,
|
||||
method: String,
|
||||
headers: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
/// Mock sender for testing
|
||||
struct MockSender {
|
||||
responses: Arc<Mutex<Vec<MockResponse>>>,
|
||||
/// Captured requests for assertions
|
||||
captured_requests: Arc<Mutex<Vec<CapturedRequest>>>,
|
||||
}
|
||||
|
||||
struct MockResponse {
|
||||
@@ -282,7 +323,10 @@ mod tests {
|
||||
|
||||
impl MockSender {
|
||||
fn new(responses: Vec<MockResponse>) -> Self {
|
||||
Self { responses: Arc::new(Mutex::new(responses)) }
|
||||
Self {
|
||||
responses: Arc::new(Mutex::new(responses)),
|
||||
captured_requests: Arc::new(Mutex::new(Vec::new())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,9 +334,16 @@ mod tests {
|
||||
impl HttpSender for MockSender {
|
||||
async fn send(
|
||||
&self,
|
||||
_request: SendableHttpRequest,
|
||||
request: SendableHttpRequest,
|
||||
_event_tx: mpsc::Sender<HttpResponseEvent>,
|
||||
) -> Result<HttpResponse> {
|
||||
// Capture the request metadata for later assertions
|
||||
self.captured_requests.lock().await.push(CapturedRequest {
|
||||
url: request.url.clone(),
|
||||
method: request.method.clone(),
|
||||
headers: request.headers.clone(),
|
||||
});
|
||||
|
||||
let mut responses = self.responses.lock().await;
|
||||
if responses.is_empty() {
|
||||
Err(crate::error::Error::RequestError("No more mock responses".to_string()))
|
||||
@@ -342,7 +393,8 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_transaction_single_redirect() {
|
||||
let redirect_headers = vec![("Location".to_string(), "https://example.com/new".to_string())];
|
||||
let redirect_headers =
|
||||
vec![("Location".to_string(), "https://example.com/new".to_string())];
|
||||
|
||||
let responses = vec![
|
||||
MockResponse { status: 302, headers: redirect_headers, body: vec![] },
|
||||
@@ -373,7 +425,8 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_transaction_max_redirects_exceeded() {
|
||||
let redirect_headers = vec![("Location".to_string(), "https://example.com/loop".to_string())];
|
||||
let redirect_headers =
|
||||
vec![("Location".to_string(), "https://example.com/loop".to_string())];
|
||||
|
||||
// Create more redirects than allowed
|
||||
let responses: Vec<MockResponse> = (0..12)
|
||||
@@ -525,7 +578,8 @@ mod tests {
|
||||
_request: SendableHttpRequest,
|
||||
_event_tx: mpsc::Sender<HttpResponseEvent>,
|
||||
) -> Result<HttpResponse> {
|
||||
let headers = vec![("set-cookie".to_string(), "session=xyz789; Path=/".to_string())];
|
||||
let headers =
|
||||
vec![("set-cookie".to_string(), "session=xyz789; Path=/".to_string())];
|
||||
|
||||
let body_stream: Pin<Box<dyn AsyncRead + Send>> =
|
||||
Box::pin(std::io::Cursor::new(vec![]));
|
||||
@@ -584,7 +638,10 @@ mod tests {
|
||||
let headers = vec![
|
||||
("set-cookie".to_string(), "session=abc123; Path=/".to_string()),
|
||||
("set-cookie".to_string(), "user_id=42; Path=/".to_string()),
|
||||
("set-cookie".to_string(), "preferences=dark; Path=/; Max-Age=86400".to_string()),
|
||||
(
|
||||
"set-cookie".to_string(),
|
||||
"preferences=dark; Path=/; Max-Age=86400".to_string(),
|
||||
),
|
||||
];
|
||||
|
||||
let body_stream: Pin<Box<dyn AsyncRead + Send>> =
|
||||
@@ -720,4 +777,116 @@ mod tests {
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(request_count.load(Ordering::SeqCst), 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_cross_origin_redirect_strips_auth_headers() {
|
||||
// Redirect from api.example.com -> s3.amazonaws.com should strip Authorization
|
||||
let responses = vec![
|
||||
MockResponse {
|
||||
status: 302,
|
||||
headers: vec![(
|
||||
"Location".to_string(),
|
||||
"https://s3.amazonaws.com/bucket/file.pdf".to_string(),
|
||||
)],
|
||||
body: vec![],
|
||||
},
|
||||
MockResponse { status: 200, headers: Vec::new(), body: b"PDF content".to_vec() },
|
||||
];
|
||||
|
||||
let sender = MockSender::new(responses);
|
||||
let captured = sender.captured_requests.clone();
|
||||
let transaction = HttpTransaction::new(sender);
|
||||
|
||||
let request = SendableHttpRequest {
|
||||
url: "https://api.example.com/download".to_string(),
|
||||
method: "GET".to_string(),
|
||||
headers: vec![
|
||||
("Authorization".to_string(), "Basic dXNlcjpwYXNz".to_string()),
|
||||
("Accept".to_string(), "application/pdf".to_string()),
|
||||
],
|
||||
options: crate::types::SendableHttpRequestOptions {
|
||||
follow_redirects: true,
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let (_tx, rx) = tokio::sync::watch::channel(false);
|
||||
let (event_tx, _event_rx) = mpsc::channel(100);
|
||||
let result = transaction.execute_with_cancellation(request, rx, event_tx).await.unwrap();
|
||||
assert_eq!(result.status, 200);
|
||||
|
||||
let requests = captured.lock().await;
|
||||
assert_eq!(requests.len(), 2);
|
||||
|
||||
// First request should have the Authorization header
|
||||
assert!(
|
||||
requests[0].headers.iter().any(|(k, _)| k.eq_ignore_ascii_case("authorization")),
|
||||
"First request should have Authorization header"
|
||||
);
|
||||
|
||||
// Second request (to different host) should NOT have the Authorization header
|
||||
assert!(
|
||||
!requests[1].headers.iter().any(|(k, _)| k.eq_ignore_ascii_case("authorization")),
|
||||
"Redirected request to different host should NOT have Authorization header"
|
||||
);
|
||||
|
||||
// Non-sensitive headers should still be present
|
||||
assert!(
|
||||
requests[1].headers.iter().any(|(k, _)| k.eq_ignore_ascii_case("accept")),
|
||||
"Non-sensitive headers should be preserved across cross-origin redirects"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_same_origin_redirect_preserves_auth_headers() {
|
||||
// Redirect within the same host should keep Authorization
|
||||
let responses = vec![
|
||||
MockResponse {
|
||||
status: 302,
|
||||
headers: vec![(
|
||||
"Location".to_string(),
|
||||
"https://api.example.com/v2/download".to_string(),
|
||||
)],
|
||||
body: vec![],
|
||||
},
|
||||
MockResponse { status: 200, headers: Vec::new(), body: b"OK".to_vec() },
|
||||
];
|
||||
|
||||
let sender = MockSender::new(responses);
|
||||
let captured = sender.captured_requests.clone();
|
||||
let transaction = HttpTransaction::new(sender);
|
||||
|
||||
let request = SendableHttpRequest {
|
||||
url: "https://api.example.com/v1/download".to_string(),
|
||||
method: "GET".to_string(),
|
||||
headers: vec![
|
||||
("Authorization".to_string(), "Bearer token123".to_string()),
|
||||
("Accept".to_string(), "application/json".to_string()),
|
||||
],
|
||||
options: crate::types::SendableHttpRequestOptions {
|
||||
follow_redirects: true,
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let (_tx, rx) = tokio::sync::watch::channel(false);
|
||||
let (event_tx, _event_rx) = mpsc::channel(100);
|
||||
let result = transaction.execute_with_cancellation(request, rx, event_tx).await.unwrap();
|
||||
assert_eq!(result.status, 200);
|
||||
|
||||
let requests = captured.lock().await;
|
||||
assert_eq!(requests.len(), 2);
|
||||
|
||||
// Both requests should have the Authorization header (same host)
|
||||
assert!(
|
||||
requests[0].headers.iter().any(|(k, _)| k.eq_ignore_ascii_case("authorization")),
|
||||
"First request should have Authorization header"
|
||||
);
|
||||
assert!(
|
||||
requests[1].headers.iter().any(|(k, _)| k.eq_ignore_ascii_case("authorization")),
|
||||
"Redirected request to same host should preserve Authorization header"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,13 @@ pub(crate) const MULTIPART_BOUNDARY: &str = "------YaakFormBoundary";
|
||||
|
||||
pub enum SendableBody {
|
||||
Bytes(Bytes),
|
||||
Stream(Pin<Box<dyn AsyncRead + Send + 'static>>),
|
||||
Stream {
|
||||
data: Pin<Box<dyn AsyncRead + Send + 'static>>,
|
||||
/// Known content length for the stream, if available. This is used by
|
||||
/// the sender to set the body size hint so that hyper can set
|
||||
/// Content-Length automatically for both HTTP/1.1 and HTTP/2.
|
||||
content_length: Option<u64>,
|
||||
},
|
||||
}
|
||||
|
||||
enum SendableBodyWithMeta {
|
||||
@@ -31,7 +37,10 @@ impl From<SendableBodyWithMeta> for SendableBody {
|
||||
fn from(value: SendableBodyWithMeta) -> Self {
|
||||
match value {
|
||||
SendableBodyWithMeta::Bytes(b) => SendableBody::Bytes(b),
|
||||
SendableBodyWithMeta::Stream { data, .. } => SendableBody::Stream(data),
|
||||
SendableBodyWithMeta::Stream { data, content_length } => SendableBody::Stream {
|
||||
data,
|
||||
content_length: content_length.map(|l| l as u64),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -186,23 +195,11 @@ async fn build_body(
|
||||
}
|
||||
}
|
||||
|
||||
// Check if Transfer-Encoding: chunked is already set
|
||||
let has_chunked_encoding = headers.iter().any(|h| {
|
||||
h.0.to_lowercase() == "transfer-encoding" && h.1.to_lowercase().contains("chunked")
|
||||
});
|
||||
|
||||
// Add a Content-Length header only if chunked encoding is not being used
|
||||
if !has_chunked_encoding {
|
||||
let content_length = match body {
|
||||
Some(SendableBodyWithMeta::Bytes(ref bytes)) => Some(bytes.len()),
|
||||
Some(SendableBodyWithMeta::Stream { content_length, .. }) => content_length,
|
||||
None => None,
|
||||
};
|
||||
|
||||
if let Some(cl) = content_length {
|
||||
headers.push(("Content-Length".to_string(), cl.to_string()));
|
||||
}
|
||||
}
|
||||
// NOTE: Content-Length is NOT set as an explicit header here. Instead, the
|
||||
// body's content length is carried via SendableBody::Stream { content_length }
|
||||
// and used by the sender to set the body size hint. This lets hyper handle
|
||||
// Content-Length automatically for both HTTP/1.1 and HTTP/2, avoiding the
|
||||
// duplicate Content-Length that breaks HTTP/2 servers.
|
||||
|
||||
Ok((body.map(|b| b.into()), headers))
|
||||
}
|
||||
@@ -928,7 +925,27 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_no_content_length_with_chunked_encoding() -> Result<()> {
|
||||
async fn test_no_content_length_header_added_by_build_body() -> Result<()> {
|
||||
let mut body = BTreeMap::new();
|
||||
body.insert("text".to_string(), json!("Hello, World!"));
|
||||
|
||||
let headers = vec![];
|
||||
|
||||
let (_, result_headers) =
|
||||
build_body("POST", &Some("text/plain".to_string()), &body, headers).await?;
|
||||
|
||||
// Content-Length should NOT be set as an explicit header. Instead, the
|
||||
// sender uses the body's size_hint to let hyper set it automatically,
|
||||
// which works correctly for both HTTP/1.1 and HTTP/2.
|
||||
let has_content_length =
|
||||
result_headers.iter().any(|h| h.0.to_lowercase() == "content-length");
|
||||
assert!(!has_content_length, "Content-Length should not be set as an explicit header");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_chunked_encoding_header_preserved() -> Result<()> {
|
||||
let mut body = BTreeMap::new();
|
||||
body.insert("text".to_string(), json!("Hello, World!"));
|
||||
|
||||
@@ -938,11 +955,6 @@ mod tests {
|
||||
let (_, result_headers) =
|
||||
build_body("POST", &Some("text/plain".to_string()), &body, headers).await?;
|
||||
|
||||
// Verify that Content-Length is NOT present when Transfer-Encoding: chunked is set
|
||||
let has_content_length =
|
||||
result_headers.iter().any(|h| h.0.to_lowercase() == "content-length");
|
||||
assert!(!has_content_length, "Content-Length should not be present with chunked encoding");
|
||||
|
||||
// Verify that the Transfer-Encoding header is still present
|
||||
let has_chunked = result_headers.iter().any(|h| {
|
||||
h.0.to_lowercase() == "transfer-encoding" && h.1.to_lowercase().contains("chunked")
|
||||
@@ -951,31 +963,4 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_content_length_without_chunked_encoding() -> Result<()> {
|
||||
let mut body = BTreeMap::new();
|
||||
body.insert("text".to_string(), json!("Hello, World!"));
|
||||
|
||||
// Headers without Transfer-Encoding: chunked
|
||||
let headers = vec![];
|
||||
|
||||
let (_, result_headers) =
|
||||
build_body("POST", &Some("text/plain".to_string()), &body, headers).await?;
|
||||
|
||||
// Verify that Content-Length IS present when Transfer-Encoding: chunked is NOT set
|
||||
let content_length_header =
|
||||
result_headers.iter().find(|h| h.0.to_lowercase() == "content-length");
|
||||
assert!(
|
||||
content_length_header.is_some(),
|
||||
"Content-Length should be present without chunked encoding"
|
||||
);
|
||||
assert_eq!(
|
||||
content_length_header.unwrap().1,
|
||||
"13",
|
||||
"Content-Length should match the body size"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
8
crates/yaak-models/bindings/gen_models.ts
generated
8
crates/yaak-models/bindings/gen_models.ts
generated
@@ -12,6 +12,8 @@ export type CookieExpires = { "AtUtc": string } | "SessionEnd";
|
||||
|
||||
export type CookieJar = { model: "cookie_jar", id: string, createdAt: string, updatedAt: string, workspaceId: string, cookies: Array<Cookie>, name: string, };
|
||||
|
||||
export type DnsOverride = { hostname: string, ipv4: Array<string>, ipv6: Array<string>, enabled?: boolean, };
|
||||
|
||||
export type EditorKeymap = "default" | "vim" | "vscode" | "emacs";
|
||||
|
||||
export type EncryptedKey = { encryptedKey: string, };
|
||||
@@ -38,7 +40,7 @@ export type HttpRequest = { model: "http_request", id: string, createdAt: string
|
||||
|
||||
export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||
|
||||
export type HttpResponse = { model: "http_response", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, bodyPath: string | null, contentLength: number | null, contentLengthCompressed: number | null, elapsed: number, elapsedHeaders: number, error: string | null, headers: Array<HttpResponseHeader>, remoteAddr: string | null, requestContentLength: number | null, requestHeaders: Array<HttpResponseHeader>, status: number, statusReason: string | null, state: HttpResponseState, url: string, version: string | null, };
|
||||
export type HttpResponse = { model: "http_response", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, bodyPath: string | null, contentLength: number | null, contentLengthCompressed: number | null, elapsed: number, elapsedHeaders: number, elapsedDns: number, error: string | null, headers: Array<HttpResponseHeader>, remoteAddr: string | null, requestContentLength: number | null, requestHeaders: Array<HttpResponseHeader>, status: number, statusReason: string | null, state: HttpResponseState, url: string, version: string | null, };
|
||||
|
||||
export type HttpResponseEvent = { model: "http_response_event", id: string, createdAt: string, updatedAt: string, workspaceId: string, responseId: string, event: HttpResponseEventData, };
|
||||
|
||||
@@ -47,7 +49,7 @@ export type HttpResponseEvent = { model: "http_response_event", id: string, crea
|
||||
* This mirrors `yaak_http::sender::HttpResponseEvent` but with serde support.
|
||||
* The `From` impl is in yaak-http to avoid circular dependencies.
|
||||
*/
|
||||
export type HttpResponseEventData = { "type": "setting", name: string, value: string, } | { "type": "info", message: string, } | { "type": "redirect", url: string, status: number, behavior: string, } | { "type": "send_url", method: string, path: string, } | { "type": "receive_url", version: string, status: string, } | { "type": "header_up", name: string, value: string, } | { "type": "header_down", name: string, value: string, } | { "type": "chunk_sent", bytes: number, } | { "type": "chunk_received", bytes: number, };
|
||||
export type HttpResponseEventData = { "type": "setting", name: string, value: string, } | { "type": "info", message: string, } | { "type": "redirect", url: string, status: number, behavior: string, } | { "type": "send_url", method: string, scheme: string, username: string, password: string, host: string, port: number, path: string, query: string, fragment: string, } | { "type": "receive_url", version: string, status: string, } | { "type": "header_up", name: string, value: string, } | { "type": "header_down", name: string, value: string, } | { "type": "chunk_sent", bytes: number, } | { "type": "chunk_received", bytes: number, } | { "type": "dns_resolved", hostname: string, addresses: Array<string>, duration: bigint, overridden: boolean, };
|
||||
|
||||
export type HttpResponseHeader = { name: string, value: string, };
|
||||
|
||||
@@ -91,6 +93,6 @@ export type WebsocketMessageType = "text" | "binary";
|
||||
|
||||
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
|
||||
|
||||
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };
|
||||
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, settingDnsOverrides: Array<DnsOverride>, };
|
||||
|
||||
export type WorkspaceMeta = { model: "workspace_meta", id: string, workspaceId: string, createdAt: string, updatedAt: string, encryptionKey: EncryptedKey | null, settingSyncDir: string | null, };
|
||||
|
||||
@@ -206,6 +206,34 @@ export function replaceModelsInStore<
|
||||
});
|
||||
}
|
||||
|
||||
export function mergeModelsInStore<
|
||||
M extends AnyModel['model'],
|
||||
T extends Extract<AnyModel, { model: M }>,
|
||||
>(model: M, models: T[], filter?: (model: T) => boolean) {
|
||||
mustStore().set(modelStoreDataAtom, (prev: ModelStoreData) => {
|
||||
const existingModels = { ...prev[model] } as Record<string, T>;
|
||||
|
||||
// Merge in new models first
|
||||
for (const m of models) {
|
||||
existingModels[m.id] = m;
|
||||
}
|
||||
|
||||
// Then filter out unwanted models
|
||||
if (filter) {
|
||||
for (const [id, m] of Object.entries(existingModels)) {
|
||||
if (!filter(m)) {
|
||||
delete existingModels[id];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...prev,
|
||||
[model]: existingModels,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function shouldIgnoreModel({ model, updateSource }: ModelPayload) {
|
||||
// Never ignore updates from non-user sources
|
||||
if (updateSource.type !== 'window') {
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- Add DNS resolution timing to http_responses
|
||||
ALTER TABLE http_responses ADD COLUMN elapsed_dns INTEGER DEFAULT 0 NOT NULL;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- Add DNS overrides setting to workspaces
|
||||
ALTER TABLE workspaces ADD COLUMN setting_dns_overrides TEXT DEFAULT '[]' NOT NULL;
|
||||
@@ -0,0 +1,12 @@
|
||||
-- Filter out headers that match the hardcoded defaults (User-Agent: yaak, Accept: */*),
|
||||
-- keeping any other custom headers the user may have added.
|
||||
UPDATE workspaces
|
||||
SET headers = (
|
||||
SELECT json_group_array(json(value))
|
||||
FROM json_each(headers)
|
||||
WHERE NOT (
|
||||
(LOWER(json_extract(value, '$.name')) = 'user-agent' AND json_extract(value, '$.value') = 'yaak')
|
||||
OR (LOWER(json_extract(value, '$.name')) = 'accept' AND json_extract(value, '$.value') = '*/*')
|
||||
)
|
||||
)
|
||||
WHERE json_array_length(headers) > 0;
|
||||
@@ -73,6 +73,20 @@ pub struct ClientCertificate {
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "gen_models.ts")]
|
||||
pub struct DnsOverride {
|
||||
pub hostname: String,
|
||||
#[serde(default)]
|
||||
pub ipv4: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub ipv6: Vec<String>,
|
||||
#[serde(default = "default_true")]
|
||||
#[ts(optional, as = "Option<bool>")]
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(export, export_to = "gen_models.ts")]
|
||||
@@ -303,6 +317,8 @@ pub struct Workspace {
|
||||
#[serde(default = "default_true")]
|
||||
pub setting_follow_redirects: bool,
|
||||
pub setting_request_timeout: i32,
|
||||
#[serde(default)]
|
||||
pub setting_dns_overrides: Vec<DnsOverride>,
|
||||
}
|
||||
|
||||
impl UpsertModelInfo for Workspace {
|
||||
@@ -343,6 +359,7 @@ impl UpsertModelInfo for Workspace {
|
||||
(SettingFollowRedirects, self.setting_follow_redirects.into()),
|
||||
(SettingRequestTimeout, self.setting_request_timeout.into()),
|
||||
(SettingValidateCertificates, self.setting_validate_certificates.into()),
|
||||
(SettingDnsOverrides, serde_json::to_string(&self.setting_dns_overrides)?.into()),
|
||||
])
|
||||
}
|
||||
|
||||
@@ -359,6 +376,7 @@ impl UpsertModelInfo for Workspace {
|
||||
WorkspaceIden::SettingFollowRedirects,
|
||||
WorkspaceIden::SettingRequestTimeout,
|
||||
WorkspaceIden::SettingValidateCertificates,
|
||||
WorkspaceIden::SettingDnsOverrides,
|
||||
]
|
||||
}
|
||||
|
||||
@@ -368,6 +386,7 @@ impl UpsertModelInfo for Workspace {
|
||||
{
|
||||
let headers: String = row.get("headers")?;
|
||||
let authentication: String = row.get("authentication")?;
|
||||
let setting_dns_overrides: String = row.get("setting_dns_overrides")?;
|
||||
Ok(Self {
|
||||
id: row.get("id")?,
|
||||
model: row.get("model")?,
|
||||
@@ -382,6 +401,7 @@ impl UpsertModelInfo for Workspace {
|
||||
setting_follow_redirects: row.get("setting_follow_redirects")?,
|
||||
setting_request_timeout: row.get("setting_request_timeout")?,
|
||||
setting_validate_certificates: row.get("setting_validate_certificates")?,
|
||||
setting_dns_overrides: serde_json::from_str(&setting_dns_overrides).unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1333,6 +1353,7 @@ pub struct HttpResponse {
|
||||
pub content_length_compressed: Option<i32>,
|
||||
pub elapsed: i32,
|
||||
pub elapsed_headers: i32,
|
||||
pub elapsed_dns: i32,
|
||||
pub error: Option<String>,
|
||||
pub headers: Vec<HttpResponseHeader>,
|
||||
pub remote_addr: Option<String>,
|
||||
@@ -1381,6 +1402,7 @@ impl UpsertModelInfo for HttpResponse {
|
||||
(ContentLengthCompressed, self.content_length_compressed.into()),
|
||||
(Elapsed, self.elapsed.into()),
|
||||
(ElapsedHeaders, self.elapsed_headers.into()),
|
||||
(ElapsedDns, self.elapsed_dns.into()),
|
||||
(Error, self.error.into()),
|
||||
(Headers, serde_json::to_string(&self.headers)?.into()),
|
||||
(RemoteAddr, self.remote_addr.into()),
|
||||
@@ -1402,6 +1424,7 @@ impl UpsertModelInfo for HttpResponse {
|
||||
HttpResponseIden::ContentLengthCompressed,
|
||||
HttpResponseIden::Elapsed,
|
||||
HttpResponseIden::ElapsedHeaders,
|
||||
HttpResponseIden::ElapsedDns,
|
||||
HttpResponseIden::Error,
|
||||
HttpResponseIden::Headers,
|
||||
HttpResponseIden::RemoteAddr,
|
||||
@@ -1435,6 +1458,7 @@ impl UpsertModelInfo for HttpResponse {
|
||||
version: r.get("version")?,
|
||||
elapsed: r.get("elapsed")?,
|
||||
elapsed_headers: r.get("elapsed_headers")?,
|
||||
elapsed_dns: r.get("elapsed_dns").unwrap_or_default(),
|
||||
remote_addr: r.get("remote_addr")?,
|
||||
status: r.get("status")?,
|
||||
status_reason: r.get("status_reason")?,
|
||||
@@ -1471,7 +1495,21 @@ pub enum HttpResponseEventData {
|
||||
},
|
||||
SendUrl {
|
||||
method: String,
|
||||
#[serde(default)]
|
||||
scheme: String,
|
||||
#[serde(default)]
|
||||
username: String,
|
||||
#[serde(default)]
|
||||
password: String,
|
||||
#[serde(default)]
|
||||
host: String,
|
||||
#[serde(default)]
|
||||
port: u16,
|
||||
path: String,
|
||||
#[serde(default)]
|
||||
query: String,
|
||||
#[serde(default)]
|
||||
fragment: String,
|
||||
},
|
||||
ReceiveUrl {
|
||||
version: String,
|
||||
@@ -1491,6 +1529,12 @@ pub enum HttpResponseEventData {
|
||||
ChunkReceived {
|
||||
bytes: usize,
|
||||
},
|
||||
DnsResolved {
|
||||
hostname: String,
|
||||
addresses: Vec<String>,
|
||||
duration: u64,
|
||||
overridden: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl Default for HttpResponseEventData {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use super::dedupe_headers;
|
||||
use crate::db_context::DbContext;
|
||||
use crate::error::Result;
|
||||
use crate::models::{GrpcRequest, GrpcRequestIden, HttpRequestHeader};
|
||||
@@ -87,6 +88,6 @@ impl<'a> DbContext<'a> {
|
||||
|
||||
metadata.append(&mut grpc_request.metadata.clone());
|
||||
|
||||
Ok(metadata)
|
||||
Ok(dedupe_headers(metadata))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use super::dedupe_headers;
|
||||
use crate::db_context::DbContext;
|
||||
use crate::error::Result;
|
||||
use crate::models::{Folder, FolderIden, HttpRequest, HttpRequestHeader, HttpRequestIden};
|
||||
@@ -87,7 +88,7 @@ impl<'a> DbContext<'a> {
|
||||
|
||||
headers.append(&mut http_request.headers.clone());
|
||||
|
||||
Ok(headers)
|
||||
Ok(dedupe_headers(headers))
|
||||
}
|
||||
|
||||
pub fn list_http_requests_for_folder_recursive(
|
||||
|
||||
@@ -19,6 +19,26 @@ mod websocket_connections;
|
||||
mod websocket_events;
|
||||
mod websocket_requests;
|
||||
mod workspace_metas;
|
||||
mod workspaces;
|
||||
pub mod workspaces;
|
||||
|
||||
const MAX_HISTORY_ITEMS: usize = 20;
|
||||
|
||||
use crate::models::HttpRequestHeader;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Deduplicate headers by name (case-insensitive), keeping the latest (most specific) value.
|
||||
/// Preserves the order of first occurrence for each header name.
|
||||
pub(crate) fn dedupe_headers(headers: Vec<HttpRequestHeader>) -> Vec<HttpRequestHeader> {
|
||||
let mut index_by_name: HashMap<String, usize> = HashMap::new();
|
||||
let mut deduped: Vec<HttpRequestHeader> = Vec::new();
|
||||
for header in headers {
|
||||
let key = header.name.to_lowercase();
|
||||
if let Some(&idx) = index_by_name.get(&key) {
|
||||
deduped[idx] = header;
|
||||
} else {
|
||||
index_by_name.insert(key, deduped.len());
|
||||
deduped.push(header);
|
||||
}
|
||||
}
|
||||
deduped
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use super::dedupe_headers;
|
||||
use crate::db_context::DbContext;
|
||||
use crate::error::Result;
|
||||
use crate::models::{HttpRequestHeader, WebsocketRequest, WebsocketRequestIden};
|
||||
@@ -95,6 +96,6 @@ impl<'a> DbContext<'a> {
|
||||
|
||||
headers.append(&mut websocket_request.headers.clone());
|
||||
|
||||
Ok(headers)
|
||||
Ok(dedupe_headers(headers))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +80,28 @@ impl<'a> DbContext<'a> {
|
||||
}
|
||||
|
||||
pub fn resolve_headers_for_workspace(&self, workspace: &Workspace) -> Vec<HttpRequestHeader> {
|
||||
workspace.headers.clone()
|
||||
let mut headers = default_headers();
|
||||
headers.extend(workspace.headers.clone());
|
||||
headers
|
||||
}
|
||||
}
|
||||
|
||||
/// Global default headers that are always sent with requests unless overridden.
|
||||
/// These are prepended to the inheritance chain so workspace/folder/request headers
|
||||
/// can override or disable them.
|
||||
pub fn default_headers() -> Vec<HttpRequestHeader> {
|
||||
vec![
|
||||
HttpRequestHeader {
|
||||
enabled: true,
|
||||
name: "User-Agent".to_string(),
|
||||
value: "yaak".to_string(),
|
||||
id: None,
|
||||
},
|
||||
HttpRequestHeader {
|
||||
enabled: true,
|
||||
name: "Accept".to_string(),
|
||||
value: "*/*".to_string(),
|
||||
id: None,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
14
crates/yaak-plugins/bindings/gen_events.ts
generated
14
crates/yaak-plugins/bindings/gen_events.ts
generated
@@ -66,7 +66,9 @@ export type DeleteModelRequest = { model: string, id: string, };
|
||||
|
||||
export type DeleteModelResponse = { model: AnyModel, };
|
||||
|
||||
export type EditorLanguage = "text" | "javascript" | "json" | "html" | "xml" | "graphql" | "markdown";
|
||||
export type DialogSize = "sm" | "md" | "lg" | "full" | "dynamic";
|
||||
|
||||
export type EditorLanguage = "text" | "javascript" | "json" | "html" | "xml" | "graphql" | "markdown" | "c" | "clojure" | "csharp" | "go" | "http" | "java" | "kotlin" | "objective_c" | "ocaml" | "php" | "powershell" | "python" | "r" | "ruby" | "shell" | "swift";
|
||||
|
||||
export type EmptyPayload = {};
|
||||
|
||||
@@ -172,7 +174,11 @@ hideGutter?: boolean,
|
||||
/**
|
||||
* Language for syntax highlighting
|
||||
*/
|
||||
language?: EditorLanguage, readOnly?: boolean, completionOptions?: Array<GenericCompletionOption>,
|
||||
language?: EditorLanguage, readOnly?: boolean,
|
||||
/**
|
||||
* Fixed number of visible rows
|
||||
*/
|
||||
rows?: number, completionOptions?: Array<GenericCompletionOption>,
|
||||
/**
|
||||
* The name of the input. The value will be stored at this object attribute in the resulting data
|
||||
*/
|
||||
@@ -476,9 +482,9 @@ label: string, title?: string, size?: WindowSize, dataDirKey?: string, };
|
||||
|
||||
export type PluginContext = { id: string, label: string | null, workspaceId: string | null, };
|
||||
|
||||
export type PromptFormRequest = { id: string, title: string, description?: string, inputs: Array<FormInput>, confirmText?: string, cancelText?: string, };
|
||||
export type PromptFormRequest = { id: string, title: string, description?: string, inputs: Array<FormInput>, confirmText?: string, cancelText?: string, size?: DialogSize, };
|
||||
|
||||
export type PromptFormResponse = { values: { [key in string]?: JsonPrimitive } | null, };
|
||||
export type PromptFormResponse = { values: { [key in string]?: JsonPrimitive } | null, done?: boolean, };
|
||||
|
||||
export type PromptTextRequest = { id: string, title: string, label: string, description?: string, defaultValue?: string, placeholder?: string,
|
||||
/**
|
||||
|
||||
8
crates/yaak-plugins/bindings/gen_models.ts
generated
8
crates/yaak-plugins/bindings/gen_models.ts
generated
@@ -12,6 +12,8 @@ export type CookieExpires = { "AtUtc": string } | "SessionEnd";
|
||||
|
||||
export type CookieJar = { model: "cookie_jar", id: string, createdAt: string, updatedAt: string, workspaceId: string, cookies: Array<Cookie>, name: string, };
|
||||
|
||||
export type DnsOverride = { hostname: string, ipv4: Array<string>, ipv6: Array<string>, enabled?: boolean, };
|
||||
|
||||
export type EditorKeymap = "default" | "vim" | "vscode" | "emacs";
|
||||
|
||||
export type EncryptedKey = { encryptedKey: string, };
|
||||
@@ -38,7 +40,7 @@ export type HttpRequest = { model: "http_request", id: string, createdAt: string
|
||||
|
||||
export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||
|
||||
export type HttpResponse = { model: "http_response", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, bodyPath: string | null, contentLength: number | null, contentLengthCompressed: number | null, elapsed: number, elapsedHeaders: number, error: string | null, headers: Array<HttpResponseHeader>, remoteAddr: string | null, requestContentLength: number | null, requestHeaders: Array<HttpResponseHeader>, status: number, statusReason: string | null, state: HttpResponseState, url: string, version: string | null, };
|
||||
export type HttpResponse = { model: "http_response", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, bodyPath: string | null, contentLength: number | null, contentLengthCompressed: number | null, elapsed: number, elapsedHeaders: number, elapsedDns: number, error: string | null, headers: Array<HttpResponseHeader>, remoteAddr: string | null, requestContentLength: number | null, requestHeaders: Array<HttpResponseHeader>, status: number, statusReason: string | null, state: HttpResponseState, url: string, version: string | null, };
|
||||
|
||||
export type HttpResponseEvent = { model: "http_response_event", id: string, createdAt: string, updatedAt: string, workspaceId: string, responseId: string, event: HttpResponseEventData, };
|
||||
|
||||
@@ -47,7 +49,7 @@ export type HttpResponseEvent = { model: "http_response_event", id: string, crea
|
||||
* This mirrors `yaak_http::sender::HttpResponseEvent` but with serde support.
|
||||
* The `From` impl is in yaak-http to avoid circular dependencies.
|
||||
*/
|
||||
export type HttpResponseEventData = { "type": "setting", name: string, value: string, } | { "type": "info", message: string, } | { "type": "redirect", url: string, status: number, behavior: string, } | { "type": "send_url", method: string, path: string, } | { "type": "receive_url", version: string, status: string, } | { "type": "header_up", name: string, value: string, } | { "type": "header_down", name: string, value: string, } | { "type": "chunk_sent", bytes: number, } | { "type": "chunk_received", bytes: number, };
|
||||
export type HttpResponseEventData = { "type": "setting", name: string, value: string, } | { "type": "info", message: string, } | { "type": "redirect", url: string, status: number, behavior: string, } | { "type": "send_url", method: string, scheme: string, username: string, password: string, host: string, port: number, path: string, query: string, fragment: string, } | { "type": "receive_url", version: string, status: string, } | { "type": "header_up", name: string, value: string, } | { "type": "header_down", name: string, value: string, } | { "type": "chunk_sent", bytes: number, } | { "type": "chunk_received", bytes: number, } | { "type": "dns_resolved", hostname: string, addresses: Array<string>, duration: bigint, overridden: boolean, };
|
||||
|
||||
export type HttpResponseHeader = { name: string, value: string, };
|
||||
|
||||
@@ -77,6 +79,6 @@ export type WebsocketEventType = "binary" | "close" | "frame" | "open" | "ping"
|
||||
|
||||
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
|
||||
|
||||
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };
|
||||
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, settingDnsOverrides: Array<DnsOverride>, };
|
||||
|
||||
export type WorkspaceMeta = { model: "workspace_meta", id: string, workspaceId: string, createdAt: string, updatedAt: string, encryptionKey: EncryptedKey | null, settingSyncDir: string | null, };
|
||||
|
||||
@@ -80,10 +80,7 @@ pub async fn check_plugin_updates(
|
||||
}
|
||||
|
||||
/// Search for plugins in the registry.
|
||||
pub async fn search_plugins(
|
||||
http_client: &Client,
|
||||
query: &str,
|
||||
) -> Result<PluginSearchResponse> {
|
||||
pub async fn search_plugins(http_client: &Client, query: &str) -> Result<PluginSearchResponse> {
|
||||
let mut url = build_url("/search");
|
||||
{
|
||||
let mut query_pairs = url.query_pairs_mut();
|
||||
|
||||
@@ -587,6 +587,19 @@ pub struct PromptFormRequest {
|
||||
pub confirm_text: Option<String>,
|
||||
#[ts(optional)]
|
||||
pub cancel_text: Option<String>,
|
||||
#[ts(optional)]
|
||||
pub size: Option<DialogSize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(export, export_to = "gen_events.ts")]
|
||||
pub enum DialogSize {
|
||||
Sm,
|
||||
Md,
|
||||
Lg,
|
||||
Full,
|
||||
Dynamic,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
|
||||
@@ -594,6 +607,8 @@ pub struct PromptFormRequest {
|
||||
#[ts(export, export_to = "gen_events.ts")]
|
||||
pub struct PromptFormResponse {
|
||||
pub values: Option<HashMap<String, JsonPrimitive>>,
|
||||
#[ts(optional)]
|
||||
pub done: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
|
||||
@@ -936,6 +951,22 @@ pub enum EditorLanguage {
|
||||
Xml,
|
||||
Graphql,
|
||||
Markdown,
|
||||
C,
|
||||
Clojure,
|
||||
Csharp,
|
||||
Go,
|
||||
Http,
|
||||
Java,
|
||||
Kotlin,
|
||||
ObjectiveC,
|
||||
Ocaml,
|
||||
Php,
|
||||
Powershell,
|
||||
Python,
|
||||
R,
|
||||
Ruby,
|
||||
Shell,
|
||||
Swift,
|
||||
}
|
||||
|
||||
impl Default for EditorLanguage {
|
||||
@@ -966,6 +997,10 @@ pub struct FormInputEditor {
|
||||
#[ts(optional)]
|
||||
pub read_only: Option<bool>,
|
||||
|
||||
/// Fixed number of visible rows
|
||||
#[ts(optional)]
|
||||
pub rows: Option<i32>,
|
||||
|
||||
#[ts(optional)]
|
||||
pub completion_options: Option<Vec<GenericCompletionOption>>,
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ use std::time::Duration;
|
||||
use tokio::fs::read_dir;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::sync::mpsc::error::TrySendError;
|
||||
use tokio::sync::{Mutex, mpsc};
|
||||
use tokio::sync::{Mutex, mpsc, oneshot};
|
||||
use tokio::time::{Instant, timeout};
|
||||
use yaak_models::models::Plugin;
|
||||
use yaak_models::util::generate_id;
|
||||
@@ -43,6 +43,7 @@ pub struct PluginManager {
|
||||
subscribers: Arc<Mutex<HashMap<String, mpsc::Sender<InternalEvent>>>>,
|
||||
plugin_handles: Arc<Mutex<Vec<PluginHandle>>>,
|
||||
kill_tx: tokio::sync::watch::Sender<bool>,
|
||||
killed_rx: Arc<Mutex<Option<oneshot::Receiver<()>>>>,
|
||||
ws_service: Arc<PluginRuntimeServerWebsocket>,
|
||||
vendored_plugin_dir: PathBuf,
|
||||
pub(crate) installed_plugin_dir: PathBuf,
|
||||
@@ -70,6 +71,7 @@ impl PluginManager {
|
||||
) -> PluginManager {
|
||||
let (events_tx, mut events_rx) = mpsc::channel(2048);
|
||||
let (kill_server_tx, kill_server_rx) = tokio::sync::watch::channel(false);
|
||||
let (killed_tx, killed_rx) = oneshot::channel();
|
||||
|
||||
let (client_disconnect_tx, mut client_disconnect_rx) = mpsc::channel(128);
|
||||
let (client_connect_tx, mut client_connect_rx) = tokio::sync::watch::channel(false);
|
||||
@@ -81,6 +83,7 @@ impl PluginManager {
|
||||
subscribers: Default::default(),
|
||||
ws_service: Arc::new(ws_service.clone()),
|
||||
kill_tx: kill_server_tx,
|
||||
killed_rx: Arc::new(Mutex::new(Some(killed_rx))),
|
||||
vendored_plugin_dir,
|
||||
installed_plugin_dir,
|
||||
dev_mode,
|
||||
@@ -141,9 +144,15 @@ impl PluginManager {
|
||||
});
|
||||
|
||||
// 2. Start Node.js runtime
|
||||
start_nodejs_plugin_runtime(&node_bin_path, &plugin_runtime_main, addr, &kill_server_rx)
|
||||
.await
|
||||
.unwrap();
|
||||
start_nodejs_plugin_runtime(
|
||||
&node_bin_path,
|
||||
&plugin_runtime_main,
|
||||
addr,
|
||||
&kill_server_rx,
|
||||
killed_tx,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
info!("Waiting for plugins to initialize");
|
||||
init_plugins_task.await.unwrap();
|
||||
|
||||
@@ -296,8 +305,15 @@ impl PluginManager {
|
||||
pub async fn terminate(&self) {
|
||||
self.kill_tx.send_replace(true);
|
||||
|
||||
// Give it a bit of time to kill
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
// Wait for the plugin runtime process to actually exit
|
||||
let killed_rx = self.killed_rx.lock().await.take();
|
||||
if let Some(rx) = killed_rx {
|
||||
if timeout(Duration::from_secs(5), rx).await.is_err() {
|
||||
warn!("Timed out waiting for plugin runtime to exit");
|
||||
} else {
|
||||
info!("Plugin runtime exited")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn reply(
|
||||
@@ -378,7 +394,8 @@ impl PluginManager {
|
||||
plugins: Vec<PluginHandle>,
|
||||
timeout_duration: Duration,
|
||||
) -> Result<Vec<InternalEvent>> {
|
||||
let label = format!("wait[{}.{}]", plugins.len(), payload.type_name());
|
||||
let event_type = payload.type_name();
|
||||
let label = format!("wait[{}.{}]", plugins.len(), event_type);
|
||||
let (rx_id, mut rx) = self.subscribe(label.as_str()).await;
|
||||
|
||||
// 1. Build the events with IDs and everything
|
||||
@@ -412,10 +429,21 @@ impl PluginManager {
|
||||
|
||||
// Timeout to prevent hanging forever if plugin doesn't respond
|
||||
if timeout(timeout_duration, collect_events).await.is_err() {
|
||||
let responded_ids: Vec<&String> =
|
||||
found_events.iter().filter_map(|e| e.reply_id.as_ref()).collect();
|
||||
let non_responding: Vec<&str> = events_to_send
|
||||
.iter()
|
||||
.filter(|e| !responded_ids.contains(&&e.id))
|
||||
.map(|e| e.plugin_name.as_str())
|
||||
.collect();
|
||||
warn!(
|
||||
"Timeout waiting for plugin responses. Got {}/{} responses",
|
||||
"Timeout ({:?}) waiting for {} responses. Got {}/{} responses. \
|
||||
Non-responding plugins: [{}]",
|
||||
timeout_duration,
|
||||
event_type,
|
||||
found_events.len(),
|
||||
events_to_send.len()
|
||||
events_to_send.len(),
|
||||
non_responding.join(", ")
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -196,7 +196,11 @@ pub fn decrypt_secure_template_function(
|
||||
}
|
||||
}
|
||||
new_tokens.push(Token::Raw {
|
||||
text: template_function_secure_run(encryption_manager, args_map, plugin_context)?,
|
||||
text: template_function_secure_run(
|
||||
encryption_manager,
|
||||
args_map,
|
||||
plugin_context,
|
||||
)?,
|
||||
});
|
||||
}
|
||||
t => {
|
||||
@@ -216,7 +220,8 @@ pub fn encrypt_secure_template_function(
|
||||
plugin_context: &PluginContext,
|
||||
template: &str,
|
||||
) -> Result<String> {
|
||||
let decrypted = decrypt_secure_template_function(&encryption_manager, plugin_context, template)?;
|
||||
let decrypted =
|
||||
decrypt_secure_template_function(&encryption_manager, plugin_context, template)?;
|
||||
let tokens = Tokens {
|
||||
tokens: vec. Do not edit this file manually.
|
||||
|
||||
export type DnsOverride = { hostname: string, ipv4: Array<string>, ipv6: Array<string>, enabled?: boolean, };
|
||||
|
||||
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, parentModel: string, parentId: string | null, variables: Array<EnvironmentVariable>, color: string | null, sortPriority: number, };
|
||||
|
||||
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||
@@ -20,4 +22,4 @@ export type SyncState = { model: "sync_state", id: string, workspaceId: string,
|
||||
|
||||
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
|
||||
|
||||
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };
|
||||
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, settingDnsOverrides: Array<DnsOverride>, };
|
||||
|
||||
@@ -296,11 +296,7 @@ pub fn compute_sync_ops(
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn workspace_models(
|
||||
db: &DbContext,
|
||||
version: &str,
|
||||
workspace_id: &str,
|
||||
) -> Result<Vec<SyncModel>> {
|
||||
fn workspace_models(db: &DbContext, version: &str, workspace_id: &str) -> Result<Vec<SyncModel>> {
|
||||
// We want to include private environments here so that we can take them into account during
|
||||
// the sync process. Otherwise, they would be treated as deleted.
|
||||
let include_private_environments = true;
|
||||
|
||||
8
crates/yaak-templates/build-wasm.cjs
Normal file
8
crates/yaak-templates/build-wasm.cjs
Normal file
@@ -0,0 +1,8 @@
|
||||
const { execSync } = require('node:child_process');
|
||||
|
||||
if (process.env.SKIP_WASM_BUILD === '1') {
|
||||
console.log('Skipping wasm-pack build (SKIP_WASM_BUILD=1)');
|
||||
return;
|
||||
}
|
||||
|
||||
execSync('wasm-pack build --target bundler', { stdio: 'inherit' });
|
||||
@@ -6,7 +6,7 @@
|
||||
"scripts": {
|
||||
"bootstrap": "npm run build",
|
||||
"build": "run-s build:*",
|
||||
"build:pack": "wasm-pack build --target bundler",
|
||||
"build:pack": "node build-wasm.cjs",
|
||||
"build:clean": "rimraf ./pkg/.gitignore"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -81,6 +81,10 @@ impl RenderOptions {
|
||||
pub fn throw() -> Self {
|
||||
Self { error_behavior: RenderErrorBehavior::Throw }
|
||||
}
|
||||
|
||||
pub fn return_empty() -> Self {
|
||||
Self { error_behavior: RenderErrorBehavior::ReturnEmpty }
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderErrorBehavior {
|
||||
|
||||
@@ -1,31 +1,5 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { WebsocketConnection, WebsocketEvent, WebsocketRequest } from '@yaakapp-internal/models';
|
||||
|
||||
export function upsertWebsocketRequest(
|
||||
request: WebsocketRequest | Partial<Omit<WebsocketRequest, 'id'>>,
|
||||
) {
|
||||
return invoke('cmd_ws_upsert_request', {
|
||||
request,
|
||||
}) as Promise<WebsocketRequest>;
|
||||
}
|
||||
|
||||
export function duplicateWebsocketRequest(requestId: string) {
|
||||
return invoke('cmd_ws_duplicate_request', {
|
||||
requestId,
|
||||
}) as Promise<WebsocketRequest>;
|
||||
}
|
||||
|
||||
export function deleteWebsocketRequest(requestId: string) {
|
||||
return invoke('cmd_ws_delete_request', {
|
||||
requestId,
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteWebsocketConnection(connectionId: string) {
|
||||
return invoke('cmd_ws_delete_connection', {
|
||||
connectionId,
|
||||
});
|
||||
}
|
||||
import { WebsocketConnection } from '@yaakapp-internal/models';
|
||||
|
||||
export function deleteWebsocketConnections(requestId: string) {
|
||||
return invoke('cmd_ws_delete_connections', {
|
||||
@@ -33,20 +7,6 @@ export function deleteWebsocketConnections(requestId: string) {
|
||||
});
|
||||
}
|
||||
|
||||
export function listWebsocketRequests({ workspaceId }: { workspaceId: string }) {
|
||||
return invoke('cmd_ws_list_requests', { workspaceId }) as Promise<WebsocketRequest[]>;
|
||||
}
|
||||
|
||||
export function listWebsocketEvents({ connectionId }: { connectionId: string }) {
|
||||
return invoke('cmd_ws_list_events', { connectionId }) as Promise<WebsocketEvent[]>;
|
||||
}
|
||||
|
||||
export function listWebsocketConnections({ workspaceId }: { workspaceId: string }) {
|
||||
return invoke('cmd_ws_list_connections', { workspaceId }) as Promise<
|
||||
WebsocketConnection[]
|
||||
>;
|
||||
}
|
||||
|
||||
export function connectWebsocket({
|
||||
requestId,
|
||||
environmentId,
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::connect::ws_connect;
|
||||
use crate::error::Result;
|
||||
use futures_util::stream::SplitSink;
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use http::HeaderMap;
|
||||
use log::{debug, info, warn};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
@@ -10,7 +11,6 @@ use tokio::net::TcpStream;
|
||||
use tokio::sync::{Mutex, mpsc};
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
use tokio_tungstenite::tungstenite::handshake::client::Response;
|
||||
use http::HeaderMap;
|
||||
use tokio_tungstenite::tungstenite::http::HeaderValue;
|
||||
use tokio_tungstenite::{MaybeTlsStream, WebSocketStream};
|
||||
use yaak_tls::ClientCertificateConfig;
|
||||
|
||||
@@ -16,6 +16,9 @@ pub async fn render_websocket_request<T: TemplateCallback>(
|
||||
|
||||
let mut url_parameters = Vec::new();
|
||||
for p in r.url_parameters.clone() {
|
||||
if !p.enabled {
|
||||
continue;
|
||||
}
|
||||
url_parameters.push(HttpUrlParameter {
|
||||
enabled: p.enabled,
|
||||
name: parse_and_render(&p.name, vars, cb, opt).await?,
|
||||
@@ -26,6 +29,9 @@ pub async fn render_websocket_request<T: TemplateCallback>(
|
||||
|
||||
let mut headers = Vec::new();
|
||||
for p in r.headers.clone() {
|
||||
if !p.enabled {
|
||||
continue;
|
||||
}
|
||||
headers.push(HttpRequestHeader {
|
||||
enabled: p.enabled,
|
||||
name: parse_and_render(&p.name, vars, cb, opt).await?,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user