Compare commits
2 Commits
wip/yaak-p
...
pr-413
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae7b3ef0f4 | ||
|
|
b6ad51c838 |
@@ -6,14 +6,14 @@ Make Yaak runnable as a standalone CLI without Tauri as a dependency. The core R
|
||||
## Project Structure
|
||||
```
|
||||
crates/ # Core crates - should NOT depend on Tauri
|
||||
crates-tauri/ # Tauri-specific crates (yaak-app-client, yaak-tauri-utils, etc.)
|
||||
crates-tauri/ # Tauri-specific crates (yaak-app, yaak-tauri-utils, etc.)
|
||||
crates-cli/ # CLI crate (yaak-cli)
|
||||
```
|
||||
|
||||
## Completed Work
|
||||
|
||||
### 1. Folder Restructure
|
||||
- Moved Tauri-dependent app code to `crates-tauri/yaak-app-client/`
|
||||
- Moved Tauri-dependent app code to `crates-tauri/yaak-app/`
|
||||
- Created `crates-tauri/yaak-tauri-utils/` for shared Tauri utilities (window traits, api_client, error handling)
|
||||
- Created `crates-cli/yaak-cli/` for the standalone CLI
|
||||
|
||||
@@ -43,13 +43,13 @@ crates-cli/ # CLI crate (yaak-cli)
|
||||
3. Move extension traits (e.g., `SomethingManagerExt`) to yaak-app or yaak-tauri-utils
|
||||
4. Initialize managers in yaak-app's `.setup()` block
|
||||
5. Remove `tauri` from Cargo.toml dependencies
|
||||
6. Update `crates-tauri/yaak-app-client/capabilities/default.json` to remove the plugin permission
|
||||
6. Update `crates-tauri/yaak-app/capabilities/default.json` to remove the plugin permission
|
||||
7. Replace `tauri::async_runtime::block_on` with `tokio::runtime::Handle::current().block_on()`
|
||||
|
||||
## Key Files
|
||||
- `crates-tauri/yaak-app-client/src/lib.rs` - Main Tauri app, setup block initializes managers
|
||||
- `crates-tauri/yaak-app-client/src/commands.rs` - Migrated Tauri commands
|
||||
- `crates-tauri/yaak-app-client/src/models_ext.rs` - Database plugin and extension traits
|
||||
- `crates-tauri/yaak-app/src/lib.rs` - Main Tauri app, setup block initializes managers
|
||||
- `crates-tauri/yaak-app/src/commands.rs` - Migrated Tauri commands
|
||||
- `crates-tauri/yaak-app/src/models_ext.rs` - Database plugin and extension traits
|
||||
- `crates-tauri/yaak-tauri-utils/src/window.rs` - WorkspaceWindowTrait for window state
|
||||
- `crates/yaak-models/src/lib.rs` - Contains `init_standalone()` for CLI usage
|
||||
|
||||
@@ -68,5 +68,5 @@ e718a5f1 Refactor models_ext to use init_standalone from yaak-models
|
||||
|
||||
## Testing
|
||||
- Run `cargo check -p <crate>` to verify a crate builds without Tauri
|
||||
- Run `npm run client:dev` to test the Tauri app still works
|
||||
- Run `npm run app-dev` to test the Tauri app still works
|
||||
- Run `cargo run -p yaak-cli -- --help` to test the CLI
|
||||
|
||||
4
.gitattributes
vendored
@@ -1,5 +1,5 @@
|
||||
crates-tauri/yaak-app-client/vendored/**/* linguist-generated=true
|
||||
crates-tauri/yaak-app-client/gen/schemas/**/* linguist-generated=true
|
||||
crates-tauri/yaak-app/vendored/**/* linguist-generated=true
|
||||
crates-tauri/yaak-app/gen/schemas/**/* linguist-generated=true
|
||||
**/bindings/* linguist-generated=true
|
||||
crates/yaak-templates/pkg/* linguist-generated=true
|
||||
|
||||
|
||||
8
.github/workflows/release-app.yml
vendored
@@ -122,8 +122,8 @@ jobs:
|
||||
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-client/macos/entitlements.yaakprotoc.plist --sign "$APPLE_SIGNING_IDENTITY" crates-tauri/yaak-app-client/vendored/protoc/yaakprotoc || true
|
||||
codesign --force --options runtime --entitlements crates-tauri/yaak-app-client/macos/entitlements.yaaknode.plist --sign "$APPLE_SIGNING_IDENTITY" crates-tauri/yaak-app-client/vendored/node/yaaknode || true
|
||||
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:
|
||||
@@ -152,7 +152,7 @@ jobs:
|
||||
releaseBody: "[Changelog __VERSION__](https://yaak.app/blog/__VERSION__)"
|
||||
releaseDraft: true
|
||||
prerelease: true
|
||||
args: "${{ matrix.args }} --config ./crates-tauri/yaak-app-client/tauri.release.conf.json"
|
||||
args: "${{ matrix.args }} --config ./crates-tauri/yaak-app/tauri.release.conf.json"
|
||||
|
||||
# Build a per-machine NSIS installer for enterprise deployment (PDQ, SCCM, Intune)
|
||||
- name: Build and upload machine-wide installer (Windows only)
|
||||
@@ -168,7 +168,7 @@ jobs:
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
run: |
|
||||
Get-ChildItem -Recurse -Path target -File -Filter "*.exe.sig" | Remove-Item -Force
|
||||
npx tauri bundle ${{ matrix.args }} --bundles nsis --config ./crates-tauri/yaak-app-client/tauri.release.conf.json --config '{"bundle":{"createUpdaterArtifacts":true,"windows":{"nsis":{"installMode":"perMachine"}}}}'
|
||||
npx tauri bundle ${{ matrix.args }} --bundles nsis --config ./crates-tauri/yaak-app/tauri.release.conf.json --config '{"bundle":{"createUpdaterArtifacts":true,"windows":{"nsis":{"installMode":"perMachine"}}}}'
|
||||
$setup = Get-ChildItem -Recurse -Path target -Filter "*setup*.exe" | Select-Object -First 1
|
||||
$setupSig = "$($setup.FullName).sig"
|
||||
$dest = $setup.FullName -replace '-setup\.exe$', '-setup-machine.exe'
|
||||
|
||||
6
.github/workflows/release-cli-npm.yml
vendored
@@ -45,8 +45,8 @@ jobs:
|
||||
with:
|
||||
name: vendored-assets
|
||||
path: |
|
||||
crates-tauri/yaak-app-client/vendored/plugin-runtime/index.cjs
|
||||
crates-tauri/yaak-app-client/vendored/plugins
|
||||
crates-tauri/yaak-app/vendored/plugin-runtime/index.cjs
|
||||
crates-tauri/yaak-app/vendored/plugins
|
||||
if-no-files-found: error
|
||||
|
||||
build-binaries:
|
||||
@@ -107,7 +107,7 @@ jobs:
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: vendored-assets
|
||||
path: crates-tauri/yaak-app-client/vendored
|
||||
path: crates-tauri/yaak-app/vendored
|
||||
|
||||
- name: Set CLI build version
|
||||
shell: bash
|
||||
|
||||
3
.gitignore
vendored
@@ -39,8 +39,7 @@ codebook.toml
|
||||
target
|
||||
|
||||
# Per-worktree Tauri config (generated by post-checkout hook)
|
||||
crates-tauri/yaak-app-client/tauri.worktree.conf.json
|
||||
crates-tauri/yaak-app-proxy/tauri.worktree.conf.json
|
||||
crates-tauri/yaak-app/tauri.worktree.conf.json
|
||||
|
||||
# Tauri auto-generated permission files
|
||||
**/permissions/autogenerated
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
- Tag safety: app releases use `v*` tags and CLI releases use `yaak-cli-*` tags; always confirm which one is requested before retagging.
|
||||
- Do not commit, push, or tag without explicit approval
|
||||
|
||||
277
Cargo.lock
generated
@@ -477,28 +477,6 @@ version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
|
||||
|
||||
[[package]]
|
||||
name = "aws-lc-rs"
|
||||
version = "1.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf"
|
||||
dependencies = [
|
||||
"aws-lc-sys",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-lc-sys"
|
||||
version = "0.38.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cmake",
|
||||
"dunce",
|
||||
"fs_extra",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum"
|
||||
version = "0.7.9"
|
||||
@@ -1222,7 +1200,7 @@ dependencies = [
|
||||
"encode_unicode",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"unicode-width 0.2.2",
|
||||
"unicode-width",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
@@ -1427,31 +1405,6 @@ version = "0.8.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"crossterm_winapi",
|
||||
"libc",
|
||||
"mio 0.8.11",
|
||||
"parking_lot",
|
||||
"signal-hook",
|
||||
"signal-hook-mio",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossterm_winapi"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.3"
|
||||
@@ -2214,12 +2167,6 @@ dependencies = [
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fs_extra"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
||||
|
||||
[[package]]
|
||||
name = "fsevent-sys"
|
||||
version = "4.1.0"
|
||||
@@ -2347,15 +2294,6 @@ dependencies = [
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fuzzy-matcher"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94"
|
||||
dependencies = [
|
||||
"thread_local 1.1.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fxhash"
|
||||
version = "0.2.1"
|
||||
@@ -3226,24 +3164,6 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inquire"
|
||||
version = "0.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fddf93031af70e75410a2511ec04d49e758ed2f26dad3404a934e0fb45cc12a"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"crossterm",
|
||||
"dyn-clone",
|
||||
"fuzzy-matcher",
|
||||
"fxhash",
|
||||
"newline-converter",
|
||||
"once_cell",
|
||||
"tempfile",
|
||||
"unicode-segmentation",
|
||||
"unicode-width 0.1.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "interfaces"
|
||||
version = "0.0.8"
|
||||
@@ -3836,18 +3756,6 @@ dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log 0.4.29",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.0.4"
|
||||
@@ -3943,15 +3851,6 @@ version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
|
||||
|
||||
[[package]]
|
||||
name = "newline-converter"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b6b097ecb1cbfed438542d16e84fd7ad9b0c76c8a65b7f9039212a3d14dc7f"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nibble_vec"
|
||||
version = "0.1.0"
|
||||
@@ -4043,7 +3942,7 @@ dependencies = [
|
||||
"kqueue",
|
||||
"libc",
|
||||
"log 0.4.29",
|
||||
"mio 1.0.4",
|
||||
"mio",
|
||||
"notify-types",
|
||||
"walkdir",
|
||||
"windows-sys 0.59.0",
|
||||
@@ -4602,7 +4501,7 @@ dependencies = [
|
||||
"textwrap",
|
||||
"thiserror 2.0.17",
|
||||
"unicode-segmentation",
|
||||
"unicode-width 0.2.2",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5143,16 +5042,6 @@ dependencies = [
|
||||
"hmac",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pem"
|
||||
version = "3.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.2"
|
||||
@@ -5993,19 +5882,6 @@ dependencies = [
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rcgen"
|
||||
version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2"
|
||||
dependencies = [
|
||||
"pem",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"time",
|
||||
"yasna",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.12"
|
||||
@@ -6295,7 +6171,7 @@ version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77dff57c9de498bb1eb5b1ce682c2e3a0ae956b266fa0933c3e151b87b078967"
|
||||
dependencies = [
|
||||
"unicode-width 0.2.2",
|
||||
"unicode-width",
|
||||
"yansi",
|
||||
]
|
||||
|
||||
@@ -6320,7 +6196,7 @@ dependencies = [
|
||||
"kqueue",
|
||||
"libc",
|
||||
"log 0.4.29",
|
||||
"mio 1.0.4",
|
||||
"mio",
|
||||
"rolldown-notify-types",
|
||||
"walkdir",
|
||||
"windows-sys 0.61.2",
|
||||
@@ -6739,8 +6615,6 @@ version = "0.23.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a9586e9ee2b4f8fab52a0048ca7334d7024eef48e2cb9407e3497bb7cab7fa7"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"log 0.4.29",
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
@@ -6813,7 +6687,6 @@ version = "0.103.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"untrusted",
|
||||
@@ -7300,27 +7173,6 @@ version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
version = "0.3.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"signal-hook-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-mio"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"mio 0.8.11",
|
||||
"signal-hook",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.5"
|
||||
@@ -8216,7 +8068,7 @@ checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
|
||||
dependencies = [
|
||||
"smawk",
|
||||
"unicode-linebreak",
|
||||
"unicode-width 0.2.2",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8363,7 +8215,7 @@ checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio 1.0.4",
|
||||
"mio",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2 0.6.1",
|
||||
@@ -8933,12 +8785,6 @@ version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.2.2"
|
||||
@@ -9716,15 +9562,6 @@ dependencies = [
|
||||
"windows-targets 0.42.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
|
||||
dependencies = [
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
@@ -10240,7 +10077,7 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yaak-app-client"
|
||||
name = "yaak-app"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"charset",
|
||||
@@ -10297,29 +10134,13 @@ dependencies = [
|
||||
"yaak-tauri-utils",
|
||||
"yaak-templates",
|
||||
"yaak-tls",
|
||||
"yaak-window",
|
||||
"yaak-ws",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yaak-app-proxy"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"log 0.4.29",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-os",
|
||||
"yaak-proxy-lib",
|
||||
"yaak-rpc",
|
||||
"yaak-window",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yaak-cli"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"arboard",
|
||||
"assert_cmd",
|
||||
"base64 0.22.1",
|
||||
"clap",
|
||||
@@ -10329,7 +10150,6 @@ dependencies = [
|
||||
"futures",
|
||||
"hex",
|
||||
"include_dir",
|
||||
"inquire",
|
||||
"keyring",
|
||||
"log 0.4.29",
|
||||
"oxc_resolver",
|
||||
@@ -10346,7 +10166,6 @@ dependencies = [
|
||||
"walkdir",
|
||||
"webbrowser",
|
||||
"yaak",
|
||||
"yaak-api",
|
||||
"yaak-crypto",
|
||||
"yaak-http",
|
||||
"yaak-models",
|
||||
@@ -10384,25 +10203,6 @@ dependencies = [
|
||||
"yaak-models",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yaak-database"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"include_dir",
|
||||
"log 0.4.29",
|
||||
"nanoid",
|
||||
"r2d2",
|
||||
"r2d2_sqlite",
|
||||
"rusqlite",
|
||||
"sea-query",
|
||||
"sea-query-rusqlite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.17",
|
||||
"ts-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yaak-fonts"
|
||||
version = "0.1.0"
|
||||
@@ -10543,7 +10343,6 @@ dependencies = [
|
||||
"thiserror 2.0.17",
|
||||
"ts-rs",
|
||||
"yaak-core",
|
||||
"yaak-database",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10575,52 +10374,6 @@ dependencies = [
|
||||
"zip-extract",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yaak-proxy"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"pem",
|
||||
"rcgen",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yaak-proxy-lib"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"include_dir",
|
||||
"log 0.4.29",
|
||||
"r2d2",
|
||||
"r2d2_sqlite",
|
||||
"rusqlite",
|
||||
"sea-query",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"ts-rs",
|
||||
"yaak-database",
|
||||
"yaak-proxy",
|
||||
"yaak-rpc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yaak-rpc"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"log 0.4.29",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"ts-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yaak-sse"
|
||||
version = "0.1.0"
|
||||
@@ -10686,17 +10439,6 @@ dependencies = [
|
||||
"yaak-models",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yaak-window"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"log 0.4.29",
|
||||
"md5 0.8.0",
|
||||
"rand 0.9.1",
|
||||
"tauri",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yaak-ws"
|
||||
version = "0.1.0"
|
||||
@@ -10728,9 +10470,6 @@ name = "yasna"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd"
|
||||
dependencies = [
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
|
||||
19
Cargo.toml
@@ -2,9 +2,6 @@
|
||||
resolver = "2"
|
||||
members = [
|
||||
"crates/yaak",
|
||||
# Common/foundation crates
|
||||
"crates/common/yaak-database",
|
||||
"crates/common/yaak-rpc",
|
||||
# Shared crates (no Tauri dependency)
|
||||
"crates/yaak-core",
|
||||
"crates/yaak-common",
|
||||
@@ -20,19 +17,14 @@ members = [
|
||||
"crates/yaak-tls",
|
||||
"crates/yaak-ws",
|
||||
"crates/yaak-api",
|
||||
"crates/yaak-proxy",
|
||||
# Proxy-specific crates
|
||||
"crates-proxy/yaak-proxy-lib",
|
||||
# CLI crates
|
||||
"crates-cli/yaak-cli",
|
||||
# Tauri-specific crates
|
||||
"crates-tauri/yaak-app-client",
|
||||
"crates-tauri/yaak-app-proxy",
|
||||
"crates-tauri/yaak-app",
|
||||
"crates-tauri/yaak-fonts",
|
||||
"crates-tauri/yaak-license",
|
||||
"crates-tauri/yaak-mac-window",
|
||||
"crates-tauri/yaak-tauri-utils",
|
||||
"crates-tauri/yaak-window",
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
@@ -55,10 +47,6 @@ thiserror = "2.0.17"
|
||||
tokio = "1.48.0"
|
||||
ts-rs = "11.1.0"
|
||||
|
||||
# Internal crates - common/foundation
|
||||
yaak-database = { path = "crates/common/yaak-database" }
|
||||
yaak-rpc = { path = "crates/common/yaak-rpc" }
|
||||
|
||||
# Internal crates - shared
|
||||
yaak-core = { path = "crates/yaak-core" }
|
||||
yaak = { path = "crates/yaak" }
|
||||
@@ -75,17 +63,12 @@ yaak-templates = { path = "crates/yaak-templates" }
|
||||
yaak-tls = { path = "crates/yaak-tls" }
|
||||
yaak-ws = { path = "crates/yaak-ws" }
|
||||
yaak-api = { path = "crates/yaak-api" }
|
||||
yaak-proxy = { path = "crates/yaak-proxy" }
|
||||
|
||||
# Internal crates - proxy
|
||||
yaak-proxy-lib = { path = "crates-proxy/yaak-proxy-lib" }
|
||||
|
||||
# Internal crates - Tauri-specific
|
||||
yaak-fonts = { path = "crates-tauri/yaak-fonts" }
|
||||
yaak-license = { path = "crates-tauri/yaak-license" }
|
||||
yaak-mac-window = { path = "crates-tauri/yaak-mac-window" }
|
||||
yaak-tauri-utils = { path = "crates-tauri/yaak-tauri-utils" }
|
||||
yaak-window = { path = "crates-tauri/yaak-window" }
|
||||
|
||||
[profile.release]
|
||||
strip = false
|
||||
|
||||
@@ -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/crates-tauri/yaak-app-client/icons/icon.png">
|
||||
<img width="200px" src="https://github.com/mountain-loop/yaak/raw/main/crates-tauri/yaak-app/icons/icon.png">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import deepEqual from "@gilbarbara/deep-equal";
|
||||
import type { UpdateInfo } from "@yaakapp-internal/tauri-client";
|
||||
import type { Atom } from "jotai";
|
||||
import { atom } from "jotai";
|
||||
import { selectAtom } from "jotai/utils";
|
||||
import type { SplitLayoutLayout } from "../components/core/SplitLayout";
|
||||
import { atomWithKVStorage } from "./atoms/atomWithKVStorage";
|
||||
|
||||
export function deepEqualAtom<T>(a: Atom<T>) {
|
||||
return selectAtom(
|
||||
a,
|
||||
(v) => v,
|
||||
(a, b) => deepEqual(a, b),
|
||||
);
|
||||
}
|
||||
|
||||
export const workspaceLayoutAtom = atomWithKVStorage<SplitLayoutLayout>(
|
||||
"workspace_layout",
|
||||
"horizontal",
|
||||
);
|
||||
|
||||
export const updateAvailableAtom = atom<Omit<
|
||||
UpdateInfo,
|
||||
"replyEventId"
|
||||
> | null>(null);
|
||||
@@ -1,322 +0,0 @@
|
||||
import { emit } from "@tauri-apps/api/event";
|
||||
import { openUrl } from "@tauri-apps/plugin-opener";
|
||||
import { debounce } from "@yaakapp-internal/lib";
|
||||
import type {
|
||||
FormInput,
|
||||
InternalEvent,
|
||||
JsonPrimitive,
|
||||
ShowToastRequest,
|
||||
} from "@yaakapp-internal/plugins";
|
||||
import { updateAllPlugins } from "@yaakapp-internal/plugins";
|
||||
import type {
|
||||
PluginUpdateNotification,
|
||||
UpdateInfo,
|
||||
UpdateResponse,
|
||||
YaakNotification,
|
||||
} from "@yaakapp-internal/tauri-client";
|
||||
import { openSettings } from "../commands/openSettings";
|
||||
import { Button } from "../components/core/Button";
|
||||
import { ButtonInfiniteLoading } from "../components/core/ButtonInfiniteLoading";
|
||||
import { Icon } from "@yaakapp-internal/ui";
|
||||
import { HStack, VStack } from "../components/core/Stacks";
|
||||
|
||||
// Listen for toasts
|
||||
import { listenToTauriEvent } from "../hooks/useListenToTauriEvent";
|
||||
import { updateAvailableAtom } from "./atoms";
|
||||
import { stringToColor } from "./color";
|
||||
import { generateId } from "./generateId";
|
||||
import { jotaiStore } from "./jotai";
|
||||
import { showPrompt } from "./prompt";
|
||||
import { showPromptForm } from "./prompt-form";
|
||||
import { invokeCmd } from "./tauri";
|
||||
import { showToast } from "./toast";
|
||||
|
||||
export function initGlobalListeners() {
|
||||
listenToTauriEvent<ShowToastRequest>("show_toast", (event) => {
|
||||
showToast({ ...event.payload });
|
||||
});
|
||||
|
||||
listenToTauriEvent("settings", () => openSettings.mutate(null));
|
||||
|
||||
// Track active dynamic form dialogs so follow-up input updates can reach them
|
||||
const activeForms = new Map<string, (inputs: FormInput[]) => void>();
|
||||
|
||||
// Listen for plugin events
|
||||
listenToTauriEvent<InternalEvent>(
|
||||
"plugin_event",
|
||||
async ({ payload: event }) => {
|
||||
if (event.payload.type === "prompt_text_request") {
|
||||
const value = await showPrompt(event.payload);
|
||||
const result: InternalEvent = {
|
||||
id: generateId(),
|
||||
replyId: event.id,
|
||||
pluginName: event.pluginName,
|
||||
pluginRefId: event.pluginRefId,
|
||||
context: event.context,
|
||||
payload: {
|
||||
type: "prompt_text_response",
|
||||
value,
|
||||
},
|
||||
};
|
||||
await emit(event.id, result);
|
||||
} else if (event.payload.type === "prompt_form_request") {
|
||||
if (event.replyId != null) {
|
||||
// Follow-up update from plugin runtime — update the active dialog's inputs
|
||||
const updateInputs = activeForms.get(event.replyId);
|
||||
if (updateInputs) {
|
||||
updateInputs(event.payload.inputs);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Initial request — show the dialog with bidirectional support
|
||||
const emitFormResponse = (
|
||||
values: Record<string, JsonPrimitive> | null,
|
||||
done: boolean,
|
||||
) => {
|
||||
const result: InternalEvent = {
|
||||
id: generateId(),
|
||||
replyId: event.id,
|
||||
pluginName: event.pluginName,
|
||||
pluginRefId: event.pluginRefId,
|
||||
context: event.context,
|
||||
payload: {
|
||||
type: "prompt_form_response",
|
||||
values,
|
||||
done,
|
||||
},
|
||||
};
|
||||
emit(event.id, result);
|
||||
};
|
||||
|
||||
const values = await showPromptForm({
|
||||
id: event.payload.id,
|
||||
title: event.payload.title,
|
||||
description: event.payload.description,
|
||||
size: event.payload.size,
|
||||
inputs: event.payload.inputs,
|
||||
confirmText: event.payload.confirmText,
|
||||
cancelText: event.payload.cancelText,
|
||||
onValuesChange: debounce(
|
||||
(values) => emitFormResponse(values, false),
|
||||
150,
|
||||
),
|
||||
onInputsUpdated: (cb) => activeForms.set(event.id, cb),
|
||||
});
|
||||
|
||||
// Clean up and send final response
|
||||
activeForms.delete(event.id);
|
||||
emitFormResponse(values, true);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
listenToTauriEvent<string>(
|
||||
"update_installed",
|
||||
async ({ payload: version }) => {
|
||||
console.log("Got update installed event", version);
|
||||
showUpdateInstalledToast(version);
|
||||
},
|
||||
);
|
||||
|
||||
// Listen for update events
|
||||
listenToTauriEvent<UpdateInfo>("update_available", async ({ payload }) => {
|
||||
console.log("Got update available", payload);
|
||||
showUpdateAvailableToast(payload);
|
||||
});
|
||||
|
||||
listenToTauriEvent<YaakNotification>("notification", ({ payload }) => {
|
||||
console.log("Got notification event", payload);
|
||||
showNotificationToast(payload);
|
||||
});
|
||||
|
||||
// Listen for plugin update events
|
||||
listenToTauriEvent<PluginUpdateNotification>(
|
||||
"plugin_updates_available",
|
||||
({ payload }) => {
|
||||
console.log("Got plugin updates event", payload);
|
||||
showPluginUpdatesToast(payload);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function showUpdateInstalledToast(version: string) {
|
||||
const UPDATE_TOAST_ID = "update-info";
|
||||
|
||||
showToast({
|
||||
id: UPDATE_TOAST_ID,
|
||||
color: "primary",
|
||||
timeout: null,
|
||||
message: (
|
||||
<VStack>
|
||||
<h2 className="font-semibold">Yaak {version} was installed</h2>
|
||||
<p className="text-text-subtle text-sm">
|
||||
Start using the new version now?
|
||||
</p>
|
||||
</VStack>
|
||||
),
|
||||
action: ({ hide }) => (
|
||||
<ButtonInfiniteLoading
|
||||
size="xs"
|
||||
className="mr-auto min-w-[5rem]"
|
||||
color="primary"
|
||||
loadingChildren="Restarting..."
|
||||
onClick={() => {
|
||||
hide();
|
||||
setTimeout(() => invokeCmd("cmd_restart", {}), 200);
|
||||
}}
|
||||
>
|
||||
Relaunch Yaak
|
||||
</ButtonInfiniteLoading>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
async function showUpdateAvailableToast(updateInfo: UpdateInfo) {
|
||||
const UPDATE_TOAST_ID = "update-info";
|
||||
const { version, replyEventId, downloaded } = updateInfo;
|
||||
|
||||
jotaiStore.set(updateAvailableAtom, { version, downloaded });
|
||||
|
||||
// Acknowledge the event, so we don't time out and try the fallback update logic
|
||||
await emit<UpdateResponse>(replyEventId, { type: "ack" });
|
||||
|
||||
showToast({
|
||||
id: UPDATE_TOAST_ID,
|
||||
color: "info",
|
||||
timeout: null,
|
||||
message: (
|
||||
<VStack>
|
||||
<h2 className="font-semibold">Yaak {version} is available</h2>
|
||||
<p className="text-text-subtle text-sm">
|
||||
{downloaded ? "Do you want to install" : "Download and install"} the
|
||||
update?
|
||||
</p>
|
||||
</VStack>
|
||||
),
|
||||
action: () => (
|
||||
<HStack space={1.5}>
|
||||
<ButtonInfiniteLoading
|
||||
size="xs"
|
||||
color="info"
|
||||
className="min-w-[10rem]"
|
||||
loadingChildren={downloaded ? "Installing..." : "Downloading..."}
|
||||
onClick={async () => {
|
||||
await emit<UpdateResponse>(replyEventId, {
|
||||
type: "action",
|
||||
action: "install",
|
||||
});
|
||||
}}
|
||||
>
|
||||
{downloaded ? "Install Now" : "Download and Install"}
|
||||
</ButtonInfiniteLoading>
|
||||
<Button
|
||||
size="xs"
|
||||
color="info"
|
||||
variant="border"
|
||||
rightSlot={<Icon icon="external_link" />}
|
||||
onClick={async () => {
|
||||
await openUrl(`https://yaak.app/changelog/${version}`);
|
||||
}}
|
||||
>
|
||||
What's New
|
||||
</Button>
|
||||
</HStack>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
function showPluginUpdatesToast(updateInfo: PluginUpdateNotification) {
|
||||
const PLUGIN_UPDATE_TOAST_ID = "plugin-updates";
|
||||
const count = updateInfo.updateCount;
|
||||
const pluginNames = updateInfo.plugins.map((p: { name: string }) => p.name);
|
||||
|
||||
showToast({
|
||||
id: PLUGIN_UPDATE_TOAST_ID,
|
||||
color: "info",
|
||||
timeout: null,
|
||||
message: (
|
||||
<VStack>
|
||||
<h2 className="font-semibold">
|
||||
{count === 1 ? "1 plugin update" : `${count} plugin updates`}{" "}
|
||||
available
|
||||
</h2>
|
||||
<p className="text-text-subtle text-sm">
|
||||
{count === 1
|
||||
? pluginNames[0]
|
||||
: `${pluginNames.slice(0, 2).join(", ")}${count > 2 ? `, and ${count - 2} more` : ""}`}
|
||||
</p>
|
||||
</VStack>
|
||||
),
|
||||
action: ({ hide }) => (
|
||||
<HStack space={1.5}>
|
||||
<ButtonInfiniteLoading
|
||||
size="xs"
|
||||
color="info"
|
||||
className="min-w-[5rem]"
|
||||
loadingChildren="Updating..."
|
||||
onClick={async () => {
|
||||
const updated = await updateAllPlugins();
|
||||
hide();
|
||||
if (updated.length > 0) {
|
||||
showToast({
|
||||
color: "success",
|
||||
message: `Successfully updated ${updated.length} plugin${updated.length === 1 ? "" : "s"}`,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
Update All
|
||||
</ButtonInfiniteLoading>
|
||||
<Button
|
||||
size="xs"
|
||||
color="info"
|
||||
variant="border"
|
||||
onClick={() => {
|
||||
hide();
|
||||
openSettings.mutate("plugins:installed");
|
||||
}}
|
||||
>
|
||||
View Updates
|
||||
</Button>
|
||||
</HStack>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
function showNotificationToast(n: YaakNotification) {
|
||||
const actionUrl = n.action?.url;
|
||||
const actionLabel = n.action?.label;
|
||||
showToast({
|
||||
id: n.id,
|
||||
timeout: n.timeout ?? null,
|
||||
color: stringToColor(n.color) ?? undefined,
|
||||
message: (
|
||||
<VStack>
|
||||
{n.title && <h2 className="font-semibold">{n.title}</h2>}
|
||||
<p className="text-text-subtle text-sm">{n.message}</p>
|
||||
</VStack>
|
||||
),
|
||||
onClose: () => {
|
||||
invokeCmd("cmd_dismiss_notification", { notificationId: n.id }).catch(
|
||||
console.error,
|
||||
);
|
||||
},
|
||||
action: ({ hide }) => {
|
||||
return actionLabel && actionUrl ? (
|
||||
<Button
|
||||
size="xs"
|
||||
color={stringToColor(n.color) ?? undefined}
|
||||
className="mr-auto min-w-[5rem]"
|
||||
rightSlot={<Icon icon="external_link" />}
|
||||
onClick={() => {
|
||||
hide();
|
||||
return openUrl(actionUrl);
|
||||
}}
|
||||
>
|
||||
{actionLabel}
|
||||
</Button>
|
||||
) : null;
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
export type { Appearance } from "@yaakapp-internal/theme";
|
||||
export {
|
||||
getCSSAppearance,
|
||||
getWindowAppearance,
|
||||
resolveAppearance,
|
||||
subscribeToPreferredAppearance,
|
||||
subscribeToWindowAppearanceChange,
|
||||
} from "@yaakapp-internal/theme";
|
||||
@@ -1,45 +0,0 @@
|
||||
import type { GetThemesResponse } from "@yaakapp-internal/plugins";
|
||||
import { defaultDarkTheme, defaultLightTheme } from "@yaakapp-internal/theme";
|
||||
import { invokeCmd } from "../tauri";
|
||||
import type { Appearance } from "./appearance";
|
||||
import { resolveAppearance } from "./appearance";
|
||||
|
||||
export { defaultDarkTheme, defaultLightTheme } from "@yaakapp-internal/theme";
|
||||
|
||||
export async function getThemes() {
|
||||
const themes = (
|
||||
await invokeCmd<GetThemesResponse[]>("cmd_get_themes")
|
||||
).flatMap((t) => t.themes);
|
||||
themes.sort((a, b) => a.label.localeCompare(b.label));
|
||||
// Remove duplicates, in case multiple plugins provide the same theme
|
||||
const uniqueThemes = Array.from(
|
||||
new Map(themes.map((t) => [t.id, t])).values(),
|
||||
);
|
||||
return { themes: [defaultDarkTheme, defaultLightTheme, ...uniqueThemes] };
|
||||
}
|
||||
|
||||
export async function getResolvedTheme(
|
||||
preferredAppearance: Appearance,
|
||||
appearanceSetting: string,
|
||||
themeLight: string,
|
||||
themeDark: string,
|
||||
) {
|
||||
const appearance = resolveAppearance(preferredAppearance, appearanceSetting);
|
||||
const { themes } = await getThemes();
|
||||
|
||||
const darkThemes = themes.filter((t) => t.dark);
|
||||
const lightThemes = themes.filter((t) => !t.dark);
|
||||
|
||||
const dark =
|
||||
darkThemes.find((t) => t.id === themeDark) ??
|
||||
darkThemes[0] ??
|
||||
defaultDarkTheme;
|
||||
const light =
|
||||
lightThemes.find((t) => t.id === themeLight) ??
|
||||
lightThemes[0] ??
|
||||
defaultLightTheme;
|
||||
|
||||
const active = appearance === "dark" ? dark : light;
|
||||
|
||||
return { dark, light, active };
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
export type {
|
||||
YaakColorKey,
|
||||
YaakColors,
|
||||
YaakTheme,
|
||||
} from "@yaakapp-internal/theme";
|
||||
export {
|
||||
addThemeStylesToDocument,
|
||||
applyThemeToDocument,
|
||||
completeTheme,
|
||||
getThemeCSS,
|
||||
indent,
|
||||
setThemeOnDocument,
|
||||
} from "@yaakapp-internal/theme";
|
||||
@@ -1 +0,0 @@
|
||||
export { YaakColor } from "@yaakapp-internal/theme";
|
||||
@@ -1,47 +0,0 @@
|
||||
import "./main.css";
|
||||
import { RouterProvider } from "@tanstack/react-router";
|
||||
import { type } from "@tauri-apps/plugin-os";
|
||||
import {
|
||||
changeModelStoreWorkspace,
|
||||
initModelStore,
|
||||
} from "@yaakapp-internal/models";
|
||||
import { setPlatformOnDocument } from "@yaakapp-internal/theme";
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { initSync } from "./init/sync";
|
||||
import { initGlobalListeners } from "./lib/initGlobalListeners";
|
||||
import { jotaiStore } from "./lib/jotai";
|
||||
import { router } from "./lib/router";
|
||||
|
||||
const osType = type();
|
||||
setPlatformOnDocument(osType);
|
||||
|
||||
window.addEventListener("keydown", (e) => {
|
||||
const rx = /input|select|textarea/i;
|
||||
|
||||
const target = e.target;
|
||||
if (e.key !== "Backspace") return;
|
||||
if (!(target instanceof Element)) return;
|
||||
if (target.getAttribute("contenteditable") !== null) return;
|
||||
|
||||
if (
|
||||
!rx.test(target.tagName) ||
|
||||
("disabled" in target && target.disabled) ||
|
||||
("readOnly" in target && target.readOnly)
|
||||
) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize a bunch of watchers
|
||||
initSync();
|
||||
initModelStore(jotaiStore);
|
||||
initGlobalListeners();
|
||||
await changeModelStoreWorkspace(null); // Load global models
|
||||
|
||||
console.log("Creating React root");
|
||||
createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
<StrictMode>
|
||||
<RouterProvider router={router} />
|
||||
</StrictMode>,
|
||||
);
|
||||
@@ -1,16 +0,0 @@
|
||||
const sharedConfig = require("@yaakapp-internal/tailwind-config");
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
...sharedConfig,
|
||||
content: [
|
||||
"./*.{html,ts,tsx}",
|
||||
"./commands/**/*.{ts,tsx}",
|
||||
"./components/**/*.{ts,tsx}",
|
||||
"./hooks/**/*.{ts,tsx}",
|
||||
"./init/**/*.{ts,tsx}",
|
||||
"./lib/**/*.{ts,tsx}",
|
||||
"./routes/**/*.{ts,tsx}",
|
||||
"../../packages/ui/src/**/*.{ts,tsx}",
|
||||
],
|
||||
};
|
||||
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2021",
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"useDefineForClassFields": true,
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"paths": {
|
||||
"@yaakapp-internal/theme": ["../../packages/theme/src/index.ts"],
|
||||
"@yaakapp-internal/theme/*": ["../../packages/theme/src/*"],
|
||||
"@yaakapp-internal/ui": ["../../packages/ui/src/index.ts"],
|
||||
"@yaakapp-internal/ui/*": ["../../packages/ui/src/*"],
|
||||
},
|
||||
},
|
||||
"include": ["."],
|
||||
"exclude": ["vite.config.ts"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }],
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
// @ts-ignore
|
||||
import { tanstackRouter } from "@tanstack/router-plugin/vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
import { defineConfig, normalizePath } from "vite";
|
||||
import { viteStaticCopy } from "vite-plugin-static-copy";
|
||||
import svgr from "vite-plugin-svgr";
|
||||
import topLevelAwait from "vite-plugin-top-level-await";
|
||||
import wasm from "vite-plugin-wasm";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const cMapsDir = normalizePath(
|
||||
path.join(path.dirname(require.resolve("pdfjs-dist/package.json")), "cmaps"),
|
||||
);
|
||||
const standardFontsDir = normalizePath(
|
||||
path.join(
|
||||
path.dirname(require.resolve("pdfjs-dist/package.json")),
|
||||
"standard_fonts",
|
||||
),
|
||||
);
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(async () => {
|
||||
return {
|
||||
plugins: [
|
||||
wasm(),
|
||||
tanstackRouter({
|
||||
target: "react",
|
||||
routesDirectory: "./routes",
|
||||
generatedRouteTree: "./routeTree.gen.ts",
|
||||
autoCodeSplitting: true,
|
||||
}),
|
||||
svgr(),
|
||||
react(),
|
||||
topLevelAwait(),
|
||||
viteStaticCopy({
|
||||
targets: [
|
||||
{ src: cMapsDir, dest: "" },
|
||||
{ src: standardFontsDir, dest: "" },
|
||||
],
|
||||
}),
|
||||
],
|
||||
build: {
|
||||
sourcemap: true,
|
||||
outDir: "../../dist/apps/yaak-client",
|
||||
emptyOutDir: true,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
// Make chunk names readable
|
||||
chunkFileNames: "assets/chunk-[name]-[hash].js",
|
||||
entryFileNames: "assets/entry-[name]-[hash].js",
|
||||
assetFileNames: "assets/asset-[name]-[hash][extname]",
|
||||
},
|
||||
},
|
||||
},
|
||||
clearScreen: false,
|
||||
server: {
|
||||
port: parseInt(
|
||||
process.env.YAAK_CLIENT_DEV_PORT ?? process.env.YAAK_DEV_PORT ?? "1420",
|
||||
10,
|
||||
),
|
||||
strictPort: true,
|
||||
},
|
||||
envPrefix: ["VITE_", "TAURI_"],
|
||||
};
|
||||
});
|
||||
@@ -1,31 +0,0 @@
|
||||
import { Button, type ButtonProps } from "@yaakapp-internal/ui";
|
||||
import { useCallback, useState } from "react";
|
||||
import type { ActionInvocation } from "@yaakapp-internal/proxy-lib";
|
||||
import { useActionMetadata } from "./hooks";
|
||||
import { rpc } from "./rpc";
|
||||
|
||||
type ActionButtonProps = Omit<ButtonProps, "onClick" | "children"> & {
|
||||
action: ActionInvocation;
|
||||
/** Override the label from metadata */
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
export function ActionButton({ action, children, ...props }: ActionButtonProps) {
|
||||
const meta = useActionMetadata(action);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const onClick = useCallback(async () => {
|
||||
setBusy(true);
|
||||
try {
|
||||
await rpc("execute_action", action);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}, [action]);
|
||||
|
||||
return (
|
||||
<Button {...props} disabled={props.disabled || busy} isLoading={busy} onClick={onClick}>
|
||||
{children ?? meta?.label ?? "…"}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import type {
|
||||
ActionInvocation,
|
||||
ActionMetadata,
|
||||
} from "@yaakapp-internal/proxy-lib";
|
||||
import { rpc } from "./rpc";
|
||||
|
||||
let cachedActions: [ActionInvocation, ActionMetadata][] | null = null;
|
||||
|
||||
/** Fetch and cache all action metadata. */
|
||||
async function getActions(): Promise<[ActionInvocation, ActionMetadata][]> {
|
||||
if (!cachedActions) {
|
||||
const { actions } = await rpc("list_actions", {});
|
||||
cachedActions = actions;
|
||||
}
|
||||
return cachedActions;
|
||||
}
|
||||
|
||||
/** Look up metadata for a specific action invocation. */
|
||||
export function useActionMetadata(
|
||||
action: ActionInvocation,
|
||||
): ActionMetadata | null {
|
||||
const [meta, setMeta] = useState<ActionMetadata | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
getActions().then((actions) => {
|
||||
const match = actions.find(
|
||||
([inv]) => inv.scope === action.scope && inv.action === action.action,
|
||||
);
|
||||
setMeta(match?.[1] ?? null);
|
||||
});
|
||||
}, [action]);
|
||||
|
||||
return meta;
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import type {
|
||||
ActionInvocation,
|
||||
ActionMetadata,
|
||||
} from "@yaakapp-internal/proxy-lib";
|
||||
import { rpc } from "./rpc";
|
||||
|
||||
type ActionBinding = {
|
||||
invocation: ActionInvocation;
|
||||
meta: ActionMetadata;
|
||||
keys: { key: string; ctrl: boolean; shift: boolean; alt: boolean; meta: boolean };
|
||||
};
|
||||
|
||||
/** Parse a hotkey string like "Ctrl+Shift+P" into its parts. */
|
||||
function parseHotkey(hotkey: string): ActionBinding["keys"] {
|
||||
const parts = hotkey.split("+").map((p) => p.trim().toLowerCase());
|
||||
return {
|
||||
ctrl: parts.includes("ctrl") || parts.includes("control"),
|
||||
shift: parts.includes("shift"),
|
||||
alt: parts.includes("alt"),
|
||||
meta: parts.includes("meta") || parts.includes("cmd") || parts.includes("command"),
|
||||
key: parts.filter(
|
||||
(p) => !["ctrl", "control", "shift", "alt", "meta", "cmd", "command"].includes(p),
|
||||
)[0] ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
function matchesEvent(binding: ActionBinding["keys"], e: KeyboardEvent): boolean {
|
||||
return (
|
||||
e.ctrlKey === binding.ctrl &&
|
||||
e.shiftKey === binding.shift &&
|
||||
e.altKey === binding.alt &&
|
||||
e.metaKey === binding.meta &&
|
||||
e.key.toLowerCase() === binding.key
|
||||
);
|
||||
}
|
||||
|
||||
/** Fetch all actions from Rust and register a global keydown listener. */
|
||||
export async function initHotkeys(): Promise<() => void> {
|
||||
const { actions } = await rpc("list_actions", {});
|
||||
|
||||
const bindings: ActionBinding[] = actions
|
||||
.filter(
|
||||
(entry): entry is [ActionInvocation, ActionMetadata & { defaultHotkey: string }] =>
|
||||
entry[1].defaultHotkey != null,
|
||||
)
|
||||
.map(([invocation, meta]) => ({
|
||||
invocation,
|
||||
meta,
|
||||
keys: parseHotkey(meta.defaultHotkey),
|
||||
}));
|
||||
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
for (const binding of bindings) {
|
||||
if (matchesEvent(binding.keys, e)) {
|
||||
e.preventDefault();
|
||||
rpc("execute_action", binding.invocation);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
return () => window.removeEventListener("keydown", onKeyDown);
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Yaak Proxy</title>
|
||||
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html,
|
||||
body {
|
||||
background-color: #1b1a29;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="text-base">
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/theme.ts"></script>
|
||||
<script type="module" src="/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,92 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
@apply w-full h-full overflow-hidden text-text bg-surface;
|
||||
}
|
||||
|
||||
:root {
|
||||
--font-family-interface: "";
|
||||
--font-family-editor: "";
|
||||
}
|
||||
|
||||
:root {
|
||||
font-variant-ligatures: none;
|
||||
}
|
||||
|
||||
html[data-platform="linux"] {
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
::selection {
|
||||
@apply bg-selection;
|
||||
}
|
||||
|
||||
:not(a),
|
||||
:not(input):not(textarea),
|
||||
:not(input):not(textarea)::after,
|
||||
:not(input):not(textarea)::before {
|
||||
@apply select-none cursor-default;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea {
|
||||
&::placeholder {
|
||||
@apply text-placeholder;
|
||||
}
|
||||
}
|
||||
|
||||
a,
|
||||
a[href] * {
|
||||
@apply cursor-pointer !important;
|
||||
}
|
||||
|
||||
table th {
|
||||
@apply text-left;
|
||||
}
|
||||
|
||||
:not(iframe) {
|
||||
&::-webkit-scrollbar,
|
||||
&::-webkit-scrollbar-corner {
|
||||
@apply w-[8px] h-[8px] bg-transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
@apply bg-transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
@apply bg-text-subtlest rounded-[4px] opacity-20;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
@apply opacity-40 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.hide-scrollbars {
|
||||
&::-webkit-scrollbar-corner,
|
||||
&::-webkit-scrollbar {
|
||||
@apply hidden !important;
|
||||
}
|
||||
}
|
||||
|
||||
.rtl {
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
--transition-duration: 100ms ease-in-out;
|
||||
--color-white: 255 100% 100%;
|
||||
--color-black: 255 0% 0%;
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { type } from "@tauri-apps/plugin-os";
|
||||
import { HeaderSize } from "@yaakapp-internal/ui";
|
||||
import { ActionButton } from "./ActionButton";
|
||||
import classNames from "classnames";
|
||||
import { createStore, Provider, useAtomValue } from "jotai";
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "./main.css";
|
||||
import { initHotkeys } from "./hotkeys";
|
||||
import { listen, rpc } from "./rpc";
|
||||
import { applyChange, dataAtom, httpExchangesAtom, replaceAll } from "./store";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
const jotaiStore = createStore();
|
||||
|
||||
// Load initial models from the database
|
||||
rpc("list_models", {}).then((res) => {
|
||||
jotaiStore.set(dataAtom, (prev) =>
|
||||
replaceAll(prev, "http_exchange", res.httpExchanges),
|
||||
);
|
||||
});
|
||||
|
||||
// Register hotkeys from action metadata
|
||||
initHotkeys();
|
||||
|
||||
// Subscribe to model change events from the backend
|
||||
listen("model_write", (payload) => {
|
||||
jotaiStore.set(dataAtom, (prev) =>
|
||||
applyChange(prev, "http_exchange", payload.model, payload.change),
|
||||
);
|
||||
});
|
||||
|
||||
function App() {
|
||||
const osType = type();
|
||||
const exchanges = useAtomValue(httpExchangesAtom);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
"h-full w-full grid grid-rows-[auto_1fr]",
|
||||
osType === "linux" && "border border-border-subtle",
|
||||
)}
|
||||
>
|
||||
<HeaderSize
|
||||
size="lg"
|
||||
osType={osType}
|
||||
hideWindowControls={false}
|
||||
useNativeTitlebar={false}
|
||||
interfaceScale={1}
|
||||
className="x-theme-appHeader bg-surface"
|
||||
>
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="flex items-center px-2 text-sm font-semibold text-text-subtle"
|
||||
>
|
||||
Yaak Proxy
|
||||
</div>
|
||||
</HeaderSize>
|
||||
<main className="overflow-auto p-4">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<ActionButton
|
||||
action={{ scope: "global", action: "proxy_start" }}
|
||||
size="sm"
|
||||
tone="primary"
|
||||
/>
|
||||
<ActionButton
|
||||
action={{ scope: "global", action: "proxy_stop" }}
|
||||
size="sm"
|
||||
variant="border"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="text-xs font-mono">
|
||||
{exchanges.length === 0 ? (
|
||||
<p className="text-text-subtlest">No traffic yet</p>
|
||||
) : (
|
||||
<table className="w-full text-left">
|
||||
<thead>
|
||||
<tr className="text-text-subtlest border-b border-border-subtle">
|
||||
<th className="py-1 pr-3 font-medium">Method</th>
|
||||
<th className="py-1 pr-3 font-medium">URL</th>
|
||||
<th className="py-1 pr-3 font-medium">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{exchanges.map((ex) => (
|
||||
<tr key={ex.id} className="border-b border-border-subtle">
|
||||
<td className="py-1 pr-3">{ex.method}</td>
|
||||
<td className="py-1 pr-3 truncate max-w-md">{ex.url}</td>
|
||||
<td className="py-1 pr-3">{ex.resStatus ?? "—"}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
<StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Provider store={jotaiStore}>
|
||||
<App />
|
||||
</Provider>
|
||||
</QueryClientProvider>
|
||||
</StrictMode>,
|
||||
);
|
||||
@@ -1,31 +0,0 @@
|
||||
{
|
||||
"name": "@yaakapp/yaak-proxy",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev --force",
|
||||
"build": "vite build",
|
||||
"lint": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.90.5",
|
||||
"@tauri-apps/api": "^2.9.1",
|
||||
"@tauri-apps/plugin-os": "^2.3.2",
|
||||
"@yaakapp-internal/theme": "^1.0.0",
|
||||
"@yaakapp-internal/model-store": "^1.0.0",
|
||||
"@yaakapp-internal/proxy-lib": "^1.0.0",
|
||||
"@yaakapp-internal/ui": "^1.0.0",
|
||||
"classnames": "^2.5.1",
|
||||
"jotai": "^2.18.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^7.0.8"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: [
|
||||
require("@tailwindcss/nesting")(require("postcss-nesting")),
|
||||
require("tailwindcss"),
|
||||
require("autoprefixer"),
|
||||
],
|
||||
};
|
||||
@@ -1,30 +0,0 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { listen as tauriListen } from "@tauri-apps/api/event";
|
||||
import type {
|
||||
RpcEventSchema,
|
||||
RpcSchema,
|
||||
} from "@yaakapp-internal/proxy-lib";
|
||||
|
||||
type Req<K extends keyof RpcSchema> = RpcSchema[K][0];
|
||||
type Res<K extends keyof RpcSchema> = RpcSchema[K][1];
|
||||
|
||||
export async function rpc<K extends keyof RpcSchema>(
|
||||
cmd: K,
|
||||
payload: Req<K>,
|
||||
): Promise<Res<K>> {
|
||||
return invoke("rpc", { cmd, payload }) as Promise<Res<K>>;
|
||||
}
|
||||
|
||||
/** Subscribe to a backend event. Returns an unsubscribe function. */
|
||||
export function listen<K extends keyof RpcEventSchema>(
|
||||
event: K & string,
|
||||
callback: (payload: RpcEventSchema[K]) => void,
|
||||
): () => void {
|
||||
let unsub: (() => void) | null = null;
|
||||
tauriListen<RpcEventSchema[K]>(event, (e) => callback(e.payload))
|
||||
.then((fn) => {
|
||||
unsub = fn;
|
||||
})
|
||||
.catch(console.error);
|
||||
return () => unsub?.();
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { createModelStore } from "@yaakapp-internal/model-store";
|
||||
import type { HttpExchange } from "@yaakapp-internal/proxy-lib";
|
||||
|
||||
type ProxyModels = {
|
||||
http_exchange: HttpExchange;
|
||||
};
|
||||
|
||||
export const { dataAtom, applyChange, replaceAll, listAtom, orderedListAtom } =
|
||||
createModelStore<ProxyModels>(["http_exchange"]);
|
||||
|
||||
export const httpExchangesAtom = orderedListAtom(
|
||||
"http_exchange",
|
||||
"createdAt",
|
||||
"desc",
|
||||
);
|
||||
@@ -1,7 +0,0 @@
|
||||
const sharedConfig = require("@yaakapp-internal/tailwind-config");
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
...sharedConfig,
|
||||
content: ["./*.{html,ts,tsx}", "../../packages/ui/src/**/*.{ts,tsx}"],
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
import {
|
||||
applyThemeToDocument,
|
||||
defaultDarkTheme,
|
||||
platformFromUserAgent,
|
||||
setPlatformOnDocument,
|
||||
} from "@yaakapp-internal/theme";
|
||||
|
||||
setPlatformOnDocument(platformFromUserAgent(navigator.userAgent));
|
||||
applyThemeToDocument(defaultDarkTheme);
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"noUncheckedIndexedAccess": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
1
apps/yaak-proxy/vite-env.d.ts
vendored
@@ -1 +0,0 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -1,16 +0,0 @@
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
outDir: "../../dist/apps/yaak-proxy",
|
||||
emptyOutDir: true,
|
||||
},
|
||||
clearScreen: false,
|
||||
server: {
|
||||
port: parseInt(process.env.YAAK_PROXY_DEV_PORT ?? "2420", 10),
|
||||
strictPort: true,
|
||||
},
|
||||
envPrefix: ["VITE_", "TAURI_"],
|
||||
});
|
||||
@@ -42,10 +42,10 @@
|
||||
"!scripts",
|
||||
"!crates",
|
||||
"!crates-tauri",
|
||||
"!apps/yaak-client/tailwind.config.cjs",
|
||||
"!apps/yaak-client/postcss.config.cjs",
|
||||
"!apps/yaak-client/vite.config.ts",
|
||||
"!apps/yaak-client/routeTree.gen.ts",
|
||||
"!src-web/tailwind.config.cjs",
|
||||
"!src-web/postcss.config.cjs",
|
||||
"!src-web/vite.config.ts",
|
||||
"!src-web/routeTree.gen.ts",
|
||||
"!packages/plugin-runtime-types/lib",
|
||||
"!**/bindings",
|
||||
"!flatpak"
|
||||
|
||||
@@ -9,14 +9,12 @@ name = "yaak"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
arboard = "3"
|
||||
base64 = "0.22"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
console = "0.15"
|
||||
dirs = "6"
|
||||
env_logger = "0.11"
|
||||
futures = "0.3"
|
||||
inquire = { version = "0.7", features = ["editor"] }
|
||||
hex = { workspace = true }
|
||||
include_dir = "0.7"
|
||||
keyring = { workspace = true, features = ["apple-native", "windows-native", "sync-secret-service"] }
|
||||
@@ -34,7 +32,6 @@ walkdir = "2"
|
||||
webbrowser = "1"
|
||||
zip = "4"
|
||||
yaak = { workspace = true }
|
||||
yaak-api = { workspace = true }
|
||||
yaak-crypto = { workspace = true }
|
||||
yaak-http = { workspace = true }
|
||||
yaak-models = { workspace = true }
|
||||
|
||||
@@ -21,10 +21,6 @@ pub struct Cli {
|
||||
#[arg(long, short, global = true)]
|
||||
pub environment: Option<String>,
|
||||
|
||||
/// Cookie jar ID to use when sending requests
|
||||
#[arg(long = "cookie-jar", global = true, value_name = "COOKIE_JAR_ID")]
|
||||
pub cookie_jar: Option<String>,
|
||||
|
||||
/// Enable verbose send output (events and streamed response body)
|
||||
#[arg(long, short, global = true)]
|
||||
pub verbose: bool,
|
||||
@@ -51,20 +47,9 @@ pub enum Commands {
|
||||
#[command(hide = true)]
|
||||
Dev(PluginPathArg),
|
||||
|
||||
/// Backward-compatible alias for `plugin generate`
|
||||
#[command(hide = true)]
|
||||
Generate(GenerateArgs),
|
||||
|
||||
/// Backward-compatible alias for `plugin publish`
|
||||
#[command(hide = true)]
|
||||
Publish(PluginPathArg),
|
||||
|
||||
/// Send a request, folder, or workspace by ID
|
||||
Send(SendArgs),
|
||||
|
||||
/// Cookie jar commands
|
||||
CookieJar(CookieJarArgs),
|
||||
|
||||
/// Workspace commands
|
||||
Workspace(WorkspaceArgs),
|
||||
|
||||
@@ -92,22 +77,6 @@ pub struct SendArgs {
|
||||
pub fail_fast: bool,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
#[command(disable_help_subcommand = true)]
|
||||
pub struct CookieJarArgs {
|
||||
#[command(subcommand)]
|
||||
pub command: CookieJarCommands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum CookieJarCommands {
|
||||
/// List cookie jars in a workspace
|
||||
List {
|
||||
/// Workspace ID (optional when exactly one workspace exists)
|
||||
workspace_id: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
#[command(disable_help_subcommand = true)]
|
||||
pub struct WorkspaceArgs {
|
||||
@@ -181,8 +150,8 @@ pub struct RequestArgs {
|
||||
pub enum RequestCommands {
|
||||
/// List requests in a workspace
|
||||
List {
|
||||
/// Workspace ID (optional when exactly one workspace exists)
|
||||
workspace_id: Option<String>,
|
||||
/// Workspace ID
|
||||
workspace_id: String,
|
||||
},
|
||||
|
||||
/// Show a request as JSON
|
||||
@@ -290,8 +259,8 @@ pub struct FolderArgs {
|
||||
pub enum FolderCommands {
|
||||
/// List folders in a workspace
|
||||
List {
|
||||
/// Workspace ID (optional when exactly one workspace exists)
|
||||
workspace_id: Option<String>,
|
||||
/// Workspace ID
|
||||
workspace_id: String,
|
||||
},
|
||||
|
||||
/// Show a folder as JSON
|
||||
@@ -347,8 +316,8 @@ pub struct EnvironmentArgs {
|
||||
pub enum EnvironmentCommands {
|
||||
/// List environments in a workspace
|
||||
List {
|
||||
/// Workspace ID (optional when exactly one workspace exists)
|
||||
workspace_id: Option<String>,
|
||||
/// Workspace ID
|
||||
workspace_id: String,
|
||||
},
|
||||
|
||||
/// Output JSON schema for environment create/update payloads
|
||||
@@ -444,9 +413,6 @@ pub enum PluginCommands {
|
||||
/// Generate a "Hello World" Yaak plugin
|
||||
Generate(GenerateArgs),
|
||||
|
||||
/// Install a plugin from a local directory or from the registry
|
||||
Install(InstallPluginArgs),
|
||||
|
||||
/// Publish a Yaak plugin version to the plugin registry
|
||||
Publish(PluginPathArg),
|
||||
}
|
||||
@@ -467,9 +433,3 @@ pub struct GenerateArgs {
|
||||
#[arg(long)]
|
||||
pub dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Args, Clone)]
|
||||
pub struct InstallPluginArgs {
|
||||
/// Local plugin directory path, or registry plugin spec (@org/plugin[@version])
|
||||
pub source: String,
|
||||
}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
use crate::cli::{CookieJarArgs, CookieJarCommands};
|
||||
use crate::context::CliContext;
|
||||
use crate::utils::workspace::resolve_workspace_id;
|
||||
|
||||
type CommandResult<T = ()> = std::result::Result<T, String>;
|
||||
|
||||
pub fn run(ctx: &CliContext, args: CookieJarArgs) -> i32 {
|
||||
let result = match args.command {
|
||||
CookieJarCommands::List { workspace_id } => list(ctx, workspace_id.as_deref()),
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(()) => 0,
|
||||
Err(error) => {
|
||||
eprintln!("Error: {error}");
|
||||
1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn list(ctx: &CliContext, workspace_id: Option<&str>) -> CommandResult {
|
||||
let workspace_id = resolve_workspace_id(ctx, workspace_id, "cookie-jar list")?;
|
||||
let cookie_jars = ctx
|
||||
.db()
|
||||
.list_cookie_jars(&workspace_id)
|
||||
.map_err(|e| format!("Failed to list cookie jars: {e}"))?;
|
||||
|
||||
if cookie_jars.is_empty() {
|
||||
println!("No cookie jars found in workspace {}", workspace_id);
|
||||
} else {
|
||||
for cookie_jar in cookie_jars {
|
||||
println!(
|
||||
"{} - {} ({} cookies)",
|
||||
cookie_jar.id,
|
||||
cookie_jar.name,
|
||||
cookie_jar.cookies.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -6,7 +6,6 @@ use crate::utils::json::{
|
||||
parse_required_json, require_id, validate_create_id,
|
||||
};
|
||||
use crate::utils::schema::append_agent_hints;
|
||||
use crate::utils::workspace::resolve_workspace_id;
|
||||
use schemars::schema_for;
|
||||
use yaak_models::models::Environment;
|
||||
use yaak_models::util::UpdateSource;
|
||||
@@ -15,7 +14,7 @@ type CommandResult<T = ()> = std::result::Result<T, String>;
|
||||
|
||||
pub fn run(ctx: &CliContext, args: EnvironmentArgs) -> i32 {
|
||||
let result = match args.command {
|
||||
EnvironmentCommands::List { workspace_id } => list(ctx, workspace_id.as_deref()),
|
||||
EnvironmentCommands::List { workspace_id } => list(ctx, &workspace_id),
|
||||
EnvironmentCommands::Schema { pretty } => schema(pretty),
|
||||
EnvironmentCommands::Show { environment_id } => show(ctx, &environment_id),
|
||||
EnvironmentCommands::Create { workspace_id, name, json } => {
|
||||
@@ -46,11 +45,10 @@ fn schema(pretty: bool) -> CommandResult {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn list(ctx: &CliContext, workspace_id: Option<&str>) -> CommandResult {
|
||||
let workspace_id = resolve_workspace_id(ctx, workspace_id, "environment list")?;
|
||||
fn list(ctx: &CliContext, workspace_id: &str) -> CommandResult {
|
||||
let environments = ctx
|
||||
.db()
|
||||
.list_environments_ensure_base(&workspace_id)
|
||||
.list_environments_ensure_base(workspace_id)
|
||||
.map_err(|e| format!("Failed to list environments: {e}"))?;
|
||||
|
||||
if environments.is_empty() {
|
||||
@@ -94,14 +92,8 @@ fn create(
|
||||
validate_create_id(&payload, "environment")?;
|
||||
let mut environment: Environment = serde_json::from_value(payload)
|
||||
.map_err(|e| format!("Failed to parse environment create JSON: {e}"))?;
|
||||
let fallback_workspace_id =
|
||||
if workspace_id_arg.is_none() && environment.workspace_id.is_empty() {
|
||||
Some(resolve_workspace_id(ctx, None, "environment create")?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
merge_workspace_id_arg(
|
||||
workspace_id_arg.as_deref().or(fallback_workspace_id.as_deref()),
|
||||
workspace_id_arg.as_deref(),
|
||||
&mut environment.workspace_id,
|
||||
"environment create",
|
||||
)?;
|
||||
@@ -119,8 +111,9 @@ fn create(
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let workspace_id =
|
||||
resolve_workspace_id(ctx, workspace_id_arg.as_deref(), "environment create")?;
|
||||
let workspace_id = workspace_id_arg.ok_or_else(|| {
|
||||
"environment create requires workspace_id unless JSON payload is provided".to_string()
|
||||
})?;
|
||||
let name = name.ok_or_else(|| {
|
||||
"environment create requires --name unless JSON payload is provided".to_string()
|
||||
})?;
|
||||
|
||||
@@ -5,7 +5,6 @@ use crate::utils::json::{
|
||||
apply_merge_patch, is_json_shorthand, merge_workspace_id_arg, parse_optional_json,
|
||||
parse_required_json, require_id, validate_create_id,
|
||||
};
|
||||
use crate::utils::workspace::resolve_workspace_id;
|
||||
use yaak_models::models::Folder;
|
||||
use yaak_models::util::UpdateSource;
|
||||
|
||||
@@ -13,7 +12,7 @@ type CommandResult<T = ()> = std::result::Result<T, String>;
|
||||
|
||||
pub fn run(ctx: &CliContext, args: FolderArgs) -> i32 {
|
||||
let result = match args.command {
|
||||
FolderCommands::List { workspace_id } => list(ctx, workspace_id.as_deref()),
|
||||
FolderCommands::List { workspace_id } => list(ctx, &workspace_id),
|
||||
FolderCommands::Show { folder_id } => show(ctx, &folder_id),
|
||||
FolderCommands::Create { workspace_id, name, json } => {
|
||||
create(ctx, workspace_id, name, json)
|
||||
@@ -31,10 +30,9 @@ pub fn run(ctx: &CliContext, args: FolderArgs) -> i32 {
|
||||
}
|
||||
}
|
||||
|
||||
fn list(ctx: &CliContext, workspace_id: Option<&str>) -> CommandResult {
|
||||
let workspace_id = resolve_workspace_id(ctx, workspace_id, "folder list")?;
|
||||
fn list(ctx: &CliContext, workspace_id: &str) -> CommandResult {
|
||||
let folders =
|
||||
ctx.db().list_folders(&workspace_id).map_err(|e| format!("Failed to list folders: {e}"))?;
|
||||
ctx.db().list_folders(workspace_id).map_err(|e| format!("Failed to list folders: {e}"))?;
|
||||
if folders.is_empty() {
|
||||
println!("No folders found in workspace {}", workspace_id);
|
||||
} else {
|
||||
@@ -74,14 +72,8 @@ fn create(
|
||||
validate_create_id(&payload, "folder")?;
|
||||
let mut folder: Folder = serde_json::from_value(payload)
|
||||
.map_err(|e| format!("Failed to parse folder create JSON: {e}"))?;
|
||||
let fallback_workspace_id = if workspace_id_arg.is_none() && folder.workspace_id.is_empty()
|
||||
{
|
||||
Some(resolve_workspace_id(ctx, None, "folder create")?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
merge_workspace_id_arg(
|
||||
workspace_id_arg.as_deref().or(fallback_workspace_id.as_deref()),
|
||||
workspace_id_arg.as_deref(),
|
||||
&mut folder.workspace_id,
|
||||
"folder create",
|
||||
)?;
|
||||
@@ -95,7 +87,9 @@ fn create(
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let workspace_id = resolve_workspace_id(ctx, workspace_id_arg.as_deref(), "folder create")?;
|
||||
let workspace_id = workspace_id_arg.ok_or_else(|| {
|
||||
"folder create requires workspace_id unless JSON payload is provided".to_string()
|
||||
})?;
|
||||
let name = name.ok_or_else(|| {
|
||||
"folder create requires --name unless JSON payload is provided".to_string()
|
||||
})?;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
pub mod auth;
|
||||
pub mod cookie_jar;
|
||||
pub mod environment;
|
||||
pub mod folder;
|
||||
pub mod plugin;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
use crate::cli::{GenerateArgs, InstallPluginArgs, PluginPathArg};
|
||||
use crate::context::CliContext;
|
||||
use crate::cli::{GenerateArgs, PluginArgs, PluginCommands, PluginPathArg};
|
||||
use crate::ui;
|
||||
use crate::utils::http;
|
||||
use keyring::Entry;
|
||||
use rand::Rng;
|
||||
use rolldown::{
|
||||
BundleEvent, Bundler, BundlerOptions, ExperimentalOptions, InputItem, LogLevel, OutputFormat,
|
||||
Platform, WatchOption, Watcher, WatcherEvent,
|
||||
Bundler, BundlerOptions, ExperimentalOptions, InputItem, LogLevel, OutputFormat, Platform,
|
||||
WatchOption, Watcher,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashSet;
|
||||
@@ -16,11 +15,6 @@ use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use walkdir::WalkDir;
|
||||
use yaak_api::{ApiClientKind, yaak_api_client};
|
||||
use yaak_models::models::{Plugin, PluginSource};
|
||||
use yaak_models::util::UpdateSource;
|
||||
use yaak_plugins::events::PluginContext;
|
||||
use yaak_plugins::install::download_and_install;
|
||||
use zip::CompressionMethod;
|
||||
use zip::write::SimpleFileOptions;
|
||||
|
||||
@@ -63,13 +57,12 @@ pub async fn run_build(args: PluginPathArg) -> i32 {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run_install(context: &CliContext, args: InstallPluginArgs) -> i32 {
|
||||
match install(context, args).await {
|
||||
Ok(()) => 0,
|
||||
Err(error) => {
|
||||
ui::error(&error);
|
||||
1
|
||||
}
|
||||
pub async fn run(args: PluginArgs) -> i32 {
|
||||
match args.command {
|
||||
PluginCommands::Build(args) => run_build(args).await,
|
||||
PluginCommands::Dev(args) => run_dev(args).await,
|
||||
PluginCommands::Generate(args) => run_generate(args).await,
|
||||
PluginCommands::Publish(args) => run_publish(args).await,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,53 +114,12 @@ async fn dev(args: PluginPathArg) -> CommandResult {
|
||||
ensure_plugin_build_inputs(&plugin_dir)?;
|
||||
|
||||
ui::info(&format!("Watching plugin {}...", plugin_dir.display()));
|
||||
ui::info("Press Ctrl-C to stop");
|
||||
|
||||
let bundler = Bundler::new(bundler_options(&plugin_dir, true))
|
||||
.map_err(|err| format!("Failed to initialize Rolldown watcher: {err}"))?;
|
||||
let watcher = Watcher::new(vec![Arc::new(Mutex::new(bundler))], None)
|
||||
.map_err(|err| format!("Failed to start Rolldown watcher: {err}"))?;
|
||||
let emitter = watcher.emitter();
|
||||
let watch_root = plugin_dir.clone();
|
||||
let _event_logger = tokio::spawn(async move {
|
||||
loop {
|
||||
let event = {
|
||||
let rx = emitter.rx.lock().await;
|
||||
rx.recv()
|
||||
};
|
||||
|
||||
let Ok(event) = event else {
|
||||
break;
|
||||
};
|
||||
|
||||
match event {
|
||||
WatcherEvent::Change(change) => {
|
||||
let changed_path = Path::new(change.path.as_str());
|
||||
let display_path = changed_path
|
||||
.strip_prefix(&watch_root)
|
||||
.map(|p| p.display().to_string())
|
||||
.unwrap_or_else(|_| {
|
||||
changed_path
|
||||
.file_name()
|
||||
.map(|name| name.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| "unknown".to_string())
|
||||
});
|
||||
ui::info(&format!("Rebuilding plugin {display_path}"));
|
||||
}
|
||||
WatcherEvent::Event(BundleEvent::BundleEnd(_)) => {}
|
||||
WatcherEvent::Event(BundleEvent::Error(event)) => {
|
||||
if event.error.diagnostics.is_empty() {
|
||||
ui::error("Plugin build failed");
|
||||
} else {
|
||||
for diagnostic in event.error.diagnostics {
|
||||
ui::error(&diagnostic.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
WatcherEvent::Close => break,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
watcher.start().await;
|
||||
Ok(())
|
||||
@@ -257,113 +209,6 @@ async fn publish(args: PluginPathArg) -> CommandResult {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn install(context: &CliContext, args: InstallPluginArgs) -> CommandResult {
|
||||
if args.source.starts_with('@') {
|
||||
let (name, version) =
|
||||
parse_registry_install_spec(args.source.as_str()).ok_or_else(|| {
|
||||
"Invalid registry plugin spec. Expected format: @org/plugin or @org/plugin@version"
|
||||
.to_string()
|
||||
})?;
|
||||
return install_from_registry(context, name, version).await;
|
||||
}
|
||||
|
||||
install_from_directory(context, args.source.as_str()).await
|
||||
}
|
||||
|
||||
async fn install_from_registry(
|
||||
context: &CliContext,
|
||||
name: String,
|
||||
version: Option<String>,
|
||||
) -> CommandResult {
|
||||
let current_version = crate::version::cli_version();
|
||||
let http_client = yaak_api_client(ApiClientKind::Cli, current_version)
|
||||
.map_err(|err| format!("Failed to initialize API client: {err}"))?;
|
||||
let installing_version = version.clone().unwrap_or_else(|| "latest".to_string());
|
||||
ui::info(&format!("Installing registry plugin {name}@{installing_version}"));
|
||||
|
||||
let plugin_context = PluginContext::new(Some("cli".to_string()), None);
|
||||
let installed = download_and_install(
|
||||
context.plugin_manager(),
|
||||
context.query_manager(),
|
||||
&http_client,
|
||||
&plugin_context,
|
||||
name.as_str(),
|
||||
version,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| format!("Failed to install plugin: {err}"))?;
|
||||
|
||||
ui::success(&format!("Installed plugin {}@{}", installed.name, installed.version));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn install_from_directory(context: &CliContext, source: &str) -> CommandResult {
|
||||
let plugin_dir = resolve_plugin_dir(Some(PathBuf::from(source)))?;
|
||||
let plugin_dir_str = plugin_dir
|
||||
.to_str()
|
||||
.ok_or_else(|| {
|
||||
format!("Plugin directory path is not valid UTF-8: {}", plugin_dir.display())
|
||||
})?
|
||||
.to_string();
|
||||
ui::info(&format!("Installing plugin from directory {}", plugin_dir.display()));
|
||||
|
||||
let plugin = context
|
||||
.db()
|
||||
.upsert_plugin(
|
||||
&Plugin {
|
||||
directory: plugin_dir_str,
|
||||
url: None,
|
||||
enabled: true,
|
||||
source: PluginSource::Filesystem,
|
||||
..Default::default()
|
||||
},
|
||||
&UpdateSource::Background,
|
||||
)
|
||||
.map_err(|err| format!("Failed to save plugin in database: {err}"))?;
|
||||
|
||||
let plugin_context = PluginContext::new(Some("cli".to_string()), None);
|
||||
context
|
||||
.plugin_manager()
|
||||
.add_plugin(&plugin_context, &plugin)
|
||||
.await
|
||||
.map_err(|err| format!("Failed to load plugin runtime: {err}"))?;
|
||||
|
||||
ui::success(&format!("Installed plugin from {}", plugin.directory));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_registry_install_spec(source: &str) -> Option<(String, Option<String>)> {
|
||||
if !source.starts_with('@') || !source.contains('/') {
|
||||
return None;
|
||||
}
|
||||
|
||||
let rest = source.get(1..)?;
|
||||
let version_split = rest.rfind('@').map(|idx| idx + 1);
|
||||
let (name, version) = match version_split {
|
||||
Some(at_idx) => {
|
||||
let (name, version) = source.split_at(at_idx);
|
||||
let version = version.strip_prefix('@').unwrap_or_default();
|
||||
if version.is_empty() {
|
||||
return None;
|
||||
}
|
||||
(name.to_string(), Some(version.to_string()))
|
||||
}
|
||||
None => (source.to_string(), None),
|
||||
};
|
||||
|
||||
if !name.starts_with('@') {
|
||||
return None;
|
||||
}
|
||||
|
||||
let without_scope = name.get(1..)?;
|
||||
let (scope, plugin_name) = without_scope.split_once('/')?;
|
||||
if scope.is_empty() || plugin_name.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some((name, version))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct PublishResponse {
|
||||
version: String,
|
||||
|
||||
@@ -6,7 +6,6 @@ use crate::utils::json::{
|
||||
parse_required_json, require_id, validate_create_id,
|
||||
};
|
||||
use crate::utils::schema::append_agent_hints;
|
||||
use crate::utils::workspace::resolve_workspace_id;
|
||||
use schemars::schema_for;
|
||||
use serde_json::{Map, Value, json};
|
||||
use std::collections::HashMap;
|
||||
@@ -25,16 +24,13 @@ pub async fn run(
|
||||
ctx: &CliContext,
|
||||
args: RequestArgs,
|
||||
environment: Option<&str>,
|
||||
cookie_jar_id: Option<&str>,
|
||||
verbose: bool,
|
||||
) -> i32 {
|
||||
let result = match args.command {
|
||||
RequestCommands::List { workspace_id } => list(ctx, workspace_id.as_deref()),
|
||||
RequestCommands::List { workspace_id } => list(ctx, &workspace_id),
|
||||
RequestCommands::Show { request_id } => show(ctx, &request_id),
|
||||
RequestCommands::Send { request_id } => {
|
||||
return match send_request_by_id(ctx, &request_id, environment, cookie_jar_id, verbose)
|
||||
.await
|
||||
{
|
||||
return match send_request_by_id(ctx, &request_id, environment, verbose).await {
|
||||
Ok(()) => 0,
|
||||
Err(error) => {
|
||||
eprintln!("Error: {error}");
|
||||
@@ -67,11 +63,10 @@ pub async fn run(
|
||||
}
|
||||
}
|
||||
|
||||
fn list(ctx: &CliContext, workspace_id: Option<&str>) -> CommandResult {
|
||||
let workspace_id = resolve_workspace_id(ctx, workspace_id, "request list")?;
|
||||
fn list(ctx: &CliContext, workspace_id: &str) -> CommandResult {
|
||||
let requests = ctx
|
||||
.db()
|
||||
.list_http_requests(&workspace_id)
|
||||
.list_http_requests(workspace_id)
|
||||
.map_err(|e| format!("Failed to list requests: {e}"))?;
|
||||
if requests.is_empty() {
|
||||
println!("No requests found in workspace {}", workspace_id);
|
||||
@@ -355,14 +350,8 @@ fn create(
|
||||
validate_create_id(&payload, "request")?;
|
||||
let mut request: HttpRequest = serde_json::from_value(payload)
|
||||
.map_err(|e| format!("Failed to parse request create JSON: {e}"))?;
|
||||
let fallback_workspace_id = if workspace_id_arg.is_none() && request.workspace_id.is_empty()
|
||||
{
|
||||
Some(resolve_workspace_id(ctx, None, "request create")?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
merge_workspace_id_arg(
|
||||
workspace_id_arg.as_deref().or(fallback_workspace_id.as_deref()),
|
||||
workspace_id_arg.as_deref(),
|
||||
&mut request.workspace_id,
|
||||
"request create",
|
||||
)?;
|
||||
@@ -376,7 +365,9 @@ fn create(
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let workspace_id = resolve_workspace_id(ctx, workspace_id_arg.as_deref(), "request create")?;
|
||||
let workspace_id = workspace_id_arg.ok_or_else(|| {
|
||||
"request create requires workspace_id unless JSON payload is provided".to_string()
|
||||
})?;
|
||||
let name = name.unwrap_or_default();
|
||||
let url = url.unwrap_or_default();
|
||||
let method = method.unwrap_or_else(|| "GET".to_string());
|
||||
@@ -445,7 +436,6 @@ pub async fn send_request_by_id(
|
||||
ctx: &CliContext,
|
||||
request_id: &str,
|
||||
environment: Option<&str>,
|
||||
cookie_jar_id: Option<&str>,
|
||||
verbose: bool,
|
||||
) -> Result<(), String> {
|
||||
let request =
|
||||
@@ -457,7 +447,6 @@ pub async fn send_request_by_id(
|
||||
&http_request.id,
|
||||
&http_request.workspace_id,
|
||||
environment,
|
||||
cookie_jar_id,
|
||||
verbose,
|
||||
)
|
||||
.await
|
||||
@@ -476,13 +465,9 @@ async fn send_http_request_by_id(
|
||||
request_id: &str,
|
||||
workspace_id: &str,
|
||||
environment: Option<&str>,
|
||||
cookie_jar_id: Option<&str>,
|
||||
verbose: bool,
|
||||
) -> Result<(), String> {
|
||||
let cookie_jar_id = resolve_cookie_jar_id(ctx, workspace_id, cookie_jar_id)?;
|
||||
|
||||
let plugin_context =
|
||||
PluginContext::new(Some("cli".to_string()), Some(workspace_id.to_string()));
|
||||
let plugin_context = PluginContext::new(None, Some(workspace_id.to_string()));
|
||||
|
||||
let (event_tx, mut event_rx) = mpsc::channel::<SenderHttpResponseEvent>(100);
|
||||
let (body_chunk_tx, mut body_chunk_rx) = mpsc::unbounded_channel::<Vec<u8>>();
|
||||
@@ -510,7 +495,7 @@ async fn send_http_request_by_id(
|
||||
request_id,
|
||||
environment_id: environment,
|
||||
update_source: UpdateSource::Sync,
|
||||
cookie_jar_id,
|
||||
cookie_jar_id: None,
|
||||
response_dir: &response_dir,
|
||||
emit_events_to: Some(event_tx),
|
||||
emit_response_body_chunks_to: Some(body_chunk_tx),
|
||||
@@ -527,22 +512,3 @@ async fn send_http_request_by_id(
|
||||
result.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_cookie_jar_id(
|
||||
ctx: &CliContext,
|
||||
workspace_id: &str,
|
||||
explicit_cookie_jar_id: Option<&str>,
|
||||
) -> Result<Option<String>, String> {
|
||||
if let Some(cookie_jar_id) = explicit_cookie_jar_id {
|
||||
return Ok(Some(cookie_jar_id.to_string()));
|
||||
}
|
||||
|
||||
let default_cookie_jar = ctx
|
||||
.db()
|
||||
.list_cookie_jars(workspace_id)
|
||||
.map_err(|e| format!("Failed to list cookie jars: {e}"))?
|
||||
.into_iter()
|
||||
.min_by_key(|jar| jar.created_at)
|
||||
.map(|jar| jar.id);
|
||||
Ok(default_cookie_jar)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ use crate::cli::SendArgs;
|
||||
use crate::commands::request;
|
||||
use crate::context::CliContext;
|
||||
use futures::future::join_all;
|
||||
use yaak_models::queries::any_request::AnyRequest;
|
||||
|
||||
enum ExecutionMode {
|
||||
Sequential,
|
||||
@@ -13,10 +12,9 @@ pub async fn run(
|
||||
ctx: &CliContext,
|
||||
args: SendArgs,
|
||||
environment: Option<&str>,
|
||||
cookie_jar_id: Option<&str>,
|
||||
verbose: bool,
|
||||
) -> i32 {
|
||||
match send_target(ctx, args, environment, cookie_jar_id, verbose).await {
|
||||
match send_target(ctx, args, environment, verbose).await {
|
||||
Ok(()) => 0,
|
||||
Err(error) => {
|
||||
eprintln!("Error: {error}");
|
||||
@@ -29,70 +27,30 @@ async fn send_target(
|
||||
ctx: &CliContext,
|
||||
args: SendArgs,
|
||||
environment: Option<&str>,
|
||||
cookie_jar_id: Option<&str>,
|
||||
verbose: bool,
|
||||
) -> Result<(), String> {
|
||||
let mode = if args.parallel { ExecutionMode::Parallel } else { ExecutionMode::Sequential };
|
||||
|
||||
if let Ok(request) = ctx.db().get_any_request(&args.id) {
|
||||
let workspace_id = match &request {
|
||||
AnyRequest::HttpRequest(r) => r.workspace_id.clone(),
|
||||
AnyRequest::GrpcRequest(r) => r.workspace_id.clone(),
|
||||
AnyRequest::WebsocketRequest(r) => r.workspace_id.clone(),
|
||||
};
|
||||
let resolved_cookie_jar_id =
|
||||
request::resolve_cookie_jar_id(ctx, &workspace_id, cookie_jar_id)?;
|
||||
|
||||
return request::send_request_by_id(
|
||||
ctx,
|
||||
&args.id,
|
||||
environment,
|
||||
resolved_cookie_jar_id.as_deref(),
|
||||
verbose,
|
||||
)
|
||||
.await;
|
||||
if ctx.db().get_any_request(&args.id).is_ok() {
|
||||
return request::send_request_by_id(ctx, &args.id, environment, verbose).await;
|
||||
}
|
||||
|
||||
if let Ok(folder) = ctx.db().get_folder(&args.id) {
|
||||
let resolved_cookie_jar_id =
|
||||
request::resolve_cookie_jar_id(ctx, &folder.workspace_id, cookie_jar_id)?;
|
||||
|
||||
if ctx.db().get_folder(&args.id).is_ok() {
|
||||
let request_ids = collect_folder_request_ids(ctx, &args.id)?;
|
||||
if request_ids.is_empty() {
|
||||
println!("No requests found in folder {}", args.id);
|
||||
return Ok(());
|
||||
}
|
||||
return send_many(
|
||||
ctx,
|
||||
request_ids,
|
||||
mode,
|
||||
args.fail_fast,
|
||||
environment,
|
||||
resolved_cookie_jar_id.as_deref(),
|
||||
verbose,
|
||||
)
|
||||
.await;
|
||||
return send_many(ctx, request_ids, mode, args.fail_fast, environment, verbose).await;
|
||||
}
|
||||
|
||||
if let Ok(workspace) = ctx.db().get_workspace(&args.id) {
|
||||
let resolved_cookie_jar_id =
|
||||
request::resolve_cookie_jar_id(ctx, &workspace.id, cookie_jar_id)?;
|
||||
|
||||
if ctx.db().get_workspace(&args.id).is_ok() {
|
||||
let request_ids = collect_workspace_request_ids(ctx, &args.id)?;
|
||||
if request_ids.is_empty() {
|
||||
println!("No requests found in workspace {}", args.id);
|
||||
return Ok(());
|
||||
}
|
||||
return send_many(
|
||||
ctx,
|
||||
request_ids,
|
||||
mode,
|
||||
args.fail_fast,
|
||||
environment,
|
||||
resolved_cookie_jar_id.as_deref(),
|
||||
verbose,
|
||||
)
|
||||
.await;
|
||||
return send_many(ctx, request_ids, mode, args.fail_fast, environment, verbose).await;
|
||||
}
|
||||
|
||||
Err(format!("Could not resolve ID '{}' as request, folder, or workspace", args.id))
|
||||
@@ -173,7 +131,6 @@ async fn send_many(
|
||||
mode: ExecutionMode,
|
||||
fail_fast: bool,
|
||||
environment: Option<&str>,
|
||||
cookie_jar_id: Option<&str>,
|
||||
verbose: bool,
|
||||
) -> Result<(), String> {
|
||||
let mut success_count = 0usize;
|
||||
@@ -182,15 +139,7 @@ async fn send_many(
|
||||
match mode {
|
||||
ExecutionMode::Sequential => {
|
||||
for request_id in request_ids {
|
||||
match request::send_request_by_id(
|
||||
ctx,
|
||||
&request_id,
|
||||
environment,
|
||||
cookie_jar_id,
|
||||
verbose,
|
||||
)
|
||||
.await
|
||||
{
|
||||
match request::send_request_by_id(ctx, &request_id, environment, verbose).await {
|
||||
Ok(()) => success_count += 1,
|
||||
Err(error) => {
|
||||
failures.push((request_id, error));
|
||||
@@ -207,14 +156,7 @@ async fn send_many(
|
||||
.map(|request_id| async move {
|
||||
(
|
||||
request_id.clone(),
|
||||
request::send_request_by_id(
|
||||
ctx,
|
||||
request_id,
|
||||
environment,
|
||||
cookie_jar_id,
|
||||
verbose,
|
||||
)
|
||||
.await,
|
||||
request::send_request_by_id(ctx, request_id, environment, verbose).await,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -31,13 +31,18 @@ pub fn run(ctx: &CliContext, args: WorkspaceArgs) -> i32 {
|
||||
}
|
||||
|
||||
fn schema(pretty: bool) -> CommandResult {
|
||||
let mut schema = serde_json::to_value(schema_for!(Workspace))
|
||||
.map_err(|e| format!("Failed to serialize workspace schema: {e}"))?;
|
||||
let mut schema =
|
||||
serde_json::to_value(schema_for!(Workspace)).map_err(|e| format!(
|
||||
"Failed to serialize workspace schema: {e}"
|
||||
))?;
|
||||
append_agent_hints(&mut schema);
|
||||
|
||||
let output =
|
||||
if pretty { serde_json::to_string_pretty(&schema) } else { serde_json::to_string(&schema) }
|
||||
.map_err(|e| format!("Failed to format workspace schema JSON: {e}"))?;
|
||||
let output = if pretty {
|
||||
serde_json::to_string_pretty(&schema)
|
||||
} else {
|
||||
serde_json::to_string(&schema)
|
||||
}
|
||||
.map_err(|e| format!("Failed to format workspace schema JSON: {e}"))?;
|
||||
println!("{output}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -6,25 +6,17 @@ use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use yaak_crypto::manager::EncryptionManager;
|
||||
use yaak_models::blob_manager::BlobManager;
|
||||
use yaak_models::client_db::ClientDb;
|
||||
use yaak_models::db_context::DbContext;
|
||||
use yaak_models::query_manager::QueryManager;
|
||||
use yaak_plugins::events::PluginContext;
|
||||
use yaak_plugins::manager::PluginManager;
|
||||
|
||||
const EMBEDDED_PLUGIN_RUNTIME: &str = include_str!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../../crates-tauri/yaak-app-client/vendored/plugin-runtime/index.cjs"
|
||||
"/../../crates-tauri/yaak-app/vendored/plugin-runtime/index.cjs"
|
||||
));
|
||||
static EMBEDDED_VENDORED_PLUGINS: Dir<'_> =
|
||||
include_dir!("$CARGO_MANIFEST_DIR/../../crates-tauri/yaak-app-client/vendored/plugins");
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct CliExecutionContext {
|
||||
pub request_id: Option<String>,
|
||||
pub workspace_id: Option<String>,
|
||||
pub environment_id: Option<String>,
|
||||
pub cookie_jar_id: Option<String>,
|
||||
}
|
||||
include_dir!("$CARGO_MANIFEST_DIR/../../crates-tauri/yaak-app/vendored/plugins");
|
||||
|
||||
pub struct CliContext {
|
||||
data_dir: PathBuf,
|
||||
@@ -36,71 +28,63 @@ pub struct CliContext {
|
||||
}
|
||||
|
||||
impl CliContext {
|
||||
pub fn new(data_dir: PathBuf, app_id: &str) -> Self {
|
||||
pub async fn initialize(data_dir: PathBuf, app_id: &str, with_plugins: bool) -> Self {
|
||||
let db_path = data_dir.join("db.sqlite");
|
||||
let blob_path = data_dir.join("blobs.sqlite");
|
||||
let (query_manager, blob_manager, _rx) =
|
||||
match yaak_models::init_standalone(&db_path, &blob_path) {
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
eprintln!("Error: Failed to initialize database: {err}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let (query_manager, blob_manager, _rx) = yaak_models::init_standalone(&db_path, &blob_path)
|
||||
.expect("Failed to initialize database");
|
||||
|
||||
let encryption_manager = Arc::new(EncryptionManager::new(query_manager.clone(), app_id));
|
||||
|
||||
let plugin_manager = if with_plugins {
|
||||
let vendored_plugin_dir = data_dir.join("vendored-plugins");
|
||||
let installed_plugin_dir = data_dir.join("installed-plugins");
|
||||
let node_bin_path = PathBuf::from("node");
|
||||
|
||||
prepare_embedded_vendored_plugins(&vendored_plugin_dir)
|
||||
.expect("Failed to prepare bundled plugins");
|
||||
|
||||
let plugin_runtime_main =
|
||||
std::env::var("YAAK_PLUGIN_RUNTIME").map(PathBuf::from).unwrap_or_else(|_| {
|
||||
prepare_embedded_plugin_runtime(&data_dir)
|
||||
.expect("Failed to prepare embedded plugin runtime")
|
||||
});
|
||||
|
||||
match PluginManager::new(
|
||||
vendored_plugin_dir,
|
||||
installed_plugin_dir,
|
||||
node_bin_path,
|
||||
plugin_runtime_main,
|
||||
&query_manager,
|
||||
&PluginContext::new_empty(),
|
||||
false,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(plugin_manager) => Some(Arc::new(plugin_manager)),
|
||||
Err(err) => {
|
||||
eprintln!("Warning: Failed to initialize plugins: {err}");
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let plugin_event_bridge = if let Some(plugin_manager) = &plugin_manager {
|
||||
Some(CliPluginEventBridge::start(plugin_manager.clone(), query_manager.clone()).await)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Self {
|
||||
data_dir,
|
||||
query_manager,
|
||||
blob_manager,
|
||||
encryption_manager,
|
||||
plugin_manager: None,
|
||||
plugin_event_bridge: Mutex::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn init_plugins(&mut self, execution_context: CliExecutionContext) {
|
||||
let vendored_plugin_dir = self.data_dir.join("vendored-plugins");
|
||||
let installed_plugin_dir = self.data_dir.join("installed-plugins");
|
||||
let node_bin_path = PathBuf::from("node");
|
||||
|
||||
prepare_embedded_vendored_plugins(&vendored_plugin_dir)
|
||||
.expect("Failed to prepare bundled plugins");
|
||||
|
||||
let plugin_runtime_main =
|
||||
std::env::var("YAAK_PLUGIN_RUNTIME").map(PathBuf::from).unwrap_or_else(|_| {
|
||||
prepare_embedded_plugin_runtime(&self.data_dir)
|
||||
.expect("Failed to prepare embedded plugin runtime")
|
||||
});
|
||||
|
||||
match PluginManager::new(
|
||||
vendored_plugin_dir,
|
||||
installed_plugin_dir,
|
||||
node_bin_path,
|
||||
plugin_runtime_main,
|
||||
&self.query_manager,
|
||||
&PluginContext::new_empty(),
|
||||
false,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(plugin_manager) => {
|
||||
let plugin_manager = Arc::new(plugin_manager);
|
||||
let plugin_event_bridge = CliPluginEventBridge::start(
|
||||
plugin_manager.clone(),
|
||||
self.query_manager.clone(),
|
||||
self.blob_manager.clone(),
|
||||
self.encryption_manager.clone(),
|
||||
self.data_dir.clone(),
|
||||
execution_context,
|
||||
)
|
||||
.await;
|
||||
self.plugin_manager = Some(plugin_manager);
|
||||
*self.plugin_event_bridge.lock().await = Some(plugin_event_bridge);
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("Warning: Failed to initialize plugins: {err}");
|
||||
}
|
||||
plugin_manager,
|
||||
plugin_event_bridge: Mutex::new(plugin_event_bridge),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +92,7 @@ impl CliContext {
|
||||
&self.data_dir
|
||||
}
|
||||
|
||||
pub fn db(&self) -> ClientDb<'_> {
|
||||
pub fn db(&self) -> DbContext<'_> {
|
||||
self.query_manager.connect()
|
||||
}
|
||||
|
||||
|
||||
@@ -5,16 +5,14 @@ mod plugin_events;
|
||||
mod ui;
|
||||
mod utils;
|
||||
mod version;
|
||||
mod version_check;
|
||||
|
||||
use clap::Parser;
|
||||
use cli::{Cli, Commands, PluginCommands, RequestCommands};
|
||||
use context::{CliContext, CliExecutionContext};
|
||||
use yaak_models::queries::any_request::AnyRequest;
|
||||
use cli::{Cli, Commands, RequestCommands};
|
||||
use context::CliContext;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let Cli { data_dir, environment, cookie_jar, verbose, log, command } = Cli::parse();
|
||||
let Cli { data_dir, environment, verbose, log, command } = Cli::parse();
|
||||
|
||||
if let Some(log_level) = log {
|
||||
match log_level {
|
||||
@@ -34,208 +32,70 @@ async fn main() {
|
||||
dirs::data_dir().expect("Could not determine data directory").join(app_id)
|
||||
});
|
||||
|
||||
version_check::maybe_check_for_updates().await;
|
||||
let needs_context = matches!(
|
||||
&command,
|
||||
Commands::Send(_)
|
||||
| Commands::Workspace(_)
|
||||
| Commands::Request(_)
|
||||
| Commands::Folder(_)
|
||||
| Commands::Environment(_)
|
||||
);
|
||||
|
||||
let needs_plugins = matches!(
|
||||
&command,
|
||||
Commands::Send(_)
|
||||
| Commands::Request(cli::RequestArgs {
|
||||
command: RequestCommands::Send { .. } | RequestCommands::Schema { .. },
|
||||
})
|
||||
);
|
||||
|
||||
let context = if needs_context {
|
||||
Some(CliContext::initialize(data_dir, app_id, needs_plugins).await)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let exit_code = match command {
|
||||
Commands::Auth(args) => commands::auth::run(args).await,
|
||||
Commands::Plugin(args) => match args.command {
|
||||
PluginCommands::Build(args) => commands::plugin::run_build(args).await,
|
||||
PluginCommands::Dev(args) => commands::plugin::run_dev(args).await,
|
||||
PluginCommands::Generate(args) => commands::plugin::run_generate(args).await,
|
||||
PluginCommands::Publish(args) => commands::plugin::run_publish(args).await,
|
||||
PluginCommands::Install(install_args) => {
|
||||
let mut context = CliContext::new(data_dir.clone(), app_id);
|
||||
context.init_plugins(CliExecutionContext::default()).await;
|
||||
let exit_code = commands::plugin::run_install(&context, install_args).await;
|
||||
context.shutdown().await;
|
||||
exit_code
|
||||
}
|
||||
},
|
||||
Commands::Plugin(args) => commands::plugin::run(args).await,
|
||||
Commands::Build(args) => commands::plugin::run_build(args).await,
|
||||
Commands::Dev(args) => commands::plugin::run_dev(args).await,
|
||||
Commands::Generate(args) => commands::plugin::run_generate(args).await,
|
||||
Commands::Publish(args) => commands::plugin::run_publish(args).await,
|
||||
Commands::Send(args) => {
|
||||
let mut context = CliContext::new(data_dir.clone(), app_id);
|
||||
match resolve_send_execution_context(
|
||||
&context,
|
||||
&args.id,
|
||||
commands::send::run(
|
||||
context.as_ref().expect("context initialized for send"),
|
||||
args,
|
||||
environment.as_deref(),
|
||||
cookie_jar.as_deref(),
|
||||
) {
|
||||
Ok(execution_context) => {
|
||||
context.init_plugins(execution_context).await;
|
||||
let exit_code = commands::send::run(
|
||||
&context,
|
||||
args,
|
||||
environment.as_deref(),
|
||||
cookie_jar.as_deref(),
|
||||
verbose,
|
||||
)
|
||||
.await;
|
||||
context.shutdown().await;
|
||||
exit_code
|
||||
}
|
||||
Err(error) => {
|
||||
eprintln!("Error: {error}");
|
||||
1
|
||||
}
|
||||
}
|
||||
}
|
||||
Commands::CookieJar(args) => {
|
||||
let context = CliContext::new(data_dir.clone(), app_id);
|
||||
let exit_code = commands::cookie_jar::run(&context, args);
|
||||
context.shutdown().await;
|
||||
exit_code
|
||||
}
|
||||
Commands::Workspace(args) => {
|
||||
let context = CliContext::new(data_dir.clone(), app_id);
|
||||
let exit_code = commands::workspace::run(&context, args);
|
||||
context.shutdown().await;
|
||||
exit_code
|
||||
verbose,
|
||||
)
|
||||
.await
|
||||
}
|
||||
Commands::Workspace(args) => commands::workspace::run(
|
||||
context.as_ref().expect("context initialized for workspace"),
|
||||
args,
|
||||
),
|
||||
Commands::Request(args) => {
|
||||
let mut context = CliContext::new(data_dir.clone(), app_id);
|
||||
let execution_context_result = match &args.command {
|
||||
RequestCommands::Send { request_id } => resolve_request_execution_context(
|
||||
&context,
|
||||
request_id,
|
||||
environment.as_deref(),
|
||||
cookie_jar.as_deref(),
|
||||
),
|
||||
_ => Ok(CliExecutionContext::default()),
|
||||
};
|
||||
match execution_context_result {
|
||||
Ok(execution_context) => {
|
||||
let with_plugins = matches!(
|
||||
&args.command,
|
||||
RequestCommands::Send { .. } | RequestCommands::Schema { .. }
|
||||
);
|
||||
if with_plugins {
|
||||
context.init_plugins(execution_context).await;
|
||||
}
|
||||
let exit_code = commands::request::run(
|
||||
&context,
|
||||
args,
|
||||
environment.as_deref(),
|
||||
cookie_jar.as_deref(),
|
||||
verbose,
|
||||
)
|
||||
.await;
|
||||
context.shutdown().await;
|
||||
exit_code
|
||||
}
|
||||
Err(error) => {
|
||||
eprintln!("Error: {error}");
|
||||
1
|
||||
}
|
||||
}
|
||||
commands::request::run(
|
||||
context.as_ref().expect("context initialized for request"),
|
||||
args,
|
||||
environment.as_deref(),
|
||||
verbose,
|
||||
)
|
||||
.await
|
||||
}
|
||||
Commands::Folder(args) => {
|
||||
let context = CliContext::new(data_dir.clone(), app_id);
|
||||
let exit_code = commands::folder::run(&context, args);
|
||||
context.shutdown().await;
|
||||
exit_code
|
||||
}
|
||||
Commands::Environment(args) => {
|
||||
let context = CliContext::new(data_dir.clone(), app_id);
|
||||
let exit_code = commands::environment::run(&context, args);
|
||||
context.shutdown().await;
|
||||
exit_code
|
||||
commands::folder::run(context.as_ref().expect("context initialized for folder"), args)
|
||||
}
|
||||
Commands::Environment(args) => commands::environment::run(
|
||||
context.as_ref().expect("context initialized for environment"),
|
||||
args,
|
||||
),
|
||||
};
|
||||
|
||||
if let Some(context) = &context {
|
||||
context.shutdown().await;
|
||||
}
|
||||
|
||||
if exit_code != 0 {
|
||||
std::process::exit(exit_code);
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_send_execution_context(
|
||||
context: &CliContext,
|
||||
id: &str,
|
||||
environment: Option<&str>,
|
||||
explicit_cookie_jar_id: Option<&str>,
|
||||
) -> Result<CliExecutionContext, String> {
|
||||
if let Ok(request) = context.db().get_any_request(id) {
|
||||
let (request_id, workspace_id) = match request {
|
||||
AnyRequest::HttpRequest(r) => (Some(r.id), r.workspace_id),
|
||||
AnyRequest::GrpcRequest(r) => (Some(r.id), r.workspace_id),
|
||||
AnyRequest::WebsocketRequest(r) => (Some(r.id), r.workspace_id),
|
||||
};
|
||||
let cookie_jar_id = resolve_cookie_jar_id(context, &workspace_id, explicit_cookie_jar_id)?;
|
||||
return Ok(CliExecutionContext {
|
||||
request_id,
|
||||
workspace_id: Some(workspace_id),
|
||||
environment_id: environment.map(str::to_string),
|
||||
cookie_jar_id,
|
||||
});
|
||||
}
|
||||
|
||||
if let Ok(folder) = context.db().get_folder(id) {
|
||||
let cookie_jar_id =
|
||||
resolve_cookie_jar_id(context, &folder.workspace_id, explicit_cookie_jar_id)?;
|
||||
return Ok(CliExecutionContext {
|
||||
request_id: None,
|
||||
workspace_id: Some(folder.workspace_id),
|
||||
environment_id: environment.map(str::to_string),
|
||||
cookie_jar_id,
|
||||
});
|
||||
}
|
||||
|
||||
if let Ok(workspace) = context.db().get_workspace(id) {
|
||||
let cookie_jar_id = resolve_cookie_jar_id(context, &workspace.id, explicit_cookie_jar_id)?;
|
||||
return Ok(CliExecutionContext {
|
||||
request_id: None,
|
||||
workspace_id: Some(workspace.id),
|
||||
environment_id: environment.map(str::to_string),
|
||||
cookie_jar_id,
|
||||
});
|
||||
}
|
||||
|
||||
Err(format!("Could not resolve ID '{}' as request, folder, or workspace", id))
|
||||
}
|
||||
|
||||
fn resolve_request_execution_context(
|
||||
context: &CliContext,
|
||||
request_id: &str,
|
||||
environment: Option<&str>,
|
||||
explicit_cookie_jar_id: Option<&str>,
|
||||
) -> Result<CliExecutionContext, String> {
|
||||
let request = context
|
||||
.db()
|
||||
.get_any_request(request_id)
|
||||
.map_err(|e| format!("Failed to get request: {e}"))?;
|
||||
|
||||
let workspace_id = match request {
|
||||
AnyRequest::HttpRequest(r) => r.workspace_id,
|
||||
AnyRequest::GrpcRequest(r) => r.workspace_id,
|
||||
AnyRequest::WebsocketRequest(r) => r.workspace_id,
|
||||
};
|
||||
let cookie_jar_id = resolve_cookie_jar_id(context, &workspace_id, explicit_cookie_jar_id)?;
|
||||
|
||||
Ok(CliExecutionContext {
|
||||
request_id: Some(request_id.to_string()),
|
||||
workspace_id: Some(workspace_id),
|
||||
environment_id: environment.map(str::to_string),
|
||||
cookie_jar_id,
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_cookie_jar_id(
|
||||
context: &CliContext,
|
||||
workspace_id: &str,
|
||||
explicit_cookie_jar_id: Option<&str>,
|
||||
) -> Result<Option<String>, String> {
|
||||
if let Some(cookie_jar_id) = explicit_cookie_jar_id {
|
||||
return Ok(Some(cookie_jar_id.to_string()));
|
||||
}
|
||||
|
||||
let default_cookie_jar = context
|
||||
.db()
|
||||
.list_cookie_jars(workspace_id)
|
||||
.map_err(|e| format!("Failed to list cookie jars: {e}"))?
|
||||
.into_iter()
|
||||
.min_by_key(|jar| jar.created_at)
|
||||
.map(|jar| jar.id);
|
||||
Ok(default_cookie_jar)
|
||||
}
|
||||
|
||||
@@ -17,14 +17,6 @@ pub fn warning(message: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn warning_stderr(message: &str) {
|
||||
if io::stderr().is_terminal() {
|
||||
eprintln!("{:<8} {}", style("WARNING").yellow().bold(), style(message).yellow());
|
||||
} else {
|
||||
eprintln!("WARNING {message}");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn success(message: &str) {
|
||||
if io::stdout().is_terminal() {
|
||||
println!("{:<8} {}", style("SUCCESS").green().bold(), style(message).green());
|
||||
|
||||
@@ -2,4 +2,3 @@ pub mod confirm;
|
||||
pub mod http;
|
||||
pub mod json;
|
||||
pub mod schema;
|
||||
pub mod workspace;
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
use crate::context::CliContext;
|
||||
|
||||
pub fn resolve_workspace_id(
|
||||
ctx: &CliContext,
|
||||
workspace_id: Option<&str>,
|
||||
command_name: &str,
|
||||
) -> Result<String, String> {
|
||||
if let Some(workspace_id) = workspace_id {
|
||||
return Ok(workspace_id.to_string());
|
||||
}
|
||||
|
||||
let workspaces =
|
||||
ctx.db().list_workspaces().map_err(|e| format!("Failed to list workspaces: {e}"))?;
|
||||
match workspaces.as_slice() {
|
||||
[] => Err(format!("No workspaces found. {command_name} requires a workspace ID.")),
|
||||
[workspace] => Ok(workspace.id.clone()),
|
||||
_ => Err(format!("Multiple workspaces found. {command_name} requires a workspace ID.")),
|
||||
}
|
||||
}
|
||||
@@ -1,226 +0,0 @@
|
||||
use crate::ui;
|
||||
use crate::version;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::io::IsTerminal;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
use yaak_api::{ApiClientKind, yaak_api_client};
|
||||
|
||||
const CACHE_FILE_NAME: &str = "cli-version-check.json";
|
||||
const CHECK_INTERVAL_SECS: u64 = 24 * 60 * 60;
|
||||
const REQUEST_TIMEOUT: Duration = Duration::from_millis(800);
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(default)]
|
||||
struct VersionCheckResponse {
|
||||
outdated: bool,
|
||||
latest_version: Option<String>,
|
||||
upgrade_hint: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
struct CacheRecord {
|
||||
checked_at_epoch_secs: u64,
|
||||
response: VersionCheckResponse,
|
||||
last_warned_at_epoch_secs: Option<u64>,
|
||||
last_warned_version: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for CacheRecord {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
checked_at_epoch_secs: 0,
|
||||
response: VersionCheckResponse::default(),
|
||||
last_warned_at_epoch_secs: None,
|
||||
last_warned_version: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct VersionCheckRequest<'a> {
|
||||
current_version: &'a str,
|
||||
channel: String,
|
||||
install_source: String,
|
||||
platform: &'a str,
|
||||
arch: &'a str,
|
||||
}
|
||||
|
||||
pub async fn maybe_check_for_updates() {
|
||||
if should_skip_check() {
|
||||
return;
|
||||
}
|
||||
|
||||
let now = unix_epoch_secs();
|
||||
let cache_path = cache_path();
|
||||
let cached = read_cache(&cache_path);
|
||||
|
||||
if let Some(cache) = cached.as_ref().filter(|c| !is_expired(c.checked_at_epoch_secs, now)) {
|
||||
let mut record = cache.clone();
|
||||
maybe_warn_outdated(&mut record, now);
|
||||
write_cache(&cache_path, &record);
|
||||
return;
|
||||
}
|
||||
|
||||
let fresh = fetch_version_check().await;
|
||||
match fresh {
|
||||
Some(response) => {
|
||||
let mut record = CacheRecord {
|
||||
checked_at_epoch_secs: now,
|
||||
response: response.clone(),
|
||||
last_warned_at_epoch_secs: cached
|
||||
.as_ref()
|
||||
.and_then(|c| c.last_warned_at_epoch_secs),
|
||||
last_warned_version: cached.as_ref().and_then(|c| c.last_warned_version.clone()),
|
||||
};
|
||||
maybe_warn_outdated(&mut record, now);
|
||||
write_cache(&cache_path, &record);
|
||||
}
|
||||
None => {
|
||||
let fallback = cached.as_ref().map(|cache| cache.response.clone()).unwrap_or_default();
|
||||
let mut record = CacheRecord {
|
||||
checked_at_epoch_secs: now,
|
||||
response: fallback,
|
||||
last_warned_at_epoch_secs: cached
|
||||
.as_ref()
|
||||
.and_then(|c| c.last_warned_at_epoch_secs),
|
||||
last_warned_version: cached.as_ref().and_then(|c| c.last_warned_version.clone()),
|
||||
};
|
||||
maybe_warn_outdated(&mut record, now);
|
||||
write_cache(&cache_path, &record);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn should_skip_check() -> bool {
|
||||
if std::env::var("YAAK_CLI_NO_UPDATE_CHECK")
|
||||
.is_ok_and(|v| v == "1" || v.eq_ignore_ascii_case("true"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if std::env::var("CI").is_ok() {
|
||||
return true;
|
||||
}
|
||||
|
||||
!std::io::stdout().is_terminal()
|
||||
}
|
||||
|
||||
async fn fetch_version_check() -> Option<VersionCheckResponse> {
|
||||
let api_url = format!("{}/cli/check", update_base_url());
|
||||
let current_version = version::cli_version();
|
||||
let payload = VersionCheckRequest {
|
||||
current_version,
|
||||
channel: release_channel(current_version),
|
||||
install_source: install_source(),
|
||||
platform: std::env::consts::OS,
|
||||
arch: std::env::consts::ARCH,
|
||||
};
|
||||
|
||||
let client = yaak_api_client(ApiClientKind::Cli, current_version).ok()?;
|
||||
let request = client.post(api_url).json(&payload);
|
||||
|
||||
let response = tokio::time::timeout(REQUEST_TIMEOUT, request.send()).await.ok()?.ok()?;
|
||||
if !response.status().is_success() {
|
||||
return None;
|
||||
}
|
||||
|
||||
tokio::time::timeout(REQUEST_TIMEOUT, response.json::<VersionCheckResponse>()).await.ok()?.ok()
|
||||
}
|
||||
|
||||
fn release_channel(version: &str) -> String {
|
||||
version
|
||||
.split_once('-')
|
||||
.and_then(|(_, suffix)| suffix.split('.').next())
|
||||
.unwrap_or("stable")
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn install_source() -> String {
|
||||
std::env::var("YAAK_CLI_INSTALL_SOURCE")
|
||||
.ok()
|
||||
.filter(|s| !s.trim().is_empty())
|
||||
.unwrap_or_else(|| "source".to_string())
|
||||
}
|
||||
|
||||
fn update_base_url() -> &'static str {
|
||||
match std::env::var("ENVIRONMENT").ok().as_deref() {
|
||||
Some("development") => "http://localhost:9444",
|
||||
_ => "https://update.yaak.app",
|
||||
}
|
||||
}
|
||||
|
||||
fn maybe_warn_outdated(record: &mut CacheRecord, now: u64) {
|
||||
if !record.response.outdated {
|
||||
return;
|
||||
}
|
||||
|
||||
let latest =
|
||||
record.response.latest_version.clone().unwrap_or_else(|| "a newer release".to_string());
|
||||
let warn_suppressed = record.last_warned_version.as_deref() == Some(latest.as_str())
|
||||
&& record.last_warned_at_epoch_secs.is_some_and(|t| !is_expired(t, now));
|
||||
if warn_suppressed {
|
||||
return;
|
||||
}
|
||||
|
||||
let hint = record.response.upgrade_hint.clone().unwrap_or_else(default_upgrade_hint);
|
||||
ui::warning_stderr(&format!("A newer Yaak CLI version is available ({latest}). {hint}"));
|
||||
record.last_warned_version = Some(latest);
|
||||
record.last_warned_at_epoch_secs = Some(now);
|
||||
}
|
||||
|
||||
fn default_upgrade_hint() -> String {
|
||||
if install_source() == "npm" {
|
||||
let channel = release_channel(version::cli_version());
|
||||
if channel == "stable" {
|
||||
return "Run `npm install -g @yaakapp/cli@latest` to update.".to_string();
|
||||
}
|
||||
return format!("Run `npm install -g @yaakapp/cli@{channel}` to update.");
|
||||
}
|
||||
|
||||
"Update your Yaak CLI installation to the latest release.".to_string()
|
||||
}
|
||||
|
||||
fn cache_path() -> PathBuf {
|
||||
std::env::temp_dir().join("yaak-cli").join(format!("{}-{CACHE_FILE_NAME}", environment_name()))
|
||||
}
|
||||
|
||||
fn environment_name() -> &'static str {
|
||||
match std::env::var("ENVIRONMENT").ok().as_deref() {
|
||||
Some("staging") => "staging",
|
||||
Some("development") => "development",
|
||||
_ => "production",
|
||||
}
|
||||
}
|
||||
|
||||
fn read_cache(path: &Path) -> Option<CacheRecord> {
|
||||
let contents = fs::read_to_string(path).ok()?;
|
||||
serde_json::from_str::<CacheRecord>(&contents).ok()
|
||||
}
|
||||
|
||||
fn write_cache(path: &Path, record: &CacheRecord) {
|
||||
let Some(parent) = path.parent() else {
|
||||
return;
|
||||
};
|
||||
if fs::create_dir_all(parent).is_err() {
|
||||
return;
|
||||
}
|
||||
let Ok(json) = serde_json::to_string(record) else {
|
||||
return;
|
||||
};
|
||||
let _ = fs::write(path, json);
|
||||
}
|
||||
|
||||
fn is_expired(checked_at_epoch_secs: u64, now: u64) -> bool {
|
||||
now.saturating_sub(checked_at_epoch_secs) >= CHECK_INTERVAL_SECS
|
||||
}
|
||||
|
||||
fn unix_epoch_secs() -> u64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_else(|_| Duration::from_secs(0))
|
||||
.as_secs()
|
||||
}
|
||||
@@ -70,8 +70,6 @@ fn workspace_schema_outputs_json_schema() {
|
||||
.stdout(contains("\"type\":\"object\""))
|
||||
.stdout(contains("\"x-yaak-agent-hints\""))
|
||||
.stdout(contains("\"templateVariableSyntax\":\"${[ my_var ]}\""))
|
||||
.stdout(contains(
|
||||
"\"templateFunctionSyntax\":\"${[ namespace.my_func(a='aaa',b='bbb') ]}\"",
|
||||
))
|
||||
.stdout(contains("\"templateFunctionSyntax\":\"${[ namespace.my_func(a='aaa',b='bbb') ]}\""))
|
||||
.stdout(contains("\"name\""));
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
[package]
|
||||
name = "yaak-proxy-lib"
|
||||
version = "0.0.0"
|
||||
edition = "2024"
|
||||
authors = ["Gregory Schier"]
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
chrono = { workspace = true, features = ["serde"] }
|
||||
log = { workspace = true }
|
||||
include_dir = "0.7"
|
||||
r2d2 = "0.8.10"
|
||||
r2d2_sqlite = "0.25.0"
|
||||
rusqlite = { version = "0.32.1", features = ["bundled", "chrono"] }
|
||||
sea-query = { version = "0.32.1", features = ["with-chrono", "attr"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
ts-rs = { workspace = true, features = ["chrono-impl"] }
|
||||
yaak-database = { workspace = true }
|
||||
yaak-proxy = { workspace = true }
|
||||
yaak-rpc = { workspace = true }
|
||||
@@ -1,3 +0,0 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type ModelChangeEvent = { "type": "upsert", created: boolean, } | { "type": "delete" };
|
||||
@@ -1,8 +0,0 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ModelChangeEvent } from "./ModelChangeEvent";
|
||||
|
||||
export type HttpExchange = { id: string, createdAt: string, updatedAt: string, url: string, method: string, reqHeaders: Array<ProxyHeader>, reqBody: Array<number> | null, resStatus: number | null, resHeaders: Array<ProxyHeader>, resBody: Array<number> | null, error: string | null, };
|
||||
|
||||
export type ModelPayload = { model: HttpExchange, change: ModelChangeEvent, };
|
||||
|
||||
export type ProxyHeader = { name: string, value: string, };
|
||||
20
crates-proxy/yaak-proxy-lib/bindings/gen_rpc.ts
generated
@@ -1,20 +0,0 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { HttpExchange, ModelPayload } from "./gen_models";
|
||||
|
||||
export type ActionInvocation = { "scope": "global", action: GlobalAction, };
|
||||
|
||||
export type ActionMetadata = { label: string, defaultHotkey: string | null, };
|
||||
|
||||
export type GlobalAction = "proxy_start" | "proxy_stop";
|
||||
|
||||
export type ListActionsRequest = Record<string, never>;
|
||||
|
||||
export type ListActionsResponse = { actions: Array<[ActionInvocation, ActionMetadata]>, };
|
||||
|
||||
export type ListModelsRequest = Record<string, never>;
|
||||
|
||||
export type ListModelsResponse = { httpExchanges: Array<HttpExchange>, };
|
||||
|
||||
export type RpcEventSchema = { model_write: ModelPayload, };
|
||||
|
||||
export type RpcSchema = { execute_action: [ActionInvocation, boolean], list_actions: [ListActionsRequest, ListActionsResponse], list_models: [ListModelsRequest, ListModelsResponse], };
|
||||
2
crates-proxy/yaak-proxy-lib/bindings/index.ts
generated
@@ -1,2 +0,0 @@
|
||||
export * from "./gen_rpc";
|
||||
export * from "./gen_models";
|
||||
@@ -1,14 +0,0 @@
|
||||
CREATE TABLE http_exchanges
|
||||
(
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
url TEXT NOT NULL DEFAULT '',
|
||||
method TEXT NOT NULL DEFAULT '',
|
||||
req_headers TEXT NOT NULL DEFAULT '[]',
|
||||
req_body BLOB,
|
||||
res_status INTEGER,
|
||||
res_headers TEXT NOT NULL DEFAULT '[]',
|
||||
res_body BLOB,
|
||||
error TEXT
|
||||
);
|
||||
@@ -1,53 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(export, export_to = "gen_rpc.ts")]
|
||||
pub enum GlobalAction {
|
||||
ProxyStart,
|
||||
ProxyStop,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[serde(tag = "scope", rename_all = "snake_case")]
|
||||
#[ts(export, export_to = "gen_rpc.ts")]
|
||||
pub enum ActionInvocation {
|
||||
Global { action: GlobalAction },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "gen_rpc.ts")]
|
||||
pub struct ActionMetadata {
|
||||
pub label: String,
|
||||
pub default_hotkey: Option<String>,
|
||||
}
|
||||
|
||||
fn default_hotkey(mac: &str, other: &str) -> Option<String> {
|
||||
if cfg!(target_os = "macos") {
|
||||
Some(mac.into())
|
||||
} else {
|
||||
Some(other.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// All global actions with their metadata, used by `list_actions` RPC.
|
||||
pub fn all_global_actions() -> Vec<(ActionInvocation, ActionMetadata)> {
|
||||
vec![
|
||||
(
|
||||
ActionInvocation::Global { action: GlobalAction::ProxyStart },
|
||||
ActionMetadata {
|
||||
label: "Start Proxy".into(),
|
||||
default_hotkey: default_hotkey("Meta+Shift+P", "Ctrl+Shift+P"),
|
||||
},
|
||||
),
|
||||
(
|
||||
ActionInvocation::Global { action: GlobalAction::ProxyStop },
|
||||
ActionMetadata {
|
||||
label: "Stop Proxy".into(),
|
||||
default_hotkey: default_hotkey("Meta+Shift+S", "Ctrl+Shift+S"),
|
||||
},
|
||||
),
|
||||
]
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
use include_dir::{Dir, include_dir};
|
||||
use r2d2::Pool;
|
||||
use r2d2_sqlite::SqliteConnectionManager;
|
||||
use std::path::Path;
|
||||
use yaak_database::{ConnectionOrTx, DbContext, run_migrations};
|
||||
|
||||
static MIGRATIONS: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/migrations");
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ProxyQueryManager {
|
||||
pool: Pool<SqliteConnectionManager>,
|
||||
}
|
||||
|
||||
impl ProxyQueryManager {
|
||||
pub fn new(db_path: &Path) -> Self {
|
||||
let manager = SqliteConnectionManager::file(db_path);
|
||||
let pool = Pool::builder()
|
||||
.max_size(5)
|
||||
.build(manager)
|
||||
.expect("Failed to create proxy DB pool");
|
||||
run_migrations(&pool, &MIGRATIONS).expect("Failed to run proxy DB migrations");
|
||||
Self { pool }
|
||||
}
|
||||
|
||||
pub fn with_conn<F, T>(&self, func: F) -> T
|
||||
where
|
||||
F: FnOnce(&DbContext) -> T,
|
||||
{
|
||||
let conn = self.pool.get().expect("Failed to get proxy DB connection");
|
||||
let ctx = DbContext::new(ConnectionOrTx::Connection(conn));
|
||||
func(&ctx)
|
||||
}
|
||||
}
|
||||
@@ -1,225 +0,0 @@
|
||||
pub mod actions;
|
||||
pub mod db;
|
||||
pub mod models;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::sync::Mutex;
|
||||
use log::warn;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
use yaak_database::{ModelChangeEvent, UpdateSource};
|
||||
use yaak_proxy::{CapturedRequest, ProxyEvent, ProxyHandle, RequestState};
|
||||
use yaak_rpc::{RpcError, RpcEventEmitter, define_rpc};
|
||||
use crate::actions::{ActionInvocation, ActionMetadata, GlobalAction};
|
||||
use crate::db::ProxyQueryManager;
|
||||
use crate::models::{HttpExchange, ModelPayload, ProxyHeader};
|
||||
|
||||
// -- Context --
|
||||
|
||||
pub struct ProxyCtx {
|
||||
handle: Mutex<Option<ProxyHandle>>,
|
||||
pub db: ProxyQueryManager,
|
||||
pub events: RpcEventEmitter,
|
||||
}
|
||||
|
||||
impl ProxyCtx {
|
||||
pub fn new(db_path: &Path, events: RpcEventEmitter) -> Self {
|
||||
Self {
|
||||
handle: Mutex::new(None),
|
||||
db: ProxyQueryManager::new(db_path),
|
||||
events,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -- Request/response types --
|
||||
|
||||
#[derive(Deserialize, TS)]
|
||||
#[ts(export, export_to = "gen_rpc.ts")]
|
||||
pub struct ListActionsRequest {}
|
||||
|
||||
#[derive(Serialize, TS)]
|
||||
#[ts(export, export_to = "gen_rpc.ts")]
|
||||
pub struct ListActionsResponse {
|
||||
pub actions: Vec<(ActionInvocation, ActionMetadata)>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, TS)]
|
||||
#[ts(export, export_to = "gen_rpc.ts")]
|
||||
pub struct ListModelsRequest {}
|
||||
|
||||
#[derive(Serialize, TS)]
|
||||
#[ts(export, export_to = "gen_rpc.ts")]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ListModelsResponse {
|
||||
pub http_exchanges: Vec<HttpExchange>,
|
||||
}
|
||||
|
||||
// -- Handlers --
|
||||
|
||||
fn execute_action(ctx: &ProxyCtx, invocation: ActionInvocation) -> Result<bool, RpcError> {
|
||||
match invocation {
|
||||
ActionInvocation::Global { action } => match action {
|
||||
GlobalAction::ProxyStart => {
|
||||
let mut handle = ctx
|
||||
.handle
|
||||
.lock()
|
||||
.map_err(|_| RpcError { message: "lock poisoned".into() })?;
|
||||
|
||||
if handle.is_some() {
|
||||
return Ok(true); // already running
|
||||
}
|
||||
|
||||
let mut proxy_handle = yaak_proxy::start_proxy(9090)
|
||||
.map_err(|e| RpcError { message: e })?;
|
||||
|
||||
if let Some(event_rx) = proxy_handle.take_event_rx() {
|
||||
let db = ctx.db.clone();
|
||||
let events = ctx.events.clone();
|
||||
std::thread::spawn(move || run_event_loop(event_rx, db, events));
|
||||
}
|
||||
|
||||
*handle = Some(proxy_handle);
|
||||
Ok(true)
|
||||
}
|
||||
GlobalAction::ProxyStop => {
|
||||
let mut handle = ctx
|
||||
.handle
|
||||
.lock()
|
||||
.map_err(|_| RpcError { message: "lock poisoned".into() })?;
|
||||
handle.take();
|
||||
Ok(true)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn list_actions(_ctx: &ProxyCtx, _req: ListActionsRequest) -> Result<ListActionsResponse, RpcError> {
|
||||
Ok(ListActionsResponse {
|
||||
actions: crate::actions::all_global_actions(),
|
||||
})
|
||||
}
|
||||
|
||||
fn list_models(ctx: &ProxyCtx, _req: ListModelsRequest) -> Result<ListModelsResponse, RpcError> {
|
||||
ctx.db.with_conn(|db| {
|
||||
Ok(ListModelsResponse {
|
||||
http_exchanges: db.find_all::<HttpExchange>()
|
||||
.map_err(|e| RpcError { message: e.to_string() })?,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// -- Event loop --
|
||||
|
||||
fn run_event_loop(rx: std::sync::mpsc::Receiver<ProxyEvent>, db: ProxyQueryManager, events: RpcEventEmitter) {
|
||||
let mut in_flight: HashMap<u64, CapturedRequest> = HashMap::new();
|
||||
|
||||
while let Ok(event) = rx.recv() {
|
||||
match event {
|
||||
ProxyEvent::RequestStart { id, method, url, http_version } => {
|
||||
in_flight.insert(id, CapturedRequest {
|
||||
id,
|
||||
method,
|
||||
url,
|
||||
http_version,
|
||||
status: None,
|
||||
elapsed_ms: None,
|
||||
remote_http_version: None,
|
||||
request_headers: vec![],
|
||||
request_body: None,
|
||||
response_headers: vec![],
|
||||
response_body: None,
|
||||
response_body_size: 0,
|
||||
state: RequestState::Sending,
|
||||
error: None,
|
||||
});
|
||||
}
|
||||
ProxyEvent::RequestHeader { id, name, value } => {
|
||||
if let Some(r) = in_flight.get_mut(&id) {
|
||||
r.request_headers.push((name, value));
|
||||
}
|
||||
}
|
||||
ProxyEvent::RequestBody { id, body } => {
|
||||
if let Some(r) = in_flight.get_mut(&id) {
|
||||
r.request_body = Some(body);
|
||||
}
|
||||
}
|
||||
ProxyEvent::ResponseStart { id, status, http_version, elapsed_ms } => {
|
||||
if let Some(r) = in_flight.get_mut(&id) {
|
||||
r.status = Some(status);
|
||||
r.remote_http_version = Some(http_version);
|
||||
r.elapsed_ms = Some(elapsed_ms);
|
||||
r.state = RequestState::Receiving;
|
||||
}
|
||||
}
|
||||
ProxyEvent::ResponseHeader { id, name, value } => {
|
||||
if let Some(r) = in_flight.get_mut(&id) {
|
||||
r.response_headers.push((name, value));
|
||||
}
|
||||
}
|
||||
ProxyEvent::ResponseBodyChunk { .. } => {
|
||||
// Progress only — no action needed
|
||||
}
|
||||
ProxyEvent::ResponseBodyComplete { id, body, size, elapsed_ms } => {
|
||||
if let Some(mut r) = in_flight.remove(&id) {
|
||||
r.response_body = body;
|
||||
r.response_body_size = size;
|
||||
r.elapsed_ms = r.elapsed_ms.or(Some(elapsed_ms));
|
||||
r.state = RequestState::Complete;
|
||||
write_entry(&db, &events, &r);
|
||||
}
|
||||
}
|
||||
ProxyEvent::Error { id, error } => {
|
||||
if let Some(mut r) = in_flight.remove(&id) {
|
||||
r.error = Some(error);
|
||||
r.state = RequestState::Error;
|
||||
write_entry(&db, &events, &r);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn write_entry(db: &ProxyQueryManager, events: &RpcEventEmitter, r: &CapturedRequest) {
|
||||
let entry = HttpExchange {
|
||||
url: r.url.clone(),
|
||||
method: r.method.clone(),
|
||||
req_headers: r.request_headers.iter()
|
||||
.map(|(n, v)| ProxyHeader { name: n.clone(), value: v.clone() })
|
||||
.collect(),
|
||||
req_body: r.request_body.clone(),
|
||||
res_status: r.status.map(|s| s as i32),
|
||||
res_headers: r.response_headers.iter()
|
||||
.map(|(n, v)| ProxyHeader { name: n.clone(), value: v.clone() })
|
||||
.collect(),
|
||||
res_body: r.response_body.clone(),
|
||||
error: r.error.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
db.with_conn(|ctx| {
|
||||
match ctx.upsert(&entry, &UpdateSource::Background) {
|
||||
Ok((saved, created)) => {
|
||||
events.emit("model_write", &ModelPayload {
|
||||
model: saved,
|
||||
change: ModelChangeEvent::Upsert { created },
|
||||
});
|
||||
}
|
||||
Err(e) => warn!("Failed to write proxy entry: {e}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// -- Router + Schema --
|
||||
|
||||
define_rpc! {
|
||||
ProxyCtx;
|
||||
commands {
|
||||
execute_action(ActionInvocation) -> bool,
|
||||
list_actions(ListActionsRequest) -> ListActionsResponse,
|
||||
list_models(ListModelsRequest) -> ListModelsResponse,
|
||||
}
|
||||
events {
|
||||
model_write(ModelPayload),
|
||||
}
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
use chrono::NaiveDateTime;
|
||||
use rusqlite::Row;
|
||||
use sea_query::{IntoColumnRef, IntoIden, IntoTableRef, Order, SimpleExpr, enum_def};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
use yaak_database::{ModelChangeEvent, Result as DbResult, UpdateSource, UpsertModelInfo, generate_prefixed_id, upsert_date};
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "gen_models.ts")]
|
||||
pub struct ProxyHeader {
|
||||
pub name: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "gen_models.ts")]
|
||||
#[enum_def(table_name = "http_exchanges")]
|
||||
pub struct HttpExchange {
|
||||
pub id: String,
|
||||
pub created_at: NaiveDateTime,
|
||||
pub updated_at: NaiveDateTime,
|
||||
pub url: String,
|
||||
pub method: String,
|
||||
pub req_headers: Vec<ProxyHeader>,
|
||||
pub req_body: Option<Vec<u8>>,
|
||||
pub res_status: Option<i32>,
|
||||
pub res_headers: Vec<ProxyHeader>,
|
||||
pub res_body: Option<Vec<u8>>,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "gen_models.ts")]
|
||||
pub struct ModelPayload {
|
||||
pub model: HttpExchange,
|
||||
pub change: ModelChangeEvent,
|
||||
}
|
||||
|
||||
impl UpsertModelInfo for HttpExchange {
|
||||
fn table_name() -> impl IntoTableRef + IntoIden {
|
||||
HttpExchangeIden::Table
|
||||
}
|
||||
|
||||
fn id_column() -> impl IntoIden + Eq + Clone {
|
||||
HttpExchangeIden::Id
|
||||
}
|
||||
|
||||
fn generate_id() -> String {
|
||||
generate_prefixed_id("he")
|
||||
}
|
||||
|
||||
fn order_by() -> (impl IntoColumnRef, Order) {
|
||||
(HttpExchangeIden::CreatedAt, Order::Desc)
|
||||
}
|
||||
|
||||
fn get_id(&self) -> String {
|
||||
self.id.clone()
|
||||
}
|
||||
|
||||
fn insert_values(
|
||||
self,
|
||||
source: &UpdateSource,
|
||||
) -> DbResult<Vec<(impl IntoIden + Eq, impl Into<SimpleExpr>)>> {
|
||||
use HttpExchangeIden::*;
|
||||
Ok(vec![
|
||||
(CreatedAt, upsert_date(source, self.created_at)),
|
||||
(UpdatedAt, upsert_date(source, self.updated_at)),
|
||||
(Url, self.url.into()),
|
||||
(Method, self.method.into()),
|
||||
(ReqHeaders, serde_json::to_string(&self.req_headers)?.into()),
|
||||
(ReqBody, self.req_body.into()),
|
||||
(ResStatus, self.res_status.into()),
|
||||
(ResHeaders, serde_json::to_string(&self.res_headers)?.into()),
|
||||
(ResBody, self.res_body.into()),
|
||||
(Error, self.error.into()),
|
||||
])
|
||||
}
|
||||
|
||||
fn update_columns() -> Vec<impl IntoIden> {
|
||||
vec![
|
||||
HttpExchangeIden::UpdatedAt,
|
||||
HttpExchangeIden::Url,
|
||||
HttpExchangeIden::Method,
|
||||
HttpExchangeIden::ReqHeaders,
|
||||
HttpExchangeIden::ReqBody,
|
||||
HttpExchangeIden::ResStatus,
|
||||
HttpExchangeIden::ResHeaders,
|
||||
HttpExchangeIden::ResBody,
|
||||
HttpExchangeIden::Error,
|
||||
]
|
||||
}
|
||||
|
||||
fn from_row(r: &Row) -> rusqlite::Result<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let req_headers: String = r.get("req_headers")?;
|
||||
let res_headers: String = r.get("res_headers")?;
|
||||
Ok(Self {
|
||||
id: r.get("id")?,
|
||||
created_at: r.get("created_at")?,
|
||||
updated_at: r.get("updated_at")?,
|
||||
url: r.get("url")?,
|
||||
method: r.get("method")?,
|
||||
req_headers: serde_json::from_str(&req_headers).unwrap_or_default(),
|
||||
req_body: r.get("req_body")?,
|
||||
res_status: r.get("res_status")?,
|
||||
res_headers: serde_json::from_str(&res_headers).unwrap_or_default(),
|
||||
res_body: r.get("res_body")?,
|
||||
error: r.get("error")?,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"name": "@yaakapp-internal/tauri-client",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"main": "bindings/index.ts"
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
tauri_app_client_lib::run();
|
||||
}
|
||||
8
crates-tauri/yaak-app-proxy/.gitignore
vendored
@@ -1,8 +0,0 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
target/
|
||||
|
||||
gen/*
|
||||
|
||||
**/permissions/autogenerated
|
||||
**/permissions/schemas
|
||||
@@ -1,22 +0,0 @@
|
||||
[package]
|
||||
name = "yaak-app-proxy"
|
||||
version = "0.0.0"
|
||||
edition = "2024"
|
||||
authors = ["Gregory Schier"]
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
name = "tauri_app_proxy_lib"
|
||||
crate-type = ["staticlib", "cdylib", "lib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.5.3", features = [] }
|
||||
|
||||
[dependencies]
|
||||
log = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tauri = { workspace = true }
|
||||
tauri-plugin-os = "2.3.2"
|
||||
yaak-proxy-lib = { workspace = true }
|
||||
yaak-rpc = { workspace = true }
|
||||
yaak-window = { workspace = true }
|
||||
1
crates-tauri/yaak-app-proxy/bindings/index.ts
generated
@@ -1 +0,0 @@
|
||||
export {};
|
||||
@@ -1,3 +0,0 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"identifier": "default",
|
||||
"description": "Default capabilities for the Yaak Proxy app",
|
||||
"windows": [
|
||||
"*"
|
||||
],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"os:allow-os-type",
|
||||
"core:window:allow-close",
|
||||
"core:window:allow-is-fullscreen",
|
||||
"core:window:allow-is-maximized",
|
||||
"core:window:allow-maximize",
|
||||
"core:window:allow-minimize",
|
||||
"core:window:allow-start-dragging",
|
||||
"core:window:allow-unmaximize"
|
||||
]
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"name": "@yaakapp-internal/tauri-proxy",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"main": "bindings/index.ts"
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
use log::error;
|
||||
use tauri::{Emitter, Manager, RunEvent, State};
|
||||
use yaak_proxy_lib::ProxyCtx;
|
||||
use yaak_rpc::{RpcEventEmitter, RpcRouter};
|
||||
use yaak_window::window::CreateWindowConfig;
|
||||
|
||||
#[tauri::command]
|
||||
fn rpc(
|
||||
router: State<'_, RpcRouter<ProxyCtx>>,
|
||||
ctx: State<'_, ProxyCtx>,
|
||||
cmd: String,
|
||||
payload: serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
router.dispatch(&cmd, payload, &ctx).map_err(|e| e.message)
|
||||
}
|
||||
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_os::init())
|
||||
.setup(|app| {
|
||||
let data_dir = app.path().app_data_dir().expect("no app data dir");
|
||||
std::fs::create_dir_all(&data_dir).expect("failed to create app data dir");
|
||||
|
||||
let (emitter, event_rx) = RpcEventEmitter::new();
|
||||
app.manage(ProxyCtx::new(&data_dir.join("proxy.db"), emitter));
|
||||
app.manage(yaak_proxy_lib::build_router());
|
||||
|
||||
// Drain RPC events and forward as Tauri events
|
||||
let app_handle = app.handle().clone();
|
||||
std::thread::spawn(move || {
|
||||
for event in event_rx {
|
||||
if let Err(e) = app_handle.emit(event.event, event.payload) {
|
||||
error!("Failed to emit RPC event: {e}");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![rpc])
|
||||
.build(tauri::generate_context!())
|
||||
.expect("error while building yaak proxy tauri application")
|
||||
.run(|app_handle, event| {
|
||||
if let RunEvent::Ready = event {
|
||||
let config = CreateWindowConfig {
|
||||
url: "/",
|
||||
label: "main",
|
||||
title: "Yaak Proxy",
|
||||
inner_size: Some((1000.0, 700.0)),
|
||||
visible: true,
|
||||
hide_titlebar: true,
|
||||
..Default::default()
|
||||
};
|
||||
if let Err(e) = yaak_window::window::create_window(app_handle, config) {
|
||||
error!("Failed to create proxy window: {e:?}");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"productName": "Yaak Proxy",
|
||||
"version": "0.0.0",
|
||||
"identifier": "app.yaak.proxy",
|
||||
"build": {
|
||||
"beforeBuildCommand": "npm --prefix ../.. run proxy:tauri-before-build",
|
||||
"beforeDevCommand": "npm --prefix ../.. run proxy:tauri-before-dev",
|
||||
"devUrl": "http://localhost:2420",
|
||||
"frontendDist": "../../dist/apps/yaak-proxy"
|
||||
},
|
||||
"app": {
|
||||
"withGlobalTauri": false,
|
||||
"windows": []
|
||||
},
|
||||
"bundle": {
|
||||
"icon": [
|
||||
"../yaak-app-client/icons/release/32x32.png",
|
||||
"../yaak-app-client/icons/release/128x128.png",
|
||||
"../yaak-app-client/icons/release/128x128@2x.png",
|
||||
"../yaak-app-client/icons/release/icon.icns",
|
||||
"../yaak-app-client/icons/release/icon.ico"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"productName": "Yaak Proxy Dev",
|
||||
"identifier": "app.yaak.proxy.dev",
|
||||
"bundle": {
|
||||
"icon": [
|
||||
"../yaak-app-client/icons/dev/32x32.png",
|
||||
"../yaak-app-client/icons/dev/128x128.png",
|
||||
"../yaak-app-client/icons/dev/128x128@2x.png",
|
||||
"../yaak-app-client/icons/dev/icon.icns",
|
||||
"../yaak-app-client/icons/dev/icon.ico"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"build": {
|
||||
"features": []
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
[Desktop Entry]
|
||||
Categories={{categories}}
|
||||
Comment={{comment}}
|
||||
Exec={{exec}}
|
||||
Icon={{icon}}
|
||||
Name={{name}}
|
||||
StartupWMClass={{exec}}
|
||||
Terminal=false
|
||||
Type=Application
|
||||
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
name = "yaak-app-client"
|
||||
name = "yaak-app"
|
||||
version = "0.0.0"
|
||||
edition = "2024"
|
||||
authors = ["Gregory Schier"]
|
||||
@@ -7,7 +7,7 @@ publish = false
|
||||
|
||||
# Produce a library for mobile support
|
||||
[lib]
|
||||
name = "tauri_app_client_lib"
|
||||
name = "tauri_app_lib"
|
||||
crate-type = ["staticlib", "cdylib", "lib"]
|
||||
|
||||
[features]
|
||||
@@ -75,5 +75,4 @@ yaak-sse = { workspace = true }
|
||||
yaak-sync = { workspace = true }
|
||||
yaak-templates = { workspace = true }
|
||||
yaak-tls = { workspace = true }
|
||||
yaak-window = { workspace = true }
|
||||
yaak-ws = { workspace = true }
|
||||
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |