Compare commits

..

5 Commits

Author SHA1 Message Date
Gregory Schier
2439ffd28c chore: remove prompt form CLI test plugin scaffold 2026-03-03 16:01:25 -08:00
Gregory Schier
ad31755dbb feat(cli): add plugin install and complete prompt/render host events 2026-03-03 15:58:23 -08:00
Gregory Schier
b01b3a4c57 cli: implement plugin host render/send context and cookie jar support 2026-03-03 14:56:39 -08:00
Gregory Schier
ef63b88710 cli: add grpc/template render handlers and unsupported event errors 2026-03-03 09:00:14 -08:00
Gregory Schier
fb5ad8c7f7 cli: handle send/render http plugin host requests 2026-03-03 08:05:54 -08:00
703 changed files with 15737 additions and 17674 deletions

View File

@@ -1,11 +1,9 @@
# Claude Context: Detaching Tauri from Yaak
## Goal
Make Yaak runnable as a standalone CLI without Tauri as a dependency. The core Rust crates in `crates/` should be usable independently, while Tauri-specific code lives in `crates-tauri/`.
## Project Structure
```
crates/ # Core crates - should NOT depend on Tauri
crates-tauri/ # Tauri-specific crates (yaak-app, yaak-tauri-utils, etc.)
@@ -15,13 +13,11 @@ crates-cli/ # CLI crate (yaak-cli)
## Completed Work
### 1. Folder Restructure
- 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
### 2. Decoupled Crates (no longer depend on Tauri)
- **yaak-models**: Uses `init_standalone()` pattern for CLI database access
- **yaak-http**: Removed Tauri plugin, HttpConnectionManager initialized in yaak-app setup
- **yaak-common**: Only contains Tauri-free utilities (serde, platform)
@@ -29,7 +25,6 @@ crates-cli/ # CLI crate (yaak-cli)
- **yaak-grpc**: Replaced AppHandle with GrpcConfig struct, uses tokio::process::Command instead of Tauri sidecar
### 3. CLI Implementation
- Basic CLI at `crates-cli/yaak-cli/src/main.rs`
- Commands: workspaces, requests, send (by ID), get (ad-hoc URL), create
- Uses same database as Tauri app via `yaak_models::init_standalone()`
@@ -37,14 +32,12 @@ crates-cli/ # CLI crate (yaak-cli)
## Remaining Work
### Crates Still Depending on Tauri (in `crates/`)
1. **yaak-git** (3 files) - Moderate complexity
2. **yaak-plugins** (13 files) - **Hardest** - deeply integrated with Tauri for plugin-to-window communication
3. **yaak-sync** (4 files) - Moderate complexity
4. **yaak-ws** (5 files) - Moderate complexity
### Pattern for Decoupling
1. Remove Tauri plugin `init()` function from the crate
2. Move commands to `yaak-app/src/commands.rs` or keep inline in `lib.rs`
3. Move extension traits (e.g., `SomethingManagerExt`) to yaak-app or yaak-tauri-utils
@@ -54,7 +47,6 @@ crates-cli/ # CLI crate (yaak-cli)
7. Replace `tauri::async_runtime::block_on` with `tokio::runtime::Handle::current().block_on()`
## Key Files
- `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
@@ -62,11 +54,9 @@ crates-cli/ # CLI crate (yaak-cli)
- `crates/yaak-models/src/lib.rs` - Contains `init_standalone()` for CLI usage
## Git Branch
Working on `detach-tauri` branch.
## Recent Commits
```
c40cff40 Remove Tauri dependencies from yaak-crypto and yaak-grpc
df495f1d Move Tauri utilities from yaak-common to yaak-tauri-utils
@@ -77,7 +67,6 @@ 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 app-dev` to test the Tauri app still works
- Run `cargo run -p yaak-cli -- --help` to test the CLI

View File

@@ -8,7 +8,7 @@ Generate formatted release notes for Yaak releases by analyzing git history and
## What to do
1. Identifies the version tag and previous version
2. Retrieves all commits between versions
2. Retrieves all commits between versions
- If the version is a beta version, it retrieves commits between the beta version and previous beta version
- If the version is a stable version, it retrieves commits between the stable version and the previous stable version
3. Fetches PR descriptions for linked issues to find:
@@ -37,7 +37,6 @@ The skill generates markdown-formatted release notes following this structure:
**IMPORTANT**: Always add a blank lines around the markdown code fence and output the markdown code block last
**IMPORTANT**: PRs by `@gschier` should not mention the @username
**IMPORTANT**: These are app release notes. Exclude CLI-only changes (commits prefixed with `cli:` or only touching `crates-cli/`) since the CLI has its own release process.
## After Generating Release Notes

View File

@@ -32,7 +32,6 @@ Generate formatted markdown release notes for a Yaak tag.
- Keep a blank line before and after the code fence.
- Output the markdown code block last.
- Do not append `by @gschier` for PRs authored by `@gschier`.
- These are app release notes. Exclude CLI-only changes (commits prefixed with `cli:` or only touching `crates-cli/`) since the CLI has its own release process.
## Release Creation Prompt

View File

@@ -1,9 +1,10 @@
---
name: Bug report
about: Create a report to help us improve
title: ""
labels: ""
assignees: ""
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
@@ -11,7 +12,6 @@ A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
@@ -24,17 +24,15 @@ A clear and concise description of what you expected to happen.
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View File

@@ -11,7 +11,6 @@
- [ ] I added or updated tests when reasonable.
Approved feedback item (required if not a bug fix or small-scope improvement):
<!-- https://yaak.app/feedback/... -->
## Related

View File

@@ -14,20 +14,17 @@ jobs:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: voidzero-dev/setup-vp@v1
with:
node-version: "24"
cache: true
- uses: actions/setup-node@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
shared-key: ci
cache-on-failure: true
- run: vp install
- run: npm ci
- run: npm run bootstrap
- run: npm run lint
- name: Run JS Tests
run: vp test
run: npm test
- name: Run Rust Tests
run: cargo test --all

View File

@@ -47,3 +47,4 @@ jobs:
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options
# claude_args: '--allowed-tools Bash(gh pr:*)'

View File

@@ -50,11 +50,8 @@ jobs:
- name: Checkout yaakapp/app
uses: actions/checkout@v4
- name: Setup Vite+
uses: voidzero-dev/setup-vp@v1
with:
node-version: "24"
cache: true
- name: Setup Node
uses: actions/setup-node@v4
- name: install Rust stable
uses: dtolnay/rust-toolchain@stable
@@ -90,15 +87,15 @@ jobs:
echo $dir >> $env:GITHUB_PATH
& $exe --version
- run: vp install
- run: npm ci
- run: npm run bootstrap
env:
YAAK_TARGET_ARCH: ${{ matrix.yaak_arch }}
- run: npm run lint
- name: Run JS Tests
run: vp test
run: npm test
- name: Run Rust Tests
run: cargo test --all --exclude yaak-cli
run: cargo test --all
- name: Set version
run: npm run replace-version

View File

@@ -16,23 +16,23 @@ jobs:
uses: JamesIves/github-sponsors-readme-action@v1
with:
token: ${{ secrets.SPONSORS_PAT }}
file: "README.md"
file: 'README.md'
maximum: 1999
template: '<a href="https://github.com/{{{ login }}}"><img src="{{{ avatarUrl }}}" width="50px" alt="User avatar: {{{ login }}}" /></a>&nbsp;&nbsp;'
active-only: false
include-private: true
marker: "sponsors-base"
marker: 'sponsors-base'
- name: Generate Sponsors
uses: JamesIves/github-sponsors-readme-action@v1
with:
token: ${{ secrets.SPONSORS_PAT }}
file: "README.md"
file: 'README.md'
minimum: 2000
template: '<a href="https://github.com/{{{ login }}}"><img src="{{{ avatarUrl }}}" width="80px" alt="User avatar: {{{ login }}}" /></a>&nbsp;&nbsp;'
active-only: false
include-private: true
marker: "sponsors-premium"
marker: 'sponsors-premium'
# ⚠️ Note: You can use any deployment step here to automatically push the README
# changes back to your branch.
@@ -41,4 +41,4 @@ jobs:
with:
branch: main
force: false
folder: "."
folder: '.'

View File

@@ -1 +0,0 @@
24.14.0

2
.npmrc
View File

@@ -1,2 +0,0 @@
# vite-plugin-wasm has not yet declared Vite 8 in its peerDependencies
legacy-peer-deps=true

View File

@@ -1,2 +0,0 @@
**/bindings/**
crates/yaak-templates/pkg/**

View File

@@ -1 +0,0 @@
vp lint

View File

@@ -1,7 +1,3 @@
{
"recommendations": [
"rust-lang.rust-analyzer",
"bradlc.vscode-tailwindcss",
"VoidZero.vite-plus-extension-pack"
]
"recommendations": ["biomejs.biome", "rust-lang.rust-analyzer", "bradlc.vscode-tailwindcss"]
}

View File

@@ -1,8 +1,6 @@
{
"editor.defaultFormatter": "oxc.oxc-vscode",
"editor.defaultFormatter": "biomejs.biome",
"editor.formatOnSave": true,
"editor.formatOnSaveMode": "file",
"editor.codeActionsOnSave": {
"source.fixAll.oxc": "explicit"
}
"biome.enabled": true,
"biome.lint.format.enable": true
}

117
Cargo.lock generated
View File

@@ -173,17 +173,6 @@ version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
[[package]]
name = "apollo-parser"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "947e21ff51879f8a40d7519dfe619268de2afba4042a8a43878276de3cb910f0"
dependencies = [
"memchr",
"rowan",
"thiserror 2.0.17",
]
[[package]]
name = "append-only-vec"
version = "0.1.8"
@@ -1358,12 +1347,6 @@ dependencies = [
"libc",
]
[[package]]
name = "countme"
version = "3.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636"
[[package]]
name = "cow-utils"
version = "0.1.3"
@@ -4573,7 +4556,7 @@ checksum = "75b1853bc34cadaa90aa09f95713d8b77ec0c0d3e2d90ccf7a74216f40d20850"
dependencies = [
"flate2",
"postcard",
"rustc-hash 2.1.1",
"rustc-hash",
"serde",
"serde_json",
"thiserror 2.0.17",
@@ -4616,7 +4599,7 @@ dependencies = [
"hashbrown 0.16.1",
"oxc_data_structures",
"oxc_estree",
"rustc-hash 2.1.1",
"rustc-hash",
"serde",
]
@@ -4672,7 +4655,7 @@ dependencies = [
"oxc_index",
"oxc_syntax",
"petgraph 0.8.3",
"rustc-hash 2.1.1",
"rustc-hash",
]
[[package]]
@@ -4693,7 +4676,7 @@ dependencies = [
"oxc_sourcemap",
"oxc_span",
"oxc_syntax",
"rustc-hash 2.1.1",
"rustc-hash",
]
[[package]]
@@ -4705,7 +4688,7 @@ dependencies = [
"cow-utils",
"oxc-browserslist",
"oxc_syntax",
"rustc-hash 2.1.1",
"rustc-hash",
"serde",
]
@@ -4780,7 +4763,7 @@ dependencies = [
"oxc_ecmascript",
"oxc_span",
"oxc_syntax",
"rustc-hash 2.1.1",
"rustc-hash",
]
[[package]]
@@ -4797,7 +4780,7 @@ dependencies = [
"oxc_semantic",
"oxc_span",
"oxc_syntax",
"rustc-hash 2.1.1",
"rustc-hash",
]
[[package]]
@@ -4822,7 +4805,7 @@ dependencies = [
"oxc_span",
"oxc_syntax",
"oxc_traverse",
"rustc-hash 2.1.1",
"rustc-hash",
]
[[package]]
@@ -4844,7 +4827,7 @@ dependencies = [
"oxc_regular_expression",
"oxc_span",
"oxc_syntax",
"rustc-hash 2.1.1",
"rustc-hash",
"seq-macro",
]
@@ -4860,7 +4843,7 @@ dependencies = [
"oxc_diagnostics",
"oxc_span",
"phf 0.13.1",
"rustc-hash 2.1.1",
"rustc-hash",
"unicode-id-start",
]
@@ -4877,7 +4860,7 @@ dependencies = [
"once_cell",
"papaya",
"pnp",
"rustc-hash 2.1.1",
"rustc-hash",
"self_cell",
"serde",
"serde_json",
@@ -4907,7 +4890,7 @@ dependencies = [
"oxc_span",
"oxc_syntax",
"phf 0.13.1",
"rustc-hash 2.1.1",
"rustc-hash",
"self_cell",
]
@@ -4919,7 +4902,7 @@ checksum = "c7f89482522f3cd820817d48ee4ade5b10822060d6e5e4d419f05f6d8bd29d70"
dependencies = [
"base64-simd",
"json-escape-simd",
"rustc-hash 2.1.1",
"rustc-hash",
"serde",
"serde_json",
]
@@ -4983,7 +4966,7 @@ dependencies = [
"oxc_span",
"oxc_syntax",
"oxc_traverse",
"rustc-hash 2.1.1",
"rustc-hash",
"serde",
"serde_json",
"sha1",
@@ -5008,7 +4991,7 @@ dependencies = [
"oxc_syntax",
"oxc_transformer",
"oxc_traverse",
"rustc-hash 2.1.1",
"rustc-hash",
]
[[package]]
@@ -5026,7 +5009,7 @@ dependencies = [
"oxc_semantic",
"oxc_span",
"oxc_syntax",
"rustc-hash 2.1.1",
"rustc-hash",
]
[[package]]
@@ -5431,7 +5414,7 @@ dependencies = [
"nodejs-built-in-modules",
"pathdiff",
"radix_trie",
"rustc-hash 2.1.1",
"rustc-hash",
"serde",
"serde_json",
"thiserror 2.0.17",
@@ -5550,18 +5533,6 @@ dependencies = [
"termtree",
]
[[package]]
name = "pretty_graphql"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea8c38ecedb3d28a998ea783469a78587f5f984d61226cf071f6979861e9e6a9"
dependencies = [
"apollo-parser",
"memchr",
"rowan",
"tiny_pretty",
]
[[package]]
name = "proc-macro-crate"
version = "1.3.1"
@@ -5742,7 +5713,7 @@ dependencies = [
"pin-project-lite",
"quinn-proto",
"quinn-udp",
"rustc-hash 2.1.1",
"rustc-hash",
"rustls",
"socket2 0.5.10",
"thiserror 2.0.17",
@@ -5762,7 +5733,7 @@ dependencies = [
"lru-slab",
"rand 0.9.1",
"ring",
"rustc-hash 2.1.1",
"rustc-hash",
"rustls",
"rustls-pki-types",
"slab",
@@ -6256,7 +6227,7 @@ dependencies = [
"rolldown_tracing",
"rolldown_utils",
"rolldown_watcher",
"rustc-hash 2.1.1",
"rustc-hash",
"serde",
"serde_json",
"string_wizard",
@@ -6346,7 +6317,7 @@ dependencies = [
"rolldown_sourcemap",
"rolldown_std_utils",
"rolldown_utils",
"rustc-hash 2.1.1",
"rustc-hash",
"serde",
"serde_json",
"simdutf8",
@@ -6364,7 +6335,7 @@ dependencies = [
"blake3",
"dashmap",
"rolldown_debug_action",
"rustc-hash 2.1.1",
"rustc-hash",
"serde",
"serde_json",
"tracing",
@@ -6421,7 +6392,7 @@ dependencies = [
"rolldown-ariadne",
"rolldown_utils",
"ropey",
"rustc-hash 2.1.1",
"rustc-hash",
"sugar_path",
]
@@ -6455,7 +6426,7 @@ dependencies = [
"rolldown_resolver",
"rolldown_sourcemap",
"rolldown_utils",
"rustc-hash 2.1.1",
"rustc-hash",
"serde",
"serde_json",
"string_wizard",
@@ -6475,7 +6446,7 @@ dependencies = [
"rolldown_common",
"rolldown_plugin",
"rolldown_utils",
"rustc-hash 2.1.1",
"rustc-hash",
"serde_json",
"xxhash-rust",
]
@@ -6546,7 +6517,7 @@ dependencies = [
"oxc",
"oxc_sourcemap",
"rolldown_utils",
"rustc-hash 2.1.1",
"rustc-hash",
]
[[package]]
@@ -6598,7 +6569,7 @@ dependencies = [
"regex 1.11.1",
"regress",
"rolldown_std_utils",
"rustc-hash 2.1.1",
"rustc-hash",
"serde_json",
"simdutf8",
"sugar_path",
@@ -6628,18 +6599,6 @@ dependencies = [
"str_indices",
]
[[package]]
name = "rowan"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "417a3a9f582e349834051b8a10c8d71ca88da4211e4093528e36b9845f6b5f21"
dependencies = [
"countme",
"hashbrown 0.14.5",
"rustc-hash 1.1.0",
"text-size",
]
[[package]]
name = "rusqlite"
version = "0.32.1"
@@ -6682,12 +6641,6 @@ dependencies = [
"serde_json",
]
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustc-hash"
version = "2.1.1"
@@ -7500,7 +7453,7 @@ dependencies = [
"memchr",
"oxc_index",
"oxc_sourcemap",
"rustc-hash 2.1.1",
"rustc-hash",
"serde",
]
@@ -8201,12 +8154,6 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683"
[[package]]
name = "text-size"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233"
[[package]]
name = "textwrap"
version = "0.16.2"
@@ -8329,12 +8276,6 @@ dependencies = [
"crunchy",
]
[[package]]
name = "tiny_pretty"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "650d82e943da333637be9f1567d33d605e76810a26464edfd7ae74f7ef181e95"
[[package]]
name = "tinystr"
version = "0.8.1"
@@ -10257,7 +10198,6 @@ dependencies = [
"md5 0.8.0",
"mime_guess",
"openssl-sys",
"pretty_graphql",
"r2d2",
"r2d2_sqlite",
"rand 0.9.1",
@@ -10459,7 +10399,6 @@ dependencies = [
"urlencoding",
"yaak-common",
"yaak-models",
"yaak-templates",
"yaak-tls",
"zstd",
]

View File

@@ -1,30 +1,30 @@
[workspace]
resolver = "2"
members = [
"crates/yaak",
# Shared crates (no Tauri dependency)
"crates/yaak-core",
"crates/yaak-common",
"crates/yaak-crypto",
"crates/yaak-git",
"crates/yaak-grpc",
"crates/yaak-http",
"crates/yaak-models",
"crates/yaak-plugins",
"crates/yaak-sse",
"crates/yaak-sync",
"crates/yaak-templates",
"crates/yaak-tls",
"crates/yaak-ws",
"crates/yaak-api",
# CLI crates
"crates-cli/yaak-cli",
# Tauri-specific crates
"crates-tauri/yaak-app",
"crates-tauri/yaak-fonts",
"crates-tauri/yaak-license",
"crates-tauri/yaak-mac-window",
"crates-tauri/yaak-tauri-utils",
"crates/yaak",
# Shared crates (no Tauri dependency)
"crates/yaak-core",
"crates/yaak-common",
"crates/yaak-crypto",
"crates/yaak-git",
"crates/yaak-grpc",
"crates/yaak-http",
"crates/yaak-models",
"crates/yaak-plugins",
"crates/yaak-sse",
"crates/yaak-sync",
"crates/yaak-templates",
"crates/yaak-tls",
"crates/yaak-ws",
"crates/yaak-api",
# CLI crates
"crates-cli/yaak-cli",
# Tauri-specific crates
"crates-tauri/yaak-app",
"crates-tauri/yaak-fonts",
"crates-tauri/yaak-license",
"crates-tauri/yaak-mac-window",
"crates-tauri/yaak-tauri-utils",
]
[workspace.dependencies]

View File

@@ -1,26 +1,24 @@
# Developer Setup
Yaak is a combined Node.js and Rust monorepo. It is a [Tauri](https://tauri.app) project, so
Yaak is a combined Node.js and Rust monorepo. It is a [Tauri](https://tauri.app) project, so
uses Rust and HTML/CSS/JS for the main application but there is also a plugin system powered
by a Node.js sidecar that communicates to the app over gRPC.
Because of the moving parts, there are a few setup steps required before development can
Because of the moving parts, there are a few setup steps required before development can
begin.
## Prerequisites
Make sure you have the following tools installed:
- [Node.js](https://nodejs.org/en/download/package-manager) (v24+)
- [Node.js](https://nodejs.org/en/download/package-manager)
- [Rust](https://www.rust-lang.org/tools/install)
- [Vite+](https://vite.dev/guide/vite-plus) (`vp` CLI)
Check the installations with the following commands:
```shell
node -v
npm -v
vp --version
rustc --version
```
@@ -47,12 +45,12 @@ npm start
## SQLite Migrations
New migrations can be created from the `src-tauri/` directory:
```shell
npm run migration
```
Rerun the app to apply the migrations.
Rerun the app to apply the migrations.
_Note: For safety, development builds use a separate database location from production builds._
@@ -63,9 +61,9 @@ _Note: For safety, development builds use a separate database location from prod
lezer-generator components/core/Editor/<LANG>/<LANG>.grammar > components/core/Editor/<LANG>/<LANG>.ts
```
## Linting and Formatting
## Linting & Formatting
This repo uses [Vite+](https://vite.dev/guide/vite-plus) for linting (oxlint) and formatting (oxfmt).
This repo uses Biome for linting and formatting (replacing ESLint + Prettier).
- Lint the entire repo:
@@ -73,6 +71,12 @@ This repo uses [Vite+](https://vite.dev/guide/vite-plus) for linting (oxlint) an
npm run lint
```
- Auto-fix lint issues where possible:
```sh
npm run lint:fix
```
- Format code:
```sh
@@ -80,7 +84,5 @@ npm run format
```
Notes:
- A pre-commit hook runs `vp lint` automatically on commit.
- Some workspace packages also run `tsc --noEmit` for type-checking.
- VS Code users should install the recommended extensions for format-on-save support.
- Many workspace packages also expose the same scripts (`lint`, `lint:fix`, and `format`).
- TypeScript type-checking still runs separately via `tsc --noEmit` in relevant packages.

View File

@@ -16,6 +16,8 @@
</p>
<br>
<p align="center">
<!-- sponsors-premium --><a href="https://github.com/MVST-Solutions"><img src="https:&#x2F;&#x2F;github.com&#x2F;MVST-Solutions.png" width="80px" alt="User avatar: MVST-Solutions" /></a>&nbsp;&nbsp;<a href="https://github.com/dharsanb"><img src="https:&#x2F;&#x2F;github.com&#x2F;dharsanb.png" width="80px" alt="User avatar: dharsanb" /></a>&nbsp;&nbsp;<a href="https://github.com/railwayapp"><img src="https:&#x2F;&#x2F;github.com&#x2F;railwayapp.png" width="80px" alt="User avatar: railwayapp" /></a>&nbsp;&nbsp;<a href="https://github.com/caseyamcl"><img src="https:&#x2F;&#x2F;github.com&#x2F;caseyamcl.png" width="80px" alt="User avatar: caseyamcl" /></a>&nbsp;&nbsp;<a href="https://github.com/bytebase"><img src="https:&#x2F;&#x2F;github.com&#x2F;bytebase.png" width="80px" alt="User avatar: bytebase" /></a>&nbsp;&nbsp;<a href="https://github.com/"><img src="https:&#x2F;&#x2F;raw.githubusercontent.com&#x2F;JamesIves&#x2F;github-sponsors-readme-action&#x2F;dev&#x2F;.github&#x2F;assets&#x2F;placeholder.png" width="80px" alt="User avatar: " /></a>&nbsp;&nbsp;<!-- sponsors-premium -->
</p>
@@ -25,10 +27,12 @@
![Yaak API Client](https://yaak.app/static/screenshot.png)
## Features
Yaak is an offline-first API client designed to stay out of your way while giving you everything you need when you need it.
Built with [Tauri](https://tauri.app), Rust, and React, its fast, lightweight, and private. No telemetry, no VC funding, and no cloud lock-in.
Yaak is an offline-first API client designed to stay out of your way while giving you everything you need when you need it.
Built with [Tauri](https://tauri.app), Rust, and React, its fast, lightweight, and private. No telemetry, no VC funding, and no cloud lock-in.
### 🌐 Work with any API
@@ -37,23 +41,21 @@ Built with [Tauri](https://tauri.app), Rust, and React, its fast, lightweight
- Filter and inspect responses with JSONPath or XPath.
### 🔐 Stay secure
- Use OAuth 2.0, JWT, Basic Auth, or custom plugins for authentication.
- Secure sensitive values with encrypted secrets.
- Secure sensitive values with encrypted secrets.
- Store secrets in your OS keychain.
### ☁️ Organize & collaborate
- Group requests into workspaces and nested folders.
- Use environment variables to switch between dev, staging, and prod.
- Mirror workspaces to your filesystem for versioning in Git or syncing with Dropbox.
### 🧩 Extend & customize
- Insert dynamic values like UUIDs or timestamps with template tags.
- Pick from built-in themes or build your own.
- Create plugins to extend authentication, template tags, or the UI.
## Contribution Policy
> [!IMPORTANT]

54
biome.json Normal file
View File

@@ -0,0 +1,54 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.11/schema.json",
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"a11y": {
"useKeyWithClickEvents": "off"
}
}
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 100,
"bracketSpacing": true
},
"css": {
"parser": {
"tailwindDirectives": true
},
"linter": {
"enabled": false
}
},
"javascript": {
"formatter": {
"quoteStyle": "single",
"jsxQuoteStyle": "double",
"trailingCommas": "all",
"semicolons": "always"
}
},
"files": {
"includes": [
"**",
"!**/node_modules",
"!**/dist",
"!**/build",
"!target",
"!scripts",
"!crates",
"!crates-tauri",
"!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"
]
}
}

View File

@@ -29,14 +29,7 @@ schemars = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
sha2 = { workspace = true }
tokio = { workspace = true, features = [
"rt-multi-thread",
"macros",
"io-util",
"net",
"signal",
"time",
] }
tokio = { workspace = true, features = ["rt-multi-thread", "macros", "io-util", "net", "signal", "time"] }
walkdir = "2"
webbrowser = "1"
zip = "4"

View File

@@ -1,6 +1,6 @@
# Yaak CLI
The `yaak` CLI for publishing plugins and creating/updating/sending requests.
The `yaak` CLI for publishing plugins and creating/updating/sending requests.
## Installation
@@ -24,8 +24,8 @@ Use the `yaak` CLI with agents like Claude or Codex to do useful things for you.
Here are some example prompts:
```text
Scan my API routes and create a workspace (using yaak cli) with
all the requests needed for me to do manual testing?
Scan my API routes and create a workspace (using yaak cli) with
all the requests needed for me to do manual testing?
```
```text

View File

@@ -36,71 +36,72 @@ pub struct CliContext {
}
impl CliContext {
pub fn new(data_dir: PathBuf, app_id: &str) -> 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,
pub async fn new(
data_dir: PathBuf,
query_manager: QueryManager,
blob_manager: BlobManager,
encryption_manager: Arc<EncryptionManager>,
with_plugins: bool,
execution_context: CliExecutionContext,
) -> Self {
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!("Error: Failed to initialize database: {err}");
std::process::exit(1);
eprintln!("Warning: Failed to initialize plugins: {err}");
None
}
};
let encryption_manager = Arc::new(EncryptionManager::new(query_manager.clone(), app_id));
}
} else {
None
};
let plugin_event_bridge = if let Some(plugin_manager) = &plugin_manager {
Some(
CliPluginEventBridge::start(
plugin_manager.clone(),
query_manager.clone(),
blob_manager.clone(),
encryption_manager.clone(),
data_dir.clone(),
execution_context.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),
}
}

View File

@@ -10,8 +10,10 @@ mod version_check;
use clap::Parser;
use cli::{Cli, Commands, PluginCommands, RequestCommands};
use context::{CliContext, CliExecutionContext};
use std::path::PathBuf;
use std::sync::Arc;
use yaak_crypto::manager::EncryptionManager;
use yaak_models::queries::any_request::AnyRequest;
use yaak_models::query_manager::QueryManager;
#[tokio::main]
async fn main() {
@@ -31,10 +33,23 @@ async fn main() {
let app_id = if cfg!(debug_assertions) { "app.yaak.desktop.dev" } else { "app.yaak.desktop" };
let data_dir = data_dir.unwrap_or_else(|| resolve_data_dir(app_id));
let data_dir = data_dir.unwrap_or_else(|| {
dirs::data_dir().expect("Could not determine data directory").join(app_id)
});
version_check::maybe_check_for_updates().await;
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 exit_code = match command {
Commands::Auth(args) => commands::auth::run(args).await,
Commands::Plugin(args) => match args.command {
@@ -43,8 +58,18 @@ async fn main() {
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 query_manager = query_manager.clone();
let blob_manager = blob_manager.clone();
let execution_context = CliExecutionContext::default();
let context = CliContext::new(
data_dir.clone(),
query_manager.clone(),
blob_manager,
Arc::new(EncryptionManager::new(query_manager, app_id)),
true,
execution_context,
)
.await;
let exit_code = commands::plugin::run_install(&context, install_args).await;
context.shutdown().await;
exit_code
@@ -55,15 +80,27 @@ async fn main() {
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,
let query_manager = query_manager.clone();
let blob_manager = blob_manager.clone();
let execution_context_result = resolve_send_execution_context(
&query_manager,
&args.id,
environment.as_deref(),
cookie_jar.as_deref(),
) {
);
match execution_context_result {
Ok(execution_context) => {
context.init_plugins(execution_context).await;
let context = CliContext::new(
data_dir.clone(),
query_manager.clone(),
blob_manager,
Arc::new(EncryptionManager::new(query_manager, app_id)),
true,
execution_context,
)
.await;
let exit_code = commands::send::run(
&context,
args,
@@ -82,22 +119,48 @@ async fn main() {
}
}
Commands::CookieJar(args) => {
let context = CliContext::new(data_dir.clone(), app_id);
let query_manager = query_manager.clone();
let blob_manager = blob_manager.clone();
let execution_context = CliExecutionContext::default();
let context = CliContext::new(
data_dir.clone(),
query_manager.clone(),
blob_manager,
Arc::new(EncryptionManager::new(query_manager, app_id)),
false,
execution_context,
)
.await;
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 query_manager = query_manager.clone();
let blob_manager = blob_manager.clone();
let execution_context = CliExecutionContext::default();
let context = CliContext::new(
data_dir.clone(),
query_manager.clone(),
blob_manager,
Arc::new(EncryptionManager::new(query_manager, app_id)),
false,
execution_context,
)
.await;
let exit_code = commands::workspace::run(&context, args);
context.shutdown().await;
exit_code
}
Commands::Request(args) => {
let mut context = CliContext::new(data_dir.clone(), app_id);
let query_manager = query_manager.clone();
let blob_manager = blob_manager.clone();
let execution_context_result = match &args.command {
RequestCommands::Send { request_id } => resolve_request_execution_context(
&context,
&query_manager,
request_id,
environment.as_deref(),
cookie_jar.as_deref(),
@@ -110,9 +173,16 @@ async fn main() {
&args.command,
RequestCommands::Send { .. } | RequestCommands::Schema { .. }
);
if with_plugins {
context.init_plugins(execution_context).await;
}
let context = CliContext::new(
data_dir.clone(),
query_manager.clone(),
blob_manager,
Arc::new(EncryptionManager::new(query_manager, app_id)),
with_plugins,
execution_context,
)
.await;
let exit_code = commands::request::run(
&context,
args,
@@ -131,13 +201,37 @@ async fn main() {
}
}
Commands::Folder(args) => {
let context = CliContext::new(data_dir.clone(), app_id);
let query_manager = query_manager.clone();
let blob_manager = blob_manager.clone();
let execution_context = CliExecutionContext::default();
let context = CliContext::new(
data_dir.clone(),
query_manager.clone(),
blob_manager,
Arc::new(EncryptionManager::new(query_manager, app_id)),
false,
execution_context,
)
.await;
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 query_manager = query_manager.clone();
let blob_manager = blob_manager.clone();
let execution_context = CliExecutionContext::default();
let context = CliContext::new(
data_dir.clone(),
query_manager.clone(),
blob_manager,
Arc::new(EncryptionManager::new(query_manager, app_id)),
false,
execution_context,
)
.await;
let exit_code = commands::environment::run(&context, args);
context.shutdown().await;
exit_code
@@ -150,18 +244,19 @@ async fn main() {
}
fn resolve_send_execution_context(
context: &CliContext,
query_manager: &QueryManager,
id: &str,
environment: Option<&str>,
explicit_cookie_jar_id: Option<&str>,
) -> Result<CliExecutionContext, String> {
if let Ok(request) = context.db().get_any_request(id) {
if let Ok(request) = query_manager.connect().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)?;
let cookie_jar_id =
resolve_cookie_jar_id(query_manager, &workspace_id, explicit_cookie_jar_id)?;
return Ok(CliExecutionContext {
request_id,
workspace_id: Some(workspace_id),
@@ -170,9 +265,9 @@ fn resolve_send_execution_context(
});
}
if let Ok(folder) = context.db().get_folder(id) {
if let Ok(folder) = query_manager.connect().get_folder(id) {
let cookie_jar_id =
resolve_cookie_jar_id(context, &folder.workspace_id, explicit_cookie_jar_id)?;
resolve_cookie_jar_id(query_manager, &folder.workspace_id, explicit_cookie_jar_id)?;
return Ok(CliExecutionContext {
request_id: None,
workspace_id: Some(folder.workspace_id),
@@ -181,8 +276,9 @@ fn resolve_send_execution_context(
});
}
if let Ok(workspace) = context.db().get_workspace(id) {
let cookie_jar_id = resolve_cookie_jar_id(context, &workspace.id, explicit_cookie_jar_id)?;
if let Ok(workspace) = query_manager.connect().get_workspace(id) {
let cookie_jar_id =
resolve_cookie_jar_id(query_manager, &workspace.id, explicit_cookie_jar_id)?;
return Ok(CliExecutionContext {
request_id: None,
workspace_id: Some(workspace.id),
@@ -195,13 +291,13 @@ fn resolve_send_execution_context(
}
fn resolve_request_execution_context(
context: &CliContext,
query_manager: &QueryManager,
request_id: &str,
environment: Option<&str>,
explicit_cookie_jar_id: Option<&str>,
) -> Result<CliExecutionContext, String> {
let request = context
.db()
let request = query_manager
.connect()
.get_any_request(request_id)
.map_err(|e| format!("Failed to get request: {e}"))?;
@@ -210,7 +306,8 @@ fn resolve_request_execution_context(
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)?;
let cookie_jar_id =
resolve_cookie_jar_id(query_manager, &workspace_id, explicit_cookie_jar_id)?;
Ok(CliExecutionContext {
request_id: Some(request_id.to_string()),
@@ -221,7 +318,7 @@ fn resolve_request_execution_context(
}
fn resolve_cookie_jar_id(
context: &CliContext,
query_manager: &QueryManager,
workspace_id: &str,
explicit_cookie_jar_id: Option<&str>,
) -> Result<Option<String>, String> {
@@ -229,8 +326,8 @@ fn resolve_cookie_jar_id(
return Ok(Some(cookie_jar_id.to_string()));
}
let default_cookie_jar = context
.db()
let default_cookie_jar = query_manager
.connect()
.list_cookie_jars(workspace_id)
.map_err(|e| format!("Failed to list cookie jars: {e}"))?
.into_iter()
@@ -238,46 +335,3 @@ fn resolve_cookie_jar_id(
.map(|jar| jar.id);
Ok(default_cookie_jar)
}
fn resolve_data_dir(app_id: &str) -> PathBuf {
if let Some(dir) = wsl_data_dir(app_id) {
return dir;
}
dirs::data_dir().expect("Could not determine data directory").join(app_id)
}
/// Detect WSL and resolve the Windows AppData\Roaming path for the Yaak data directory.
fn wsl_data_dir(app_id: &str) -> Option<PathBuf> {
if !cfg!(target_os = "linux") {
return None;
}
let proc_version = std::fs::read_to_string("/proc/version").ok()?;
let is_wsl = proc_version.to_lowercase().contains("microsoft");
if !is_wsl {
return None;
}
// We're in WSL, so try to resolve the Yaak app's data directory in Windows
// Get the Windows %APPDATA% path via cmd.exe
let appdata_output =
std::process::Command::new("cmd.exe").args(["/C", "echo", "%APPDATA%"]).output().ok()?;
let win_path = String::from_utf8(appdata_output.stdout).ok()?.trim().to_string();
if win_path.is_empty() || win_path == "%APPDATA%" {
return None;
}
// Convert Windows path to WSL path using wslpath (handles custom mount points)
let wslpath_output = std::process::Command::new("wslpath").arg(&win_path).output().ok()?;
let wsl_appdata = String::from_utf8(wslpath_output.stdout).ok()?.trim().to_string();
if wsl_appdata.is_empty() {
return None;
}
let wsl_path = PathBuf::from(wsl_appdata).join(app_id);
if wsl_path.exists() { Some(wsl_path) } else { None }
}

View File

@@ -3,7 +3,7 @@ use arboard::Clipboard;
use console::Term;
use inquire::{Confirm, Editor, Password, PasswordDisplayMode, Select, Text};
use serde_json::Value;
use std::collections::HashMap;
use std::collections::{BTreeMap, HashMap};
use std::io::IsTerminal;
use std::path::PathBuf;
use std::sync::Arc;
@@ -11,12 +11,11 @@ use tokio::task::JoinHandle;
use yaak::plugin_events::{
GroupedPluginEvent, HostRequest, SharedPluginEventContext, handle_shared_plugin_event,
};
use yaak::render::{render_grpc_request, render_http_request};
use yaak::render::render_http_request;
use yaak::send::{SendHttpRequestWithPluginsParams, send_http_request_with_plugins};
use yaak_crypto::manager::EncryptionManager;
use yaak_models::blob_manager::BlobManager;
use yaak_models::models::Environment;
use yaak_models::queries::any_request::AnyRequest;
use yaak_models::models::{Environment, GrpcRequest, HttpRequestHeader};
use yaak_models::query_manager::QueryManager;
use yaak_models::render::make_vars_hashmap;
use yaak_models::util::UpdateSource;
@@ -29,7 +28,7 @@ use yaak_plugins::events::{
};
use yaak_plugins::manager::PluginManager;
use yaak_plugins::template_callback::PluginTemplateCallback;
use yaak_templates::{RenderOptions, TemplateCallback, render_json_value_raw};
use yaak_templates::{RenderOptions, TemplateCallback, parse_and_render, render_json_value_raw};
pub struct CliPluginEventBridge {
rx_id: String,
@@ -269,7 +268,7 @@ async fn build_plugin_reply(
);
let render_options = RenderOptions::throw();
match render_grpc_request(
match render_grpc_request_for_cli(
&grpc_request,
environment_chain,
&template_callback,
@@ -362,19 +361,10 @@ async fn build_plugin_reply(
..event.context.clone()
};
let folder_id = execution_context.request_id.as_ref().and_then(|rid| {
match host_context.query_manager.connect().get_any_request(rid) {
Ok(AnyRequest::HttpRequest(r)) => r.folder_id,
Ok(AnyRequest::GrpcRequest(r)) => r.folder_id,
Ok(AnyRequest::WebsocketRequest(r)) => r.folder_id,
Err(_) => None,
}
});
let environment_chain =
match host_context.query_manager.connect().resolve_environments(
&workspace_id,
folder_id.as_deref(),
None,
execution_context.environment_id.as_deref(),
) {
Ok(chain) => chain,
@@ -532,6 +522,60 @@ async fn render_json_value_for_cli<T: TemplateCallback>(
render_json_value_raw(value, vars, cb, opt).await
}
async fn render_grpc_request_for_cli<T: TemplateCallback>(
grpc_request: &GrpcRequest,
environment_chain: Vec<Environment>,
cb: &T,
opt: &RenderOptions,
) -> yaak_templates::error::Result<GrpcRequest> {
let vars = &make_vars_hashmap(environment_chain);
let mut metadata = Vec::new();
for p in grpc_request.metadata.clone() {
if !p.enabled {
continue;
}
metadata.push(HttpRequestHeader {
enabled: p.enabled,
name: parse_and_render(p.name.as_str(), vars, cb, opt).await?,
value: parse_and_render(p.value.as_str(), vars, cb, opt).await?,
id: p.id,
})
}
let authentication = {
let mut disabled = false;
let mut auth = BTreeMap::new();
match grpc_request.authentication.get("disabled") {
Some(Value::Bool(true)) => {
disabled = true;
}
Some(Value::String(tmpl)) => {
disabled = parse_and_render(tmpl.as_str(), vars, cb, opt)
.await
.unwrap_or_default()
.is_empty();
}
_ => {}
}
if disabled {
auth.insert("disabled".to_string(), Value::Bool(true));
} else {
for (k, v) in grpc_request.authentication.clone() {
if k == "disabled" {
auth.insert(k, Value::Bool(false));
} else {
auth.insert(k, render_json_value_raw(v, vars, cb, opt).await?);
}
}
}
auth
};
let url = parse_and_render(grpc_request.url.as_str(), vars, cb, opt).await?;
Ok(GrpcRequest { url, metadata, authentication, ..grpc_request.to_owned() })
}
fn parse_cookie_name_value(raw_cookie: &str) -> Option<(String, String)> {
let first_part = raw_cookie.split(';').next()?.trim();

View File

@@ -30,21 +30,11 @@ eventsource-client = { git = "https://github.com/yaakapp/rust-eventsource-client
http = { version = "1.2.0", default-features = false }
log = { workspace = true }
md5 = "0.8.0"
pretty_graphql = "0.2"
r2d2 = "0.8.10"
r2d2_sqlite = "0.25.0"
mime_guess = "2.0.5"
rand = "0.9.0"
reqwest = { workspace = true, features = [
"multipart",
"gzip",
"brotli",
"deflate",
"json",
"rustls-tls-manual-roots-no-provider",
"socks",
"http2",
] }
reqwest = { workspace = true, features = ["multipart", "gzip", "brotli", "deflate", "json", "rustls-tls-manual-roots-no-provider", "socks", "http2"] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true, features = ["raw_value"] }
tauri = { workspace = true, features = ["devtools", "protocol-asset"] }

View File

@@ -1,7 +1,9 @@
{
"identifier": "default",
"description": "Default capabilities for all build variants",
"windows": ["*"],
"windows": [
"*"
],
"permissions": [
"core:app:allow-identifier",
"core:event:allow-emit",

View File

@@ -1,6 +1,6 @@
{
"name": "@yaakapp-internal/tauri",
"version": "1.0.0",
"private": true,
"version": "1.0.0",
"main": "bindings/index.ts"
}

View File

@@ -34,7 +34,6 @@ use tokio::time;
use yaak_common::command::new_checked_command;
use yaak_crypto::manager::EncryptionManager;
use yaak_grpc::manager::{GrpcConfig, GrpcHandle};
use yaak_templates::strip_json_comments::strip_json_comments;
use yaak_grpc::{Code, ServiceDefinition, serialize_message};
use yaak_mac_window::AppHandleMacWindowExt;
use yaak_models::models::{
@@ -434,7 +433,6 @@ async fn cmd_grpc_go<R: Runtime>(
result.expect("Failed to render template")
})
});
let msg = strip_json_comments(&msg);
in_msg_tx.try_send(msg.clone()).unwrap();
}
Ok(IncomingMsg::Commit) => {
@@ -470,7 +468,6 @@ async fn cmd_grpc_go<R: Runtime>(
&RenderOptions { error_behavior: RenderErrorBehavior::Throw },
)
.await?;
let msg = strip_json_comments(&msg);
app_handle.db().upsert_grpc_event(
&GrpcEvent {
@@ -872,14 +869,6 @@ async fn cmd_format_json(text: &str) -> YaakResult<String> {
Ok(format_json(text, " "))
}
#[tauri::command]
async fn cmd_format_graphql(text: &str) -> YaakResult<String> {
match pretty_graphql::format_text(text, &Default::default()) {
Ok(formatted) => Ok(formatted),
Err(_) => Ok(text.to_string()),
}
}
#[tauri::command]
async fn cmd_http_response_body<R: Runtime>(
window: WebviewWindow<R>,
@@ -1383,12 +1372,13 @@ async fn cmd_reload_plugins<R: Runtime>(
app_handle: AppHandle<R>,
window: WebviewWindow<R>,
plugin_manager: State<'_, PluginManager>,
) -> YaakResult<Vec<(String, String)>> {
) -> YaakResult<()> {
let plugins = app_handle.db().list_plugins()?;
let plugin_context =
PluginContext::new(Some(window.label().to_string()), window.workspace_id());
let errors = plugin_manager.initialize_all_plugins(plugins, &plugin_context).await;
Ok(errors)
let _errors = plugin_manager.initialize_all_plugins(plugins, &plugin_context).await;
// Note: errors are returned but we don't show toasts here since this is a manual reload
Ok(())
}
#[tauri::command]
@@ -1648,7 +1638,6 @@ pub fn run() {
cmd_http_request_body,
cmd_http_response_body,
cmd_format_json,
cmd_format_graphql,
cmd_get_http_authentication_summaries,
cmd_get_http_authentication_config,
cmd_get_sse_events,
@@ -1730,7 +1719,6 @@ pub fn run() {
git_ext::cmd_git_rm_remote,
//
// Plugin commands
plugins_ext::cmd_plugin_init_errors,
plugins_ext::cmd_plugins_install_from_directory,
plugins_ext::cmd_plugins_search,
plugins_ext::cmd_plugins_install,

View File

@@ -198,13 +198,6 @@ pub async fn cmd_plugins_uninstall<R: Runtime>(
Ok(delete_and_uninstall(plugin_manager, &query_manager, &plugin_context, plugin_id).await?)
}
#[command]
pub async fn cmd_plugin_init_errors(
plugin_manager: State<'_, PluginManager>,
) -> Result<Vec<(String, String)>> {
Ok(plugin_manager.take_init_errors().await)
}
#[command]
pub async fn cmd_plugins_updates<R: Runtime>(
app_handle: AppHandle<R>,
@@ -313,7 +306,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
dev_mode,
)
.await
.expect("Failed to start plugin runtime");
.expect("Failed to initialize plugins");
app_handle_clone.manage(manager);
});

View File

@@ -1,6 +1,8 @@
use log::info;
use serde_json::Value;
pub use yaak::render::{render_grpc_request, render_http_request};
use yaak_models::models::Environment;
use std::collections::BTreeMap;
pub use yaak::render::render_http_request;
use yaak_models::models::{Environment, GrpcRequest, HttpRequestHeader};
use yaak_models::render::make_vars_hashmap;
use yaak_templates::{RenderOptions, TemplateCallback, parse_and_render, render_json_value_raw};
@@ -23,3 +25,61 @@ pub async fn render_json_value<T: TemplateCallback>(
let vars = &make_vars_hashmap(environment_chain);
render_json_value_raw(value, vars, cb, opt).await
}
pub async fn render_grpc_request<T: TemplateCallback>(
r: &GrpcRequest,
environment_chain: Vec<Environment>,
cb: &T,
opt: &RenderOptions,
) -> yaak_templates::error::Result<GrpcRequest> {
let vars = &make_vars_hashmap(environment_chain);
let mut metadata = Vec::new();
for p in r.metadata.clone() {
if !p.enabled {
continue;
}
metadata.push(HttpRequestHeader {
enabled: p.enabled,
name: parse_and_render(p.name.as_str(), vars, cb, &opt).await?,
value: parse_and_render(p.value.as_str(), vars, cb, &opt).await?,
id: p.id,
})
}
let authentication = {
let mut disabled = false;
let mut auth = BTreeMap::new();
match r.authentication.get("disabled") {
Some(Value::Bool(true)) => {
disabled = true;
}
Some(Value::String(tmpl)) => {
disabled = parse_and_render(tmpl.as_str(), vars, cb, &opt)
.await
.unwrap_or_default()
.is_empty();
info!(
"Rendering authentication.disabled as a template: {disabled} from \"{tmpl}\""
);
}
_ => {}
}
if disabled {
auth.insert("disabled".to_string(), Value::Bool(true));
} else {
for (k, v) in r.authentication.clone() {
if k == "disabled" {
auth.insert(k, Value::Bool(false));
} else {
auth.insert(k, render_json_value_raw(v, vars, cb, &opt).await?);
}
}
}
auth
};
let url = parse_and_render(r.url.as_str(), vars, cb, &opt).await?;
Ok(GrpcRequest { url, metadata, authentication, ..r.to_owned() })
}

View File

@@ -24,7 +24,6 @@ use yaak_models::util::UpdateSource;
use yaak_plugins::events::{CallHttpAuthenticationRequest, HttpHeader, RenderPurpose};
use yaak_plugins::manager::PluginManager;
use yaak_plugins::template_callback::PluginTemplateCallback;
use yaak_templates::strip_json_comments::maybe_strip_json_comments;
use yaak_templates::{RenderErrorBehavior, RenderOptions};
use yaak_tls::find_client_certificate;
use yaak_ws::{WebsocketManager, render_websocket_request};
@@ -73,10 +72,8 @@ pub async fn cmd_ws_send<R: Runtime>(
)
.await?;
let message = maybe_strip_json_comments(&request.message);
let mut ws_manager = ws_manager.lock().await;
ws_manager.send(&connection.id, Message::Text(message.clone().into())).await?;
ws_manager.send(&connection.id, Message::Text(request.message.clone().into())).await?;
app_handle.db().upsert_websocket_event(
&WebsocketEvent {
@@ -85,7 +82,7 @@ pub async fn cmd_ws_send<R: Runtime>(
workspace_id: connection.workspace_id.clone(),
is_server: false,
message_type: WebsocketEventType::Text,
message: message.into(),
message: request.message.into(),
..Default::default()
},
&UpdateSource::from_window_label(window.label()),

View File

@@ -14,7 +14,10 @@
"assetProtocol": {
"enable": true,
"scope": {
"allow": ["$APPDATA/responses/*", "$RESOURCE/static/*"]
"allow": [
"$APPDATA/responses/*",
"$RESOURCE/static/*"
]
}
}
}
@@ -22,7 +25,9 @@
"plugins": {
"deep-link": {
"desktop": {
"schemes": ["yaak"]
"schemes": [
"yaak"
]
}
}
},

View File

@@ -16,7 +16,9 @@
},
"plugins": {
"updater": {
"endpoints": ["https://update.yaak.app/check/{{target}}/{{arch}}/{{current_version}}"],
"endpoints": [
"https://update.yaak.app/check/{{target}}/{{arch}}/{{current_version}}"
],
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEVGRkFGMjQxRUNEOTQ3MzAKUldRd1I5bnNRZkw2NzRtMnRlWTN3R24xYUR3aGRsUjJzWGwvdHdEcGljb3ZJMUNlMjFsaHlqVU4K"
}
},

View File

@@ -1,14 +1,14 @@
import { useQuery } from "@tanstack/react-query";
import { invoke } from "@tauri-apps/api/core";
import { Fonts } from "./bindings/gen_fonts";
import { useQuery } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api/core';
import { Fonts } from './bindings/gen_fonts';
export async function listFonts() {
return invoke<Fonts>("plugin:yaak-fonts|list", {});
return invoke<Fonts>('plugin:yaak-fonts|list', {});
}
export function useFonts() {
return useQuery({
queryKey: ["list_fonts"],
queryKey: ['list_fonts'],
queryFn: () => listFonts(),
});
}

View File

@@ -1,6 +1,6 @@
{
"name": "@yaakapp-internal/fonts",
"version": "1.0.0",
"private": true,
"version": "1.0.0",
"main": "index.ts"
}

View File

@@ -1,35 +1,35 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { appInfo } from "@yaakapp/app/lib/appInfo";
import { useEffect } from "react";
import { LicenseCheckStatus } from "./bindings/license";
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';
import { appInfo } from '@yaakapp/app/lib/appInfo';
import { useEffect } from 'react';
import { LicenseCheckStatus } from './bindings/license';
export * from "./bindings/license";
export * from './bindings/license';
const CHECK_QUERY_KEY = ["license.check"];
const CHECK_QUERY_KEY = ['license.check'];
export function useLicense() {
const queryClient = useQueryClient();
const activate = useMutation<void, string, { licenseKey: string }>({
mutationKey: ["license.activate"],
mutationFn: (payload) => invoke("plugin:yaak-license|activate", payload),
mutationKey: ['license.activate'],
mutationFn: (payload) => invoke('plugin:yaak-license|activate', payload),
onSuccess: () => queryClient.invalidateQueries({ queryKey: CHECK_QUERY_KEY }),
});
const deactivate = useMutation<void, string, void>({
mutationKey: ["license.deactivate"],
mutationFn: () => invoke("plugin:yaak-license|deactivate"),
mutationKey: ['license.deactivate'],
mutationFn: () => invoke('plugin:yaak-license|deactivate'),
onSuccess: () => queryClient.invalidateQueries({ queryKey: CHECK_QUERY_KEY }),
});
// Check the license again after a license is activated
useEffect(() => {
const unlisten = listen("license-activated", async () => {
const unlisten = listen('license-activated', async () => {
await queryClient.invalidateQueries({ queryKey: CHECK_QUERY_KEY });
});
return () => {
void unlisten.then((fn) => fn());
unlisten.then((fn) => fn());
};
}, []);
@@ -41,7 +41,7 @@ export function useLicense() {
if (!appInfo.featureLicense) {
return null;
}
return invoke<LicenseCheckStatus>("plugin:yaak-license|check");
return invoke<LicenseCheckStatus>('plugin:yaak-license|check');
},
});

View File

@@ -1,6 +1,6 @@
{
"name": "@yaakapp-internal/license",
"version": "1.0.0",
"private": true,
"version": "1.0.0",
"main": "index.ts"
}

View File

@@ -1,9 +1,9 @@
import { invoke } from "@tauri-apps/api/core";
import { invoke } from '@tauri-apps/api/core';
export function setWindowTitle(title: string) {
invoke("plugin:yaak-mac-window|set_title", { title }).catch(console.error);
invoke('plugin:yaak-mac-window|set_title', { title }).catch(console.error);
}
export function setWindowTheme(bgColor: string) {
invoke("plugin:yaak-mac-window|set_theme", { bgColor }).catch(console.error);
invoke('plugin:yaak-mac-window|set_theme', { bgColor }).catch(console.error);
}

View File

@@ -1,6 +1,6 @@
{
"name": "@yaakapp-internal/mac-window",
"version": "1.0.0",
"private": true,
"version": "1.0.0",
"main": "index.ts"
}

View File

@@ -1,3 +1,6 @@
[default]
description = "Default permissions for the plugin"
permissions = ["allow-set-title", "allow-set-theme"]
permissions = [
"allow-set-title",
"allow-set-theme",
]

View File

@@ -12,11 +12,6 @@ unsafe impl Sync for UnsafeWindowHandle {}
const WINDOW_CONTROL_PAD_X: f64 = 13.0;
const WINDOW_CONTROL_PAD_Y: f64 = 18.0;
/// Extra pixels to add to the title bar height when the default title bar is
/// already as tall as button_height + PAD_Y (i.e. macOS Tahoe 26+, where the
/// default is 32px and 14 + 18 = 32). On pre-Tahoe this is unused because the
/// default title bar is shorter than button_height + PAD_Y.
const TITLEBAR_EXTRA_HEIGHT: f64 = 4.0;
const MAIN_WINDOW_PREFIX: &str = "main_";
pub(crate) fn update_window_title<R: Runtime>(window: Window<R>, title: String) {
@@ -100,29 +95,12 @@ fn position_traffic_lights(ns_window_handle: UnsafeWindowHandle, x: f64, y: f64,
ns_window.standardWindowButton_(NSWindowButton::NSWindowMiniaturizeButton);
let zoom = ns_window.standardWindowButton_(NSWindowButton::NSWindowZoomButton);
let title_bar_container_view = close.superview().superview();
let close_rect: NSRect = msg_send![close, frame];
let button_height = close_rect.size.height;
let title_bar_container_view = close.superview().superview();
// Capture the OS default title bar height on the first call, before
// we've modified it. This avoids the height growing on repeated calls.
use std::sync::OnceLock;
static DEFAULT_TITLEBAR_HEIGHT: OnceLock<f64> = OnceLock::new();
let default_height =
*DEFAULT_TITLEBAR_HEIGHT.get_or_init(|| NSView::frame(title_bar_container_view).size.height);
// On pre-Tahoe, button_height + y is larger than the default title bar
// height, so the resize works as before. On Tahoe (26+), the default is
// already 32px and button_height + y = 32, so nothing changes. In that
// case, add TITLEBAR_EXTRA_HEIGHT extra pixels to push the buttons down.
let desired = button_height + y;
let title_bar_frame_height = if desired > default_height {
desired
} else {
default_height + TITLEBAR_EXTRA_HEIGHT
};
let title_bar_frame_height = button_height + y;
let mut title_bar_rect = NSView::frame(title_bar_container_view);
title_bar_rect.size.height = title_bar_frame_height;
title_bar_rect.origin.y = NSView::frame(ns_window).size.height - title_bar_frame_height;

View File

@@ -21,10 +21,3 @@ pub fn get_str_map<'a>(v: &'a BTreeMap<String, Value>, key: &str) -> &'a str {
Some(v) => v.as_str().unwrap_or_default(),
}
}
pub fn get_bool_map(v: &BTreeMap<String, Value>, key: &str, fallback: bool) -> bool {
match v.get(key) {
None => fallback,
Some(v) => v.as_bool().unwrap_or(fallback),
}
}

View File

@@ -1,17 +1,17 @@
import { invoke } from "@tauri-apps/api/core";
import { invoke } from '@tauri-apps/api/core';
export function enableEncryption(workspaceId: string) {
return invoke<void>("cmd_enable_encryption", { workspaceId });
return invoke<void>('cmd_enable_encryption', { workspaceId });
}
export function revealWorkspaceKey(workspaceId: string) {
return invoke<string>("cmd_reveal_workspace_key", { workspaceId });
return invoke<string>('cmd_reveal_workspace_key', { workspaceId });
}
export function setWorkspaceKey(args: { workspaceId: string; key: string }) {
return invoke<void>("cmd_set_workspace_key", args);
return invoke<void>('cmd_set_workspace_key', args);
}
export function disableEncryption(workspaceId: string) {
return invoke<void>("cmd_disable_encryption", { workspaceId });
return invoke<void>('cmd_disable_encryption', { workspaceId });
}

View File

@@ -1,6 +1,6 @@
{
"name": "@yaakapp-internal/crypto",
"version": "1.0.0",
"private": true,
"version": "1.0.0",
"main": "index.ts"
}

View File

@@ -1,66 +1,60 @@
import { useQuery } from "@tanstack/react-query";
import { invoke } from "@tauri-apps/api/core";
import { createFastMutation } from "@yaakapp/app/hooks/useFastMutation";
import { queryClient } from "@yaakapp/app/lib/queryClient";
import { useMemo } from "react";
import {
BranchDeleteResult,
CloneResult,
GitCommit,
GitRemote,
GitStatusSummary,
PullResult,
PushResult,
} from "./bindings/gen_git";
import { showToast } from "@yaakapp/app/lib/toast";
import { useQuery } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api/core';
import { createFastMutation } from '@yaakapp/app/hooks/useFastMutation';
import { queryClient } from '@yaakapp/app/lib/queryClient';
import { useMemo } from 'react';
import { BranchDeleteResult, CloneResult, GitCommit, GitRemote, GitStatusSummary, PullResult, PushResult } from './bindings/gen_git';
import { showToast } from '@yaakapp/app/lib/toast';
export * from "./bindings/gen_git";
export * from "./bindings/gen_models";
export * from './bindings/gen_git';
export * from './bindings/gen_models';
export interface GitCredentials {
username: string;
password: string;
}
export type DivergedStrategy = "force_reset" | "merge" | "cancel";
export type DivergedStrategy = 'force_reset' | 'merge' | 'cancel';
export type UncommittedChangesStrategy = "reset" | "cancel";
export type UncommittedChangesStrategy = 'reset' | 'cancel';
export interface GitCallbacks {
addRemote: () => Promise<GitRemote | null>;
promptCredentials: (
result: Extract<PushResult, { type: "needs_credentials" }>,
result: Extract<PushResult, { type: 'needs_credentials' }>,
) => Promise<GitCredentials | null>;
promptDiverged: (result: Extract<PullResult, { type: "diverged" }>) => Promise<DivergedStrategy>;
promptDiverged: (
result: Extract<PullResult, { type: 'diverged' }>,
) => Promise<DivergedStrategy>;
promptUncommittedChanges: () => Promise<UncommittedChangesStrategy>;
forceSync: () => Promise<void>;
}
const onSuccess = () => queryClient.invalidateQueries({ queryKey: ["git"] });
const onSuccess = () => queryClient.invalidateQueries({ queryKey: ['git'] });
export function useGit(dir: string, callbacks: GitCallbacks, refreshKey?: string) {
const mutations = useMemo(() => gitMutations(dir, callbacks), [dir, callbacks]);
const fetchAll = useQuery<void, string>({
queryKey: ["git", "fetch_all", dir, refreshKey],
queryFn: () => invoke("cmd_git_fetch_all", { dir }),
queryKey: ['git', 'fetch_all', dir, refreshKey],
queryFn: () => invoke('cmd_git_fetch_all', { dir }),
refetchInterval: 10 * 60_000,
});
return [
{
remotes: useQuery<GitRemote[], string>({
queryKey: ["git", "remotes", dir, refreshKey],
queryKey: ['git', 'remotes', dir, refreshKey],
queryFn: () => getRemotes(dir),
placeholderData: (prev) => prev,
}),
log: useQuery<GitCommit[], string>({
queryKey: ["git", "log", dir, refreshKey],
queryFn: () => invoke("cmd_git_log", { dir }),
queryKey: ['git', 'log', dir, refreshKey],
queryFn: () => invoke('cmd_git_log', { dir }),
placeholderData: (prev) => prev,
}),
status: useQuery<GitStatusSummary, string>({
refetchOnMount: true,
queryKey: ["git", "status", dir, refreshKey, fetchAll.dataUpdatedAt],
queryFn: () => invoke("cmd_git_status", { dir }),
queryKey: ['git', 'status', dir, refreshKey, fetchAll.dataUpdatedAt],
queryFn: () => invoke('cmd_git_status', { dir }),
placeholderData: (prev) => prev,
}),
},
@@ -73,167 +67,151 @@ export const gitMutations = (dir: string, callbacks: GitCallbacks) => {
const remotes = await getRemotes(dir);
if (remotes.length === 0) {
const remote = await callbacks.addRemote();
if (remote == null) throw new Error("No remote found");
if (remote == null) throw new Error('No remote found');
}
const result = await invoke<PushResult>("cmd_git_push", { dir });
if (result.type !== "needs_credentials") return result;
const result = await invoke<PushResult>('cmd_git_push', { dir });
if (result.type !== 'needs_credentials') return result;
// Needs credentials, prompt for them
const creds = await callbacks.promptCredentials(result);
if (creds == null) throw new Error("Canceled");
if (creds == null) throw new Error('Canceled');
await invoke("cmd_git_add_credential", {
await invoke('cmd_git_add_credential', {
remoteUrl: result.url,
username: creds.username,
password: creds.password,
});
// Push again
return invoke<PushResult>("cmd_git_push", { dir });
return invoke<PushResult>('cmd_git_push', { dir });
};
const handleError = (err: unknown) => {
showToast({
id: err instanceof Error ? err.message : String(err),
message: err instanceof Error ? err.message : String(err),
color: "danger",
id: `${err}`,
message: `${err}`,
color: 'danger',
timeout: 5000,
});
};
}
return {
init: createFastMutation<void, string, void>({
mutationKey: ["git", "init"],
mutationFn: () => invoke("cmd_git_initialize", { dir }),
mutationKey: ['git', 'init'],
mutationFn: () => invoke('cmd_git_initialize', { dir }),
onSuccess,
}),
add: createFastMutation<void, string, { relaPaths: string[] }>({
mutationKey: ["git", "add", dir],
mutationFn: (args) => invoke("cmd_git_add", { dir, ...args }),
mutationKey: ['git', 'add', dir],
mutationFn: (args) => invoke('cmd_git_add', { dir, ...args }),
onSuccess,
}),
addRemote: createFastMutation<GitRemote, string, GitRemote>({
mutationKey: ["git", "add-remote"],
mutationFn: (args) => invoke("cmd_git_add_remote", { dir, ...args }),
mutationKey: ['git', 'add-remote'],
mutationFn: (args) => invoke('cmd_git_add_remote', { dir, ...args }),
onSuccess,
}),
rmRemote: createFastMutation<void, string, { name: string }>({
mutationKey: ["git", "rm-remote", dir],
mutationFn: (args) => invoke("cmd_git_rm_remote", { dir, ...args }),
mutationKey: ['git', 'rm-remote', dir],
mutationFn: (args) => invoke('cmd_git_rm_remote', { dir, ...args }),
onSuccess,
}),
createBranch: createFastMutation<void, string, { branch: string; base?: string }>({
mutationKey: ["git", "branch", dir],
mutationFn: (args) => invoke("cmd_git_branch", { dir, ...args }),
mutationKey: ['git', 'branch', dir],
mutationFn: (args) => invoke('cmd_git_branch', { dir, ...args }),
onSuccess,
}),
mergeBranch: createFastMutation<void, string, { branch: string }>({
mutationKey: ["git", "merge", dir],
mutationFn: (args) => invoke("cmd_git_merge_branch", { dir, ...args }),
mutationKey: ['git', 'merge', dir],
mutationFn: (args) => invoke('cmd_git_merge_branch', { dir, ...args }),
onSuccess,
}),
deleteBranch: createFastMutation<
BranchDeleteResult,
string,
{ branch: string; force?: boolean }
>({
mutationKey: ["git", "delete-branch", dir],
mutationFn: (args) => invoke("cmd_git_delete_branch", { dir, ...args }),
deleteBranch: createFastMutation<BranchDeleteResult, string, { branch: string, force?: boolean }>({
mutationKey: ['git', 'delete-branch', dir],
mutationFn: (args) => invoke('cmd_git_delete_branch', { dir, ...args }),
onSuccess,
}),
deleteRemoteBranch: createFastMutation<void, string, { branch: string }>({
mutationKey: ["git", "delete-remote-branch", dir],
mutationFn: (args) => invoke("cmd_git_delete_remote_branch", { dir, ...args }),
mutationKey: ['git', 'delete-remote-branch', dir],
mutationFn: (args) => invoke('cmd_git_delete_remote_branch', { dir, ...args }),
onSuccess,
}),
renameBranch: createFastMutation<void, string, { oldName: string; newName: string }>({
mutationKey: ["git", "rename-branch", dir],
mutationFn: (args) => invoke("cmd_git_rename_branch", { dir, ...args }),
renameBranch: createFastMutation<void, string, { oldName: string, newName: string }>({
mutationKey: ['git', 'rename-branch', dir],
mutationFn: (args) => invoke('cmd_git_rename_branch', { dir, ...args }),
onSuccess,
}),
checkout: createFastMutation<string, string, { branch: string; force: boolean }>({
mutationKey: ["git", "checkout", dir],
mutationFn: (args) => invoke("cmd_git_checkout", { dir, ...args }),
mutationKey: ['git', 'checkout', dir],
mutationFn: (args) => invoke('cmd_git_checkout', { dir, ...args }),
onSuccess,
}),
commit: createFastMutation<void, string, { message: string }>({
mutationKey: ["git", "commit", dir],
mutationFn: (args) => invoke("cmd_git_commit", { dir, ...args }),
mutationKey: ['git', 'commit', dir],
mutationFn: (args) => invoke('cmd_git_commit', { dir, ...args }),
onSuccess,
}),
commitAndPush: createFastMutation<PushResult, string, { message: string }>({
mutationKey: ["git", "commit_push", dir],
mutationKey: ['git', 'commit_push', dir],
mutationFn: async (args) => {
await invoke("cmd_git_commit", { dir, ...args });
await invoke('cmd_git_commit', { dir, ...args });
return push();
},
onSuccess,
}),
push: createFastMutation<PushResult, string, void>({
mutationKey: ["git", "push", dir],
mutationKey: ['git', 'push', dir],
mutationFn: push,
onSuccess,
}),
pull: createFastMutation<PullResult, string, void>({
mutationKey: ["git", "pull", dir],
mutationKey: ['git', 'pull', dir],
async mutationFn() {
const result = await invoke<PullResult>("cmd_git_pull", { dir });
const result = await invoke<PullResult>('cmd_git_pull', { dir });
if (result.type === "needs_credentials") {
if (result.type === 'needs_credentials') {
const creds = await callbacks.promptCredentials(result);
if (creds == null) throw new Error("Canceled");
if (creds == null) throw new Error('Canceled');
await invoke("cmd_git_add_credential", {
await invoke('cmd_git_add_credential', {
remoteUrl: result.url,
username: creds.username,
password: creds.password,
});
// Pull again after credentials
return invoke<PullResult>("cmd_git_pull", { dir });
return invoke<PullResult>('cmd_git_pull', { dir });
}
if (result.type === "uncommitted_changes") {
void callbacks
.promptUncommittedChanges()
.then(async (strategy) => {
if (strategy === "cancel") return;
if (result.type === 'uncommitted_changes') {
callbacks.promptUncommittedChanges().then(async (strategy) => {
if (strategy === 'cancel') return;
await invoke("cmd_git_reset_changes", { dir });
return invoke<PullResult>("cmd_git_pull", { dir });
})
.then(async () => {
await onSuccess();
await callbacks.forceSync();
}, handleError);
await invoke('cmd_git_reset_changes', { dir });
return invoke<PullResult>('cmd_git_pull', { dir });
}).then(async () => { onSuccess(); await callbacks.forceSync(); }, handleError);
}
if (result.type === "diverged") {
void callbacks
.promptDiverged(result)
.then((strategy) => {
if (strategy === "cancel") return;
if (result.type === 'diverged') {
callbacks.promptDiverged(result).then((strategy) => {
if (strategy === 'cancel') return;
if (strategy === "force_reset") {
return invoke<PullResult>("cmd_git_pull_force_reset", {
dir,
remote: result.remote,
branch: result.branch,
});
}
return invoke<PullResult>("cmd_git_pull_merge", {
if (strategy === 'force_reset') {
return invoke<PullResult>('cmd_git_pull_force_reset', {
dir,
remote: result.remote,
branch: result.branch,
});
})
.then(async () => {
await onSuccess();
await callbacks.forceSync();
}, handleError);
}
return invoke<PullResult>('cmd_git_pull_merge', {
dir,
remote: result.remote,
branch: result.branch,
});
}).then(async () => { onSuccess(); await callbacks.forceSync(); }, handleError);
}
return result;
@@ -241,20 +219,20 @@ export const gitMutations = (dir: string, callbacks: GitCallbacks) => {
onSuccess,
}),
unstage: createFastMutation<void, string, { relaPaths: string[] }>({
mutationKey: ["git", "unstage", dir],
mutationFn: (args) => invoke("cmd_git_unstage", { dir, ...args }),
mutationKey: ['git', 'unstage', dir],
mutationFn: (args) => invoke('cmd_git_unstage', { dir, ...args }),
onSuccess,
}),
resetChanges: createFastMutation<void, string, void>({
mutationKey: ["git", "reset-changes", dir],
mutationFn: () => invoke("cmd_git_reset_changes", { dir }),
mutationKey: ['git', 'reset-changes', dir],
mutationFn: () => invoke('cmd_git_reset_changes', { dir }),
onSuccess,
}),
} as const;
};
async function getRemotes(dir: string) {
return invoke<GitRemote[]>("cmd_git_remotes", { dir });
return invoke<GitRemote[]>('cmd_git_remotes', { dir });
}
/**
@@ -263,24 +241,21 @@ async function getRemotes(dir: string) {
export async function gitClone(
url: string,
dir: string,
promptCredentials: (args: {
url: string;
error: string | null;
}) => Promise<GitCredentials | null>,
promptCredentials: (args: { url: string; error: string | null }) => Promise<GitCredentials | null>,
): Promise<CloneResult> {
const result = await invoke<CloneResult>("cmd_git_clone", { url, dir });
if (result.type !== "needs_credentials") return result;
const result = await invoke<CloneResult>('cmd_git_clone', { url, dir });
if (result.type !== 'needs_credentials') return result;
// Prompt for credentials
const creds = await promptCredentials({ url: result.url, error: result.error });
if (creds == null) return { type: "cancelled" };
if (creds == null) return {type: 'cancelled'};
// Store credentials and retry
await invoke("cmd_git_add_credential", {
await invoke('cmd_git_add_credential', {
remoteUrl: result.url,
username: creds.username,
password: creds.password,
});
return invoke<CloneResult>("cmd_git_clone", { url, dir });
return invoke<CloneResult>('cmd_git_clone', { url, dir });
}

View File

@@ -1,6 +1,6 @@
{
"name": "@yaakapp-internal/git",
"version": "1.0.0",
"private": true,
"version": "1.0.0",
"main": "index.ts"
}

View File

@@ -19,12 +19,7 @@ hyper-util = { version = "0.1.17", default-features = false, features = ["client
log = { workspace = true }
mime_guess = "2.0.5"
regex = "1.11.1"
reqwest = { workspace = true, features = [
"rustls-tls-manual-roots-no-provider",
"socks",
"http2",
"stream",
] }
reqwest = { workspace = true, features = ["rustls-tls-manual-roots-no-provider", "socks", "http2", "stream"] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
thiserror = { workspace = true }
@@ -34,5 +29,4 @@ tower-service = "0.3.3"
urlencoding = "2.1.3"
yaak-common = { workspace = true }
yaak-models = { workspace = true }
yaak-templates = { workspace = true }
yaak-tls = { workspace = true }

View File

@@ -30,8 +30,6 @@ pub enum HttpResponseEvent {
url: String,
status: u16,
behavior: RedirectBehavior,
dropped_body: bool,
dropped_headers: Vec<String>,
},
SendUrl {
method: String,
@@ -69,28 +67,12 @@ impl Display for HttpResponseEvent {
match self {
HttpResponseEvent::Setting(name, value) => write!(f, "* Setting {}={}", name, value),
HttpResponseEvent::Info(s) => write!(f, "* {}", s),
HttpResponseEvent::Redirect {
url,
status,
behavior,
dropped_body,
dropped_headers,
} => {
HttpResponseEvent::Redirect { url, status, behavior } => {
let behavior_str = match behavior {
RedirectBehavior::Preserve => "preserve",
RedirectBehavior::DropBody => "drop body",
};
let body_str = if *dropped_body { ", body dropped" } else { "" };
let headers_str = if dropped_headers.is_empty() {
String::new()
} else {
format!(", headers dropped: {}", dropped_headers.join(", "))
};
write!(
f,
"* Redirect {} -> {} ({}{}{})",
status, url, behavior_str, body_str, headers_str
)
write!(f, "* Redirect {} -> {} ({})", status, url, behavior_str)
}
HttpResponseEvent::SendUrl {
method,
@@ -148,21 +130,13 @@ impl From<HttpResponseEvent> for yaak_models::models::HttpResponseEventData {
match event {
HttpResponseEvent::Setting(name, value) => D::Setting { name, value },
HttpResponseEvent::Info(message) => D::Info { message },
HttpResponseEvent::Redirect {
url,
status,
behavior,
dropped_body,
dropped_headers,
} => D::Redirect {
HttpResponseEvent::Redirect { url, status, behavior } => D::Redirect {
url,
status,
behavior: match behavior {
RedirectBehavior::Preserve => "preserve".to_string(),
RedirectBehavior::DropBody => "drop_body".to_string(),
},
dropped_body,
dropped_headers,
},
HttpResponseEvent::SendUrl {
method,

View File

@@ -1,7 +1,7 @@
use crate::cookies::CookieStore;
use crate::error::Result;
use crate::sender::{HttpResponse, HttpResponseEvent, HttpSender, RedirectBehavior};
use crate::types::{SendableBody, SendableHttpRequest};
use crate::types::SendableHttpRequest;
use log::debug;
use tokio::sync::mpsc;
use tokio::sync::watch::Receiver;
@@ -87,11 +87,6 @@ impl<S: HttpSender> HttpTransaction<S> {
};
// Build request for this iteration
let preserved_body = match &current_body {
Some(SendableBody::Bytes(b)) => Some(SendableBody::Bytes(b.clone())),
_ => None,
};
let request_had_body = current_body.is_some();
let req = SendableHttpRequest {
url: current_url.clone(),
method: current_method.clone(),
@@ -187,6 +182,8 @@ impl<S: HttpSender> HttpTransaction<S> {
format!("{}/{}", base_path, location)
};
Self::remove_sensitive_headers(&mut current_headers, &previous_url, &current_url);
// Determine redirect behavior based on status code and method
let behavior = if status == 303 {
// 303 See Other always changes to GET
@@ -200,8 +197,11 @@ impl<S: HttpSender> HttpTransaction<S> {
RedirectBehavior::Preserve
};
let mut dropped_headers =
Self::remove_sensitive_headers(&mut current_headers, &previous_url, &current_url);
send_event(HttpResponseEvent::Redirect {
url: current_url.clone(),
status,
behavior: behavior.clone(),
});
// Handle method changes for certain redirect codes
if matches!(behavior, RedirectBehavior::DropBody) {
@@ -211,40 +211,13 @@ impl<S: HttpSender> HttpTransaction<S> {
// Remove content-related headers
current_headers.retain(|h| {
let name_lower = h.0.to_lowercase();
let should_drop =
name_lower.starts_with("content-") || name_lower == "transfer-encoding";
if should_drop {
Self::push_header_if_missing(&mut dropped_headers, &h.0);
}
!should_drop
!name_lower.starts_with("content-") && name_lower != "transfer-encoding"
});
}
// Restore body for Preserve redirects (307/308), drop for others.
// Stream bodies can't be replayed (same limitation as reqwest).
current_body = if matches!(behavior, RedirectBehavior::Preserve) {
if request_had_body && preserved_body.is_none() {
// Stream body was consumed and can't be replayed (same as reqwest)
return Err(crate::error::Error::RequestError(
"Cannot follow redirect: request body was a stream and cannot be resent"
.to_string(),
));
}
preserved_body
} else {
None
};
// Body was dropped if the request had one but we can't resend it
let dropped_body = request_had_body && current_body.is_none();
send_event(HttpResponseEvent::Redirect {
url: current_url.clone(),
status,
behavior: behavior.clone(),
dropped_body,
dropped_headers,
});
// Reset body for next iteration (since it was moved in the send call)
// For redirects that change method to GET or for all redirects since body was consumed
current_body = None;
redirect_count += 1;
}
@@ -258,8 +231,7 @@ impl<S: HttpSender> HttpTransaction<S> {
headers: &mut Vec<(String, String)>,
previous_url: &str,
next_url: &str,
) -> Vec<String> {
let mut dropped_headers = Vec::new();
) {
let previous_host = Url::parse(previous_url).ok().and_then(|u| {
u.host_str().map(|h| format!("{}:{}", h, u.port_or_known_default().unwrap_or(0)))
});
@@ -269,24 +241,13 @@ impl<S: HttpSender> HttpTransaction<S> {
if previous_host != next_host {
headers.retain(|h| {
let name_lower = h.0.to_lowercase();
let should_drop = name_lower == "authorization"
|| name_lower == "cookie"
|| name_lower == "cookie2"
|| name_lower == "proxy-authorization"
|| name_lower == "www-authenticate";
if should_drop {
Self::push_header_if_missing(&mut dropped_headers, &h.0);
}
!should_drop
name_lower != "authorization"
&& name_lower != "cookie"
&& name_lower != "cookie2"
&& name_lower != "proxy-authorization"
&& name_lower != "www-authenticate"
});
}
dropped_headers
}
fn push_header_if_missing(headers: &mut Vec<String>, name: &str) {
if !headers.iter().any(|h| h.eq_ignore_ascii_case(name)) {
headers.push(name.to_string());
}
}
/// Check if a status code indicates a redirect

View File

@@ -9,9 +9,8 @@ use std::collections::BTreeMap;
use std::pin::Pin;
use std::time::Duration;
use tokio::io::AsyncRead;
use yaak_common::serde::{get_bool, get_bool_map, get_str, get_str_map};
use yaak_common::serde::{get_bool, get_str, get_str_map};
use yaak_models::models::HttpRequest;
use yaak_templates::strip_json_comments::{maybe_strip_json_comments, strip_json_comments};
pub(crate) const MULTIPART_BOUNDARY: &str = "------YaakFormBoundary";
@@ -135,69 +134,16 @@ pub fn append_query_params(url: &str, params: Vec<(String, String)>) -> String {
result
}
fn strip_query_params(url: &str, names: &[&str]) -> String {
// Split off fragment
let (base_and_query, fragment) = if let Some(hash_pos) = url.find('#') {
(&url[..hash_pos], Some(&url[hash_pos..]))
} else {
(url, None)
};
let result = if let Some(q_pos) = base_and_query.find('?') {
let base = &base_and_query[..q_pos];
let query = &base_and_query[q_pos + 1..];
let filtered: Vec<&str> = query
.split('&')
.filter(|pair| {
let key = pair.split('=').next().unwrap_or("");
let decoded = urlencoding::decode(key).unwrap_or_default();
!names.contains(&decoded.as_ref())
})
.collect();
if filtered.is_empty() {
base.to_string()
} else {
format!("{}?{}", base, filtered.join("&"))
}
} else {
base_and_query.to_string()
};
match fragment {
Some(f) => format!("{}{}", result, f),
None => result,
}
}
fn build_url(r: &HttpRequest) -> String {
let (url_string, params) = apply_path_placeholders(&ensure_proto(&r.url), &r.url_parameters);
let mut url = append_query_params(
append_query_params(
&url_string,
params
.iter()
.filter(|p| p.enabled && !p.name.is_empty())
.map(|p| (p.name.clone(), p.value.clone()))
.collect(),
);
// GraphQL GET requests encode query/variables as URL query parameters
if r.method.to_lowercase() == "get" && r.body_type.as_deref() == Some("graphql") {
url = append_graphql_query_params(&url, &r.body);
}
url
}
fn append_graphql_query_params(url: &str, body: &BTreeMap<String, serde_json::Value>) -> String {
let query = get_str_map(body, "query").to_string();
let variables = strip_json_comments(&get_str_map(body, "variables"));
let mut params = vec![("query".to_string(), query)];
if !variables.trim().is_empty() {
params.push(("variables".to_string(), variables));
}
// Strip existing query/variables params to avoid duplicates
let url = strip_query_params(url, &["query", "variables"]);
append_query_params(&url, params)
)
}
fn build_headers(r: &HttpRequest) -> Vec<(String, String)> {
@@ -231,7 +177,7 @@ async fn build_body(
(build_form_body(&body), Some("application/x-www-form-urlencoded".to_string()))
}
"multipart/form-data" => build_multipart_body(&body, &headers).await?,
_ if body.contains_key("text") => (build_text_body(&body, body_type), None),
_ if body.contains_key("text") => (build_text_body(&body), None),
t => {
warn!("Unsupported body type: {}", t);
(None, None)
@@ -306,20 +252,13 @@ async fn build_binary_body(
}))
}
fn build_text_body(body: &BTreeMap<String, serde_json::Value>, body_type: &str) -> Option<SendableBodyWithMeta> {
fn build_text_body(body: &BTreeMap<String, serde_json::Value>) -> Option<SendableBodyWithMeta> {
let text = get_str_map(body, "text");
if text.is_empty() {
return None;
}
let send_comments = get_bool_map(body, "sendJsonComments", false);
let text = if !send_comments && body_type == "application/json" {
maybe_strip_json_comments(text)
None
} else {
text.to_string()
};
Some(SendableBodyWithMeta::Bytes(Bytes::from(text)))
Some(SendableBodyWithMeta::Bytes(Bytes::from(text.to_string())))
}
}
fn build_graphql_body(
@@ -327,7 +266,7 @@ fn build_graphql_body(
body: &BTreeMap<String, serde_json::Value>,
) -> Option<SendableBodyWithMeta> {
let query = get_str_map(body, "query");
let variables = strip_json_comments(&get_str_map(body, "variables"));
let variables = get_str_map(body, "variables");
if method.to_lowercase() == "get" {
// GraphQL GET requests use query parameters, not a body
@@ -745,7 +684,7 @@ mod tests {
let mut body = BTreeMap::new();
body.insert("text".to_string(), json!("Hello, World!"));
let result = build_text_body(&body, "application/json");
let result = build_text_body(&body);
match result {
Some(SendableBodyWithMeta::Bytes(bytes)) => {
assert_eq!(bytes, Bytes::from("Hello, World!"))
@@ -759,7 +698,7 @@ mod tests {
let mut body = BTreeMap::new();
body.insert("text".to_string(), json!(""));
let result = build_text_body(&body, "application/json");
let result = build_text_body(&body);
assert!(result.is_none());
}
@@ -767,57 +706,10 @@ mod tests {
async fn test_text_body_missing() {
let body = BTreeMap::new();
let result = build_text_body(&body, "application/json");
let result = build_text_body(&body);
assert!(result.is_none());
}
#[tokio::test]
async fn test_text_body_strips_json_comments_by_default() {
let mut body = BTreeMap::new();
body.insert("text".to_string(), json!("{\n // comment\n \"foo\": \"bar\"\n}"));
let result = build_text_body(&body, "application/json");
match result {
Some(SendableBodyWithMeta::Bytes(bytes)) => {
let text = String::from_utf8_lossy(&bytes);
assert!(!text.contains("// comment"));
assert!(text.contains("\"foo\": \"bar\""));
}
_ => panic!("Expected Some(SendableBody::Bytes)"),
}
}
#[tokio::test]
async fn test_text_body_send_json_comments_when_opted_in() {
let mut body = BTreeMap::new();
body.insert("text".to_string(), json!("{\n // comment\n \"foo\": \"bar\"\n}"));
body.insert("sendJsonComments".to_string(), json!(true));
let result = build_text_body(&body, "application/json");
match result {
Some(SendableBodyWithMeta::Bytes(bytes)) => {
let text = String::from_utf8_lossy(&bytes);
assert!(text.contains("// comment"));
}
_ => panic!("Expected Some(SendableBody::Bytes)"),
}
}
#[tokio::test]
async fn test_text_body_no_strip_for_non_json() {
let mut body = BTreeMap::new();
body.insert("text".to_string(), json!("// not json\nsome text"));
let result = build_text_body(&body, "text/plain");
match result {
Some(SendableBodyWithMeta::Bytes(bytes)) => {
let text = String::from_utf8_lossy(&bytes);
assert!(text.contains("// not json"));
}
_ => panic!("Expected Some(SendableBody::Bytes)"),
}
}
#[tokio::test]
async fn test_form_urlencoded_body() -> Result<()> {
let mut body = BTreeMap::new();

View File

@@ -49,7 +49,7 @@ export type HttpResponseEvent = { model: "http_response_event", id: string, crea
* This mirrors `yaak_http::sender::HttpResponseEvent` but with serde support.
* The `From` impl is in yaak-http to avoid circular dependencies.
*/
export type HttpResponseEventData = { "type": "setting", name: string, value: string, } | { "type": "info", message: string, } | { "type": "redirect", url: string, status: number, behavior: string, dropped_body: boolean, dropped_headers: Array<string>, } | { "type": "send_url", method: string, scheme: string, username: string, password: string, host: string, port: number, path: string, query: string, fragment: string, } | { "type": "receive_url", version: string, status: string, } | { "type": "header_up", name: string, value: string, } | { "type": "header_down", name: string, value: string, } | { "type": "chunk_sent", bytes: number, } | { "type": "chunk_received", bytes: number, } | { "type": "dns_resolved", hostname: string, addresses: Array<string>, duration: bigint, overridden: boolean, };
export type HttpResponseEventData = { "type": "setting", name: string, value: string, } | { "type": "info", message: string, } | { "type": "redirect", url: string, status: number, behavior: string, } | { "type": "send_url", method: string, scheme: string, username: string, password: string, host: string, port: number, path: string, query: string, fragment: string, } | { "type": "receive_url", version: string, status: string, } | { "type": "header_up", name: string, value: string, } | { "type": "header_down", name: string, value: string, } | { "type": "chunk_sent", bytes: number, } | { "type": "chunk_received", bytes: number, } | { "type": "dns_resolved", hostname: string, addresses: Array<string>, duration: bigint, overridden: boolean, };
export type HttpResponseHeader = { name: string, value: string, };

View File

@@ -1,39 +1,35 @@
import { atom } from "jotai";
import { atom } from 'jotai';
import { selectAtom } from "jotai/utils";
import type { AnyModel } from "../bindings/gen_models";
import { ExtractModel } from "./types";
import { newStoreData } from "./util";
import { selectAtom } from 'jotai/utils';
import type { AnyModel } from '../bindings/gen_models';
import { ExtractModel } from './types';
import { newStoreData } from './util';
export const modelStoreDataAtom = atom(newStoreData());
export const cookieJarsAtom = createOrderedModelAtom("cookie_jar", "name", "asc");
export const environmentsAtom = createOrderedModelAtom("environment", "sortPriority", "asc");
export const foldersAtom = createModelAtom("folder");
export const grpcConnectionsAtom = createOrderedModelAtom("grpc_connection", "createdAt", "desc");
export const grpcEventsAtom = createOrderedModelAtom("grpc_event", "createdAt", "asc");
export const grpcRequestsAtom = createModelAtom("grpc_request");
export const httpRequestsAtom = createModelAtom("http_request");
export const httpResponsesAtom = createOrderedModelAtom("http_response", "createdAt", "desc");
export const httpResponseEventsAtom = createOrderedModelAtom(
"http_response_event",
"createdAt",
"asc",
);
export const keyValuesAtom = createModelAtom("key_value");
export const pluginsAtom = createModelAtom("plugin");
export const settingsAtom = createSingularModelAtom("settings");
export const websocketRequestsAtom = createModelAtom("websocket_request");
export const websocketEventsAtom = createOrderedModelAtom("websocket_event", "createdAt", "asc");
export const cookieJarsAtom = createOrderedModelAtom('cookie_jar', 'name', 'asc');
export const environmentsAtom = createOrderedModelAtom('environment', 'sortPriority', 'asc');
export const foldersAtom = createModelAtom('folder');
export const grpcConnectionsAtom = createOrderedModelAtom('grpc_connection', 'createdAt', 'desc');
export const grpcEventsAtom = createOrderedModelAtom('grpc_event', 'createdAt', 'asc');
export const grpcRequestsAtom = createModelAtom('grpc_request');
export const httpRequestsAtom = createModelAtom('http_request');
export const httpResponsesAtom = createOrderedModelAtom('http_response', 'createdAt', 'desc');
export const httpResponseEventsAtom = createOrderedModelAtom('http_response_event', 'createdAt', 'asc');
export const keyValuesAtom = createModelAtom('key_value');
export const pluginsAtom = createModelAtom('plugin');
export const settingsAtom = createSingularModelAtom('settings');
export const websocketRequestsAtom = createModelAtom('websocket_request');
export const websocketEventsAtom = createOrderedModelAtom('websocket_event', 'createdAt', 'asc');
export const websocketConnectionsAtom = createOrderedModelAtom(
"websocket_connection",
"createdAt",
"desc",
'websocket_connection',
'createdAt',
'desc',
);
export const workspaceMetasAtom = createModelAtom("workspace_meta");
export const workspacesAtom = createOrderedModelAtom("workspace", "name", "asc");
export const workspaceMetasAtom = createModelAtom('workspace_meta');
export const workspacesAtom = createOrderedModelAtom('workspace', 'name', 'asc');
export function createModelAtom<M extends AnyModel["model"]>(modelType: M) {
export function createModelAtom<M extends AnyModel['model']>(modelType: M) {
return selectAtom(
modelStoreDataAtom,
(data) => Object.values(data[modelType] ?? {}),
@@ -41,19 +37,19 @@ export function createModelAtom<M extends AnyModel["model"]>(modelType: M) {
);
}
export function createSingularModelAtom<M extends AnyModel["model"]>(modelType: M) {
export function createSingularModelAtom<M extends AnyModel['model']>(modelType: M) {
return selectAtom(modelStoreDataAtom, (data) => {
const modelData = Object.values(data[modelType] ?? {});
const item = modelData[0];
if (item == null) throw new Error("Failed creating singular model with no data: " + modelType);
if (item == null) throw new Error('Failed creating singular model with no data: ' + modelType);
return item;
});
}
export function createOrderedModelAtom<M extends AnyModel["model"]>(
export function createOrderedModelAtom<M extends AnyModel['model']>(
modelType: M,
field: keyof ExtractModel<AnyModel, M>,
order: "asc" | "desc",
order: 'asc' | 'desc',
) {
return selectAtom(
modelStoreDataAtom,
@@ -62,7 +58,7 @@ export function createOrderedModelAtom<M extends AnyModel["model"]>(
return Object.values(modelData).sort(
(a: ExtractModel<AnyModel, M>, b: ExtractModel<AnyModel, M>) => {
const n = a[field] > b[field] ? 1 : -1;
return order === "desc" ? n * -1 : n;
return order === 'desc' ? n * -1 : n;
},
);
},

View File

@@ -1,11 +1,11 @@
import { AnyModel } from "../bindings/gen_models";
import { AnyModel } from '../bindings/gen_models';
export * from "../bindings/gen_models";
export * from "../bindings/gen_util";
export * from "./store";
export * from "./atoms";
export * from '../bindings/gen_models';
export * from '../bindings/gen_util';
export * from './store';
export * from './atoms';
export function modelTypeLabel(m: AnyModel): string {
const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1);
return m.model.split("_").map(capitalize).join(" ");
return m.model.split('_').map(capitalize).join(' ');
}

View File

@@ -1,10 +1,10 @@
import { invoke } from "@tauri-apps/api/core";
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
import { resolvedModelName } from "@yaakapp/app/lib/resolvedModelName";
import { AnyModel, ModelPayload } from "../bindings/gen_models";
import { modelStoreDataAtom } from "./atoms";
import { ExtractModel, JotaiStore, ModelStoreData } from "./types";
import { newStoreData } from "./util";
import { invoke } from '@tauri-apps/api/core';
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
import { resolvedModelName } from '@yaakapp/app/lib/resolvedModelName';
import { AnyModel, ModelPayload } from '../bindings/gen_models';
import { modelStoreDataAtom } from './atoms';
import { ExtractModel, JotaiStore, ModelStoreData } from './types';
import { newStoreData } from './util';
let _store: JotaiStore | null = null;
@@ -12,11 +12,11 @@ export function initModelStore(store: JotaiStore) {
_store = store;
getCurrentWebviewWindow()
.listen<ModelPayload>("model_write", ({ payload }) => {
.listen<ModelPayload>('model_write', ({ payload }) => {
if (shouldIgnoreModel(payload)) return;
mustStore().set(modelStoreDataAtom, (prev: ModelStoreData) => {
if (payload.change.type === "upsert") {
if (payload.change.type === 'upsert') {
return {
...prev,
[payload.model.model]: {
@@ -36,7 +36,7 @@ export function initModelStore(store: JotaiStore) {
function mustStore(): JotaiStore {
if (_store == null) {
throw new Error("Model store was not initialized");
throw new Error('Model store was not initialized');
}
return _store;
@@ -45,8 +45,8 @@ function mustStore(): JotaiStore {
let _activeWorkspaceId: string | null = null;
export async function changeModelStoreWorkspace(workspaceId: string | null) {
console.log("Syncing models with new workspace", workspaceId);
const workspaceModelsStr = await invoke<string>("models_workspace_models", {
console.log('Syncing models with new workspace', workspaceId);
const workspaceModelsStr = await invoke<string>('models_workspace_models', {
workspaceId, // NOTE: if no workspace id provided, it will just fetch global models
});
const workspaceModels = JSON.parse(workspaceModelsStr) as AnyModel[];
@@ -57,12 +57,12 @@ export async function changeModelStoreWorkspace(workspaceId: string | null) {
mustStore().set(modelStoreDataAtom, data);
console.log("Synced model store with workspace", workspaceId, data);
console.log('Synced model store with workspace', workspaceId, data);
_activeWorkspaceId = workspaceId;
}
export function listModels<M extends AnyModel["model"], T extends ExtractModel<AnyModel, M>>(
export function listModels<M extends AnyModel['model'], T extends ExtractModel<AnyModel, M>>(
modelType: M | ReadonlyArray<M>,
): T[] {
let data = mustStore().get(modelStoreDataAtom);
@@ -70,7 +70,7 @@ export function listModels<M extends AnyModel["model"], T extends ExtractModel<A
return types.flatMap((t) => Object.values(data[t]) as T[]);
}
export function getModel<M extends AnyModel["model"], T extends ExtractModel<AnyModel, M>>(
export function getModel<M extends AnyModel['model'], T extends ExtractModel<AnyModel, M>>(
modelType: M | ReadonlyArray<M>,
id: string,
): T | null {
@@ -83,17 +83,18 @@ export function getModel<M extends AnyModel["model"], T extends ExtractModel<Any
return null;
}
export function getAnyModel(id: string): AnyModel | null {
export function getAnyModel(
id: string,
): AnyModel | null {
let data = mustStore().get(modelStoreDataAtom);
for (const t of Object.keys(data)) {
// oxlint-disable-next-line no-explicit-any
let v = (data as any)[t]?.[id];
if (v?.model === t) return v;
}
return null;
}
export function patchModelById<M extends AnyModel["model"], T extends ExtractModel<AnyModel, M>>(
export function patchModelById<M extends AnyModel['model'], T extends ExtractModel<AnyModel, M>>(
model: M,
id: string,
patch: Partial<T> | ((prev: T) => T),
@@ -103,54 +104,54 @@ export function patchModelById<M extends AnyModel["model"], T extends ExtractMod
throw new Error(`Failed to get model to patch id=${id} model=${model}`);
}
const newModel = typeof patch === "function" ? patch(prev) : { ...prev, ...patch };
const newModel = typeof patch === 'function' ? patch(prev) : { ...prev, ...patch };
return updateModel(newModel);
}
export async function patchModel<M extends AnyModel["model"], T extends ExtractModel<AnyModel, M>>(
base: Pick<T, "id" | "model">,
export async function patchModel<M extends AnyModel['model'], T extends ExtractModel<AnyModel, M>>(
base: Pick<T, 'id' | 'model'>,
patch: Partial<T>,
): Promise<string> {
return patchModelById<M, T>(base.model, base.id, patch);
}
export async function updateModel<M extends AnyModel["model"], T extends ExtractModel<AnyModel, M>>(
export async function updateModel<M extends AnyModel['model'], T extends ExtractModel<AnyModel, M>>(
model: T,
): Promise<string> {
return invoke<string>("models_upsert", { model });
return invoke<string>('models_upsert', { model });
}
export async function deleteModelById<
M extends AnyModel["model"],
M extends AnyModel['model'],
T extends ExtractModel<AnyModel, M>,
>(modelType: M | M[], id: string) {
let model = getModel<M, T>(modelType, id);
await deleteModel(model);
}
export async function deleteModel<M extends AnyModel["model"], T extends ExtractModel<AnyModel, M>>(
export async function deleteModel<M extends AnyModel['model'], T extends ExtractModel<AnyModel, M>>(
model: T | null,
) {
if (model == null) {
throw new Error("Failed to delete null model");
throw new Error('Failed to delete null model');
}
await invoke<string>("models_delete", { model });
await invoke<string>('models_delete', { model });
}
export function duplicateModel<M extends AnyModel["model"], T extends ExtractModel<AnyModel, M>>(
export function duplicateModel<M extends AnyModel['model'], T extends ExtractModel<AnyModel, M>>(
model: T | null,
) {
if (model == null) {
throw new Error("Failed to duplicate null model");
throw new Error('Failed to duplicate null model');
}
// If the model has a name, try to duplicate it with a name that doesn't conflict
let name = "name" in model ? resolvedModelName(model) : undefined;
let name = 'name' in model ? resolvedModelName(model) : undefined;
if (name != null) {
const existingModels = listModels(model.model);
for (let i = 0; i < 100; i++) {
const hasConflict = existingModels.some((m) => {
if ("folderId" in m && "folderId" in model && model.folderId !== m.folderId) {
if ('folderId' in m && 'folderId' in model && model.folderId !== m.folderId) {
return false;
} else if (resolvedModelName(m) !== name) {
return false;
@@ -164,7 +165,7 @@ export function duplicateModel<M extends AnyModel["model"], T extends ExtractMod
// Name conflict. Try another one
const m: RegExpMatchArray | null = name.match(/ Copy( (?<n>\d+))?$/);
if (m != null && m.groups?.n == null) {
name = name.substring(0, m.index) + " Copy 2";
name = name.substring(0, m.index) + ' Copy 2';
} else if (m != null && m.groups?.n != null) {
name = name.substring(0, m.index) + ` Copy ${parseInt(m.groups.n) + 1}`;
} else {
@@ -173,23 +174,23 @@ export function duplicateModel<M extends AnyModel["model"], T extends ExtractMod
}
}
return invoke<string>("models_duplicate", { model: { ...model, name } });
return invoke<string>('models_duplicate', { model: { ...model, name } });
}
export async function createGlobalModel<T extends Exclude<AnyModel, { workspaceId: string }>>(
patch: Partial<T> & Pick<T, "model">,
patch: Partial<T> & Pick<T, 'model'>,
): Promise<string> {
return invoke<string>("models_upsert", { model: patch });
return invoke<string>('models_upsert', { model: patch });
}
export async function createWorkspaceModel<T extends Extract<AnyModel, { workspaceId: string }>>(
patch: Partial<T> & Pick<T, "model" | "workspaceId">,
patch: Partial<T> & Pick<T, 'model' | 'workspaceId'>,
): Promise<string> {
return invoke<string>("models_upsert", { model: patch });
return invoke<string>('models_upsert', { model: patch });
}
export function replaceModelsInStore<
M extends AnyModel["model"],
M extends AnyModel['model'],
T extends Extract<AnyModel, { model: M }>,
>(model: M, models: T[]) {
const newModels: Record<string, T> = {};
@@ -206,7 +207,7 @@ export function replaceModelsInStore<
}
export function mergeModelsInStore<
M extends AnyModel["model"],
M extends AnyModel['model'],
T extends Extract<AnyModel, { model: M }>,
>(model: M, models: T[], filter?: (model: T) => boolean) {
mustStore().set(modelStoreDataAtom, (prev: ModelStoreData) => {
@@ -235,7 +236,7 @@ export function mergeModelsInStore<
function shouldIgnoreModel({ model, updateSource }: ModelPayload) {
// Never ignore updates from non-user sources
if (updateSource.type !== "window") {
if (updateSource.type !== 'window') {
return false;
}
@@ -245,11 +246,11 @@ function shouldIgnoreModel({ model, updateSource }: ModelPayload) {
}
// Only sync models that belong to this workspace, if a workspace ID is present
if ("workspaceId" in model && model.workspaceId !== _activeWorkspaceId) {
if ('workspaceId' in model && model.workspaceId !== _activeWorkspaceId) {
return true;
}
if (model.model === "key_value" && model.namespace === "no_sync") {
if (model.model === 'key_value' && model.namespace === 'no_sync') {
return true;
}

View File

@@ -1,8 +1,8 @@
import { createStore } from "jotai";
import { AnyModel } from "../bindings/gen_models";
import { createStore } from 'jotai';
import { AnyModel } from '../bindings/gen_models';
export type ExtractModel<T, M> = T extends { model: M } ? T : never;
export type ModelStoreData<T extends AnyModel = AnyModel> = {
[M in T["model"]]: Record<string, Extract<T, { model: M }>>;
[M in T['model']]: Record<string, Extract<T, { model: M }>>;
};
export type JotaiStore = ReturnType<typeof createStore>;

View File

@@ -1,4 +1,4 @@
import { ModelStoreData } from "./types";
import { ModelStoreData } from './types';
export function newStoreData(): ModelStoreData {
return {

View File

@@ -1,6 +1,6 @@
{
"name": "@yaakapp-internal/models",
"version": "1.0.0",
"private": true,
"version": "1.0.0",
"main": "guest-js/index.ts"
}

View File

@@ -1499,10 +1499,6 @@ pub enum HttpResponseEventData {
url: String,
status: u16,
behavior: String,
#[serde(default)]
dropped_body: bool,
#[serde(default)]
dropped_headers: Vec<String>,
},
SendUrl {
method: String,

View File

@@ -62,7 +62,7 @@ export type HttpResponseEvent = { model: "http_response_event", id: string, crea
* This mirrors `yaak_http::sender::HttpResponseEvent` but with serde support.
* The `From` impl is in yaak-http to avoid circular dependencies.
*/
export type HttpResponseEventData = { "type": "setting", name: string, value: string, } | { "type": "info", message: string, } | { "type": "redirect", url: string, status: number, behavior: string, dropped_body: boolean, dropped_headers: Array<string>, } | { "type": "send_url", method: string, scheme: string, username: string, password: string, host: string, port: number, path: string, query: string, fragment: string, } | { "type": "receive_url", version: string, status: string, } | { "type": "header_up", name: string, value: string, } | { "type": "header_down", name: string, value: string, } | { "type": "chunk_sent", bytes: number, } | { "type": "chunk_received", bytes: number, } | { "type": "dns_resolved", hostname: string, addresses: Array<string>, duration: bigint, overridden: boolean, };
export type HttpResponseEventData = { "type": "setting", name: string, value: string, } | { "type": "info", message: string, } | { "type": "redirect", url: string, status: number, behavior: string, } | { "type": "send_url", method: string, scheme: string, username: string, password: string, host: string, port: number, path: string, query: string, fragment: string, } | { "type": "receive_url", version: string, status: string, } | { "type": "header_up", name: string, value: string, } | { "type": "header_down", name: string, value: string, } | { "type": "chunk_sent", bytes: number, } | { "type": "chunk_received", bytes: number, } | { "type": "dns_resolved", hostname: string, addresses: Array<string>, duration: bigint, overridden: boolean, };
export type HttpResponseHeader = { name: string, value: string, };

View File

@@ -1,30 +1,30 @@
import { invoke } from "@tauri-apps/api/core";
import { PluginNameVersion, PluginSearchResponse, PluginUpdatesResponse } from "./bindings/gen_api";
import { invoke } from '@tauri-apps/api/core';
import { PluginNameVersion, PluginSearchResponse, PluginUpdatesResponse } from './bindings/gen_api';
export * from "./bindings/gen_models";
export * from "./bindings/gen_events";
export * from "./bindings/gen_search";
export * from './bindings/gen_models';
export * from './bindings/gen_events';
export * from './bindings/gen_search';
export async function searchPlugins(query: string) {
return invoke<PluginSearchResponse>("cmd_plugins_search", { query });
return invoke<PluginSearchResponse>('cmd_plugins_search', { query });
}
export async function installPlugin(name: string, version: string | null) {
return invoke<void>("cmd_plugins_install", { name, version });
return invoke<void>('cmd_plugins_install', { name, version });
}
export async function uninstallPlugin(pluginId: string) {
return invoke<void>("cmd_plugins_uninstall", { pluginId });
return invoke<void>('cmd_plugins_uninstall', { pluginId });
}
export async function checkPluginUpdates() {
return invoke<PluginUpdatesResponse>("cmd_plugins_updates", {});
return invoke<PluginUpdatesResponse>('cmd_plugins_updates', {});
}
export async function updateAllPlugins() {
return invoke<PluginNameVersion[]>("cmd_plugins_update_all", {});
return invoke<PluginNameVersion[]>('cmd_plugins_update_all', {});
}
export async function installPluginFromDirectory(directory: string) {
return invoke<void>("cmd_plugins_install_from_directory", { directory });
return invoke<void>('cmd_plugins_install_from_directory', { directory });
}

View File

@@ -1,6 +1,6 @@
{
"name": "@yaakapp-internal/plugins",
"version": "1.0.0",
"private": true,
"version": "1.0.0",
"main": "index.ts"
}

View File

@@ -50,8 +50,6 @@ pub struct PluginManager {
vendored_plugin_dir: PathBuf,
pub(crate) installed_plugin_dir: PathBuf,
dev_mode: bool,
/// Errors from plugin initialization, retrievable once via `take_init_errors`.
init_errors: Arc<Mutex<Vec<(String, String)>>>,
}
/// Callback for plugin initialization events (e.g., toast notifications)
@@ -95,7 +93,6 @@ impl PluginManager {
vendored_plugin_dir,
installed_plugin_dir,
dev_mode,
init_errors: Default::default(),
};
// Forward events to subscribers
@@ -186,21 +183,17 @@ impl PluginManager {
let init_errors = plugin_manager.initialize_all_plugins(plugins, plugin_context).await;
if !init_errors.is_empty() {
for (dir, err) in &init_errors {
warn!("Plugin failed to initialize: {dir}: {err}");
}
*plugin_manager.init_errors.lock().await = init_errors;
let joined = init_errors
.into_iter()
.map(|(dir, err)| format!("{dir}: {err}"))
.collect::<Vec<_>>()
.join("; ");
return Err(PluginErr(format!("Failed to initialize plugin(s): {joined}")));
}
Ok(plugin_manager)
}
/// Take any initialization errors, clearing them from the manager.
/// Returns a list of `(plugin_directory, error_message)` pairs.
pub async fn take_init_errors(&self) -> Vec<(String, String)> {
std::mem::take(&mut *self.init_errors.lock().await)
}
/// Get the vendored plugin directory path (resolves dev mode path if applicable)
pub fn get_plugins_dir(&self) -> PathBuf {
if self.dev_mode {

View File

@@ -1 +1 @@
export * from "./bindings/sse";
export * from './bindings/sse';

View File

@@ -1,6 +1,6 @@
{
"name": "@yaakapp-internal/sse",
"version": "1.0.0",
"private": true,
"version": "1.0.0",
"main": "index.ts"
}

View File

@@ -1,24 +1,24 @@
import { Channel, invoke } from "@tauri-apps/api/core";
import { emit } from "@tauri-apps/api/event";
import type { WatchResult } from "@yaakapp-internal/tauri";
import { SyncOp } from "./bindings/gen_sync";
import { WatchEvent } from "./bindings/gen_watch";
import { Channel, invoke } from '@tauri-apps/api/core';
import { emit } from '@tauri-apps/api/event';
import type { WatchResult } from '@yaakapp-internal/tauri';
import { SyncOp } from './bindings/gen_sync';
import { WatchEvent } from './bindings/gen_watch';
export * from "./bindings/gen_models";
export * from './bindings/gen_models';
export async function calculateSync(workspaceId: string, syncDir: string) {
return invoke<SyncOp[]>("cmd_sync_calculate", {
return invoke<SyncOp[]>('cmd_sync_calculate', {
workspaceId,
syncDir,
});
}
export async function calculateSyncFsOnly(dir: string) {
return invoke<SyncOp[]>("cmd_sync_calculate_fs", { dir });
return invoke<SyncOp[]>('cmd_sync_calculate_fs', { dir });
}
export async function applySync(workspaceId: string, syncDir: string, syncOps: SyncOp[]) {
return invoke<void>("cmd_sync_apply", {
return invoke<void>('cmd_sync_apply', {
workspaceId,
syncDir,
syncOps: syncOps,
@@ -30,40 +30,40 @@ export function watchWorkspaceFiles(
syncDir: string,
callback: (e: WatchEvent) => void,
) {
console.log("Watching workspace files", workspaceId, syncDir);
console.log('Watching workspace files', workspaceId, syncDir);
const channel = new Channel<WatchEvent>();
channel.onmessage = callback;
const unlistenPromise = invoke<WatchResult>("cmd_sync_watch", {
const unlistenPromise = invoke<WatchResult>('cmd_sync_watch', {
workspaceId,
syncDir,
channel,
});
void unlistenPromise.then(({ unlistenEvent }) => {
unlistenPromise.then(({ unlistenEvent }) => {
addWatchKey(unlistenEvent);
});
return () =>
unlistenPromise
.then(async ({ unlistenEvent }) => {
console.log("Unwatching workspace files", workspaceId, syncDir);
console.log('Unwatching workspace files', workspaceId, syncDir);
unlistenToWatcher(unlistenEvent);
})
.catch(console.error);
}
function unlistenToWatcher(unlistenEvent: string) {
void emit(unlistenEvent).then(() => {
emit(unlistenEvent).then(() => {
removeWatchKey(unlistenEvent);
});
}
function getWatchKeys() {
return sessionStorage.getItem("workspace-file-watchers")?.split(",").filter(Boolean) ?? [];
return sessionStorage.getItem('workspace-file-watchers')?.split(',').filter(Boolean) ?? [];
}
function setWatchKeys(keys: string[]) {
sessionStorage.setItem("workspace-file-watchers", keys.join(","));
sessionStorage.setItem('workspace-file-watchers', keys.join(','));
}
function addWatchKey(key: string) {
@@ -79,6 +79,6 @@ function removeWatchKey(key: string) {
// On page load, unlisten to all zombie watchers
const keys = getWatchKeys();
if (keys.length > 0) {
console.log("Unsubscribing to zombie file watchers", keys);
console.log('Unsubscribing to zombie file watchers', keys);
keys.forEach(unlistenToWatcher);
}

View File

@@ -1,6 +1,6 @@
{
"name": "@yaakapp-internal/sync",
"version": "1.0.0",
"private": true,
"version": "1.0.0",
"main": "index.ts"
}

View File

@@ -1,27 +1,8 @@
const { execSync } = require("node:child_process");
const fs = require("node:fs");
const path = require("node:path");
const { execSync } = require('node:child_process');
if (process.env.SKIP_WASM_BUILD === "1") {
console.log("Skipping wasm-pack build (SKIP_WASM_BUILD=1)");
if (process.env.SKIP_WASM_BUILD === '1') {
console.log('Skipping wasm-pack build (SKIP_WASM_BUILD=1)');
return;
}
execSync("wasm-pack build --target bundler", { stdio: "inherit" });
// Rewrite the generated entry to use Vite's ?init import style instead of
// the ES Module Integration style that wasm-pack generates, which Vite/rolldown
// does not support in production builds.
const entry = path.join(__dirname, "pkg", "yaak_templates.js");
fs.writeFileSync(
entry,
[
'import init from "./yaak_templates_bg.wasm?init";',
'export * from "./yaak_templates_bg.js";',
'import * as bg from "./yaak_templates_bg.js";',
'const instance = await init({ "./yaak_templates_bg.js": bg });',
"bg.__wbg_set_wasm(instance.exports);",
"instance.exports.__wbindgen_start();",
"",
].join("\n"),
);
execSync('wasm-pack build --target bundler', { stdio: 'inherit' });

View File

@@ -1,6 +1,6 @@
export * from "./bindings/parser";
import { Tokens } from "./bindings/parser";
import { escape_template, parse_template, unescape_template } from "./pkg";
export * from './bindings/parser';
import { Tokens } from './bindings/parser';
import { escape_template, parse_template, unescape_template } from './pkg';
export function parseTemplate(template: string) {
return parse_template(template) as Tokens;

View File

@@ -1,7 +1,7 @@
{
"name": "@yaakapp-internal/templates",
"version": "1.0.0",
"private": true,
"version": "1.0.0",
"main": "index.ts",
"scripts": {
"bootstrap": "npm run build",

View File

@@ -1,6 +1,5 @@
import init from "./yaak_templates_bg.wasm?init";
import * as wasm from "./yaak_templates_bg.wasm";
export * from "./yaak_templates_bg.js";
import * as bg from "./yaak_templates_bg.js";
const instance = await init({ "./yaak_templates_bg.js": bg });
bg.__wbg_set_wasm(instance.exports);
instance.exports.__wbindgen_start();
import { __wbg_set_wasm } from "./yaak_templates_bg.js";
__wbg_set_wasm(wasm);
wasm.__wbindgen_start();

Binary file not shown.

View File

@@ -11,7 +11,6 @@ pub fn format_json(text: &str, tab: &str) -> String {
let mut new_json = "".to_string();
let mut depth = 0;
let mut state = FormatState::None;
let mut saw_newline_in_whitespace = false;
loop {
let rest_of_chars = chars.clone();
@@ -62,62 +61,6 @@ pub fn format_json(text: &str, tab: &str) -> String {
continue;
}
// Handle line comments (//)
if current_char == '/' && chars.peek() == Some(&'/') {
chars.next(); // Skip second /
// Collect the rest of the comment until newline
let mut comment = String::from("//");
loop {
match chars.peek() {
Some(&'\n') | None => break,
Some(_) => comment.push(chars.next().unwrap()),
}
}
// Check if the comma handler already added \n + indent
let trimmed = new_json.trim_end_matches(|c: char| c == ' ' || c == '\t');
if trimmed.ends_with(",\n") && !saw_newline_in_whitespace {
// Trailing comment on the same line as comma (e.g. "foo",// comment)
new_json.truncate(trimmed.len() - 1);
new_json.push(' ');
} else if !trimmed.ends_with('\n') && !new_json.is_empty() {
// Trailing comment after a value (no newline before us)
new_json.push(' ');
}
new_json.push_str(&comment);
new_json.push('\n');
new_json.push_str(tab.to_string().repeat(depth).as_str());
saw_newline_in_whitespace = false;
continue;
}
// Handle block comments (/* ... */)
if current_char == '/' && chars.peek() == Some(&'*') {
chars.next(); // Skip *
let mut comment = String::from("/*");
loop {
match chars.next() {
None => break,
Some('*') if chars.peek() == Some(&'/') => {
chars.next(); // Skip /
comment.push_str("*/");
break;
}
Some(c) => comment.push(c),
}
}
// If we're not already on a fresh line, add newline + indent before comment
let trimmed = new_json.trim_end_matches(|c: char| c == ' ' || c == '\t');
if !trimmed.is_empty() && !trimmed.ends_with('\n') {
new_json.push('\n');
new_json.push_str(tab.to_string().repeat(depth).as_str());
}
new_json.push_str(&comment);
// After block comment, add newline + indent for the next content
new_json.push('\n');
new_json.push_str(tab.to_string().repeat(depth).as_str());
continue;
}
match current_char {
',' => {
new_json.push(current_char);
@@ -182,37 +125,20 @@ pub fn format_json(text: &str, tab: &str) -> String {
|| current_char == '\t'
|| current_char == '\r'
{
if current_char == '\n' {
saw_newline_in_whitespace = true;
}
// Don't add these
} else {
saw_newline_in_whitespace = false;
new_json.push(current_char);
}
}
}
}
// Filter out whitespace-only lines, but preserve empty lines inside block comments
let mut result_lines: Vec<&str> = Vec::new();
let mut in_block_comment = false;
for line in new_json.lines() {
if in_block_comment {
result_lines.push(line);
if line.contains("*/") {
in_block_comment = false;
}
} else {
if line.contains("/*") && !line.contains("*/") {
in_block_comment = true;
}
if !line.trim().is_empty() {
result_lines.push(line);
}
}
}
result_lines.iter().map(|line| line.trim_end()).collect::<Vec<&str>>().join("\n")
// Replace only lines containing whitespace with nothing
new_json
.lines()
.filter(|line| !line.trim().is_empty()) // Filter out whitespace-only lines
.collect::<Vec<&str>>() // Collect the non-empty lines into a vector
.join("\n") // Join the lines back into a single string
}
#[cfg(test)]
@@ -371,161 +297,6 @@ mod tests {
r#"
{}
}
"#
.trim()
);
}
#[test]
fn test_line_comment_between_keys() {
assert_eq!(
format_json(
r#"{"foo":"bar",// a comment
"baz":"qux"}"#,
" "
),
r#"
{
"foo": "bar", // a comment
"baz": "qux"
}
"#
.trim()
);
}
#[test]
fn test_line_comment_at_end() {
assert_eq!(
format_json(
r#"{"foo":"bar" // trailing
}"#,
" "
),
r#"
{
"foo": "bar" // trailing
}
"#
.trim()
);
}
#[test]
fn test_block_comment() {
assert_eq!(
format_json(r#"{"foo":"bar",/* comment */"baz":"qux"}"#, " "),
r#"
{
"foo": "bar",
/* comment */
"baz": "qux"
}
"#
.trim()
);
}
#[test]
fn test_comment_in_array() {
assert_eq!(
format_json(
r#"[1,// item comment
2,3]"#,
" "
),
r#"
[
1, // item comment
2,
3
]
"#
.trim()
);
}
#[test]
fn test_comment_only_line() {
assert_eq!(
format_json(
r#"{
// this is a standalone comment
"foo": "bar"
}"#,
" "
),
r#"
{
// this is a standalone comment
"foo": "bar"
}
"#
.trim()
);
}
#[test]
fn test_multiline_block_comment() {
assert_eq!(
format_json(
r#"{
"foo": "bar"
/**
Hello World!
Hi there
*/
}"#,
" "
),
r#"
{
"foo": "bar"
/**
Hello World!
Hi there
*/
}
"#
.trim()
);
}
// NOTE: trailing whitespace on output lines is trimmed by the formatter.
// We can't easily add a test for this because raw string literals get
// trailing whitespace stripped by the editor/linter.
#[test]
fn test_comment_inside_string_ignored() {
assert_eq!(
format_json(r#"{"foo":"// not a comment","bar":"/* also not */"}"#, " "),
r#"
{
"foo": "// not a comment",
"bar": "/* also not */"
}
"#
.trim()
);
}
#[test]
fn test_comment_on_line_after_comma() {
assert_eq!(
format_json(
r#"{
"a": "aaa",
// "b": "bbb"
}"#,
" "
),
r#"
{
"a": "aaa",
// "b": "bbb"
}
"#
.trim()
);

View File

@@ -1,7 +1,6 @@
pub mod error;
pub mod escape;
pub mod format_json;
pub mod strip_json_comments;
pub mod parser;
pub mod renderer;
pub mod wasm;

View File

@@ -1,318 +0,0 @@
/// Strips JSON comments only if the result is valid JSON. If stripping comments
/// produces invalid JSON, the original text is returned unchanged.
pub fn maybe_strip_json_comments(text: &str) -> String {
let stripped = strip_json_comments(text);
if serde_json::from_str::<serde_json::Value>(&stripped).is_ok() {
stripped
} else {
text.to_string()
}
}
/// Strips comments from JSONC, preserving the original formatting as much as possible.
///
/// - Trailing comments on a line are removed (along with preceding whitespace)
/// - Whole-line comments are removed, including the line itself
/// - Block comments are removed, including any lines that become empty
/// - Comments inside strings and template tags are left alone
pub fn strip_json_comments(text: &str) -> String {
let mut chars = text.chars().peekable();
let mut result = String::with_capacity(text.len());
let mut in_string = false;
let mut in_template_tag = false;
loop {
let current_char = match chars.next() {
None => break,
Some(c) => c,
};
// Handle JSON strings
if in_string {
result.push(current_char);
match current_char {
'"' => in_string = false,
'\\' => {
if let Some(c) = chars.next() {
result.push(c);
}
}
_ => {}
}
continue;
}
// Handle template tags
if in_template_tag {
result.push(current_char);
if current_char == ']' && chars.peek() == Some(&'}') {
result.push(chars.next().unwrap());
in_template_tag = false;
}
continue;
}
// Check for template tag start
if current_char == '$' && chars.peek() == Some(&'{') {
let mut lookahead = chars.clone();
lookahead.next(); // skip {
if lookahead.peek() == Some(&'[') {
in_template_tag = true;
result.push(current_char);
result.push(chars.next().unwrap()); // {
result.push(chars.next().unwrap()); // [
continue;
}
}
// Check for line comment
if current_char == '/' && chars.peek() == Some(&'/') {
chars.next(); // skip second /
// Consume until newline
loop {
match chars.peek() {
Some(&'\n') | None => break,
Some(_) => {
chars.next();
}
}
}
// Trim trailing whitespace that preceded the comment
let trimmed_len = result.trim_end_matches(|c: char| c == ' ' || c == '\t').len();
result.truncate(trimmed_len);
continue;
}
// Check for block comment
if current_char == '/' && chars.peek() == Some(&'*') {
chars.next(); // skip *
// Consume until */
loop {
match chars.next() {
None => break,
Some('*') if chars.peek() == Some(&'/') => {
chars.next(); // skip /
break;
}
Some(_) => {}
}
}
// Trim trailing whitespace that preceded the comment
let trimmed_len = result.trim_end_matches(|c: char| c == ' ' || c == '\t').len();
result.truncate(trimmed_len);
// Skip whitespace/newline after the block comment if the next line is content
// (this handles the case where the block comment is on its own line)
continue;
}
if current_char == '"' {
in_string = true;
}
result.push(current_char);
}
// Remove lines that are now empty (were comment-only lines)
let result = result
.lines()
.filter(|line| !line.trim().is_empty())
.collect::<Vec<&str>>()
.join("\n");
// Remove trailing commas before } or ]
strip_trailing_commas(&result)
}
/// Removes trailing commas before closing braces/brackets, respecting strings.
fn strip_trailing_commas(text: &str) -> String {
let mut result = String::with_capacity(text.len());
let chars: Vec<char> = text.chars().collect();
let mut i = 0;
let mut in_string = false;
while i < chars.len() {
let ch = chars[i];
if in_string {
result.push(ch);
match ch {
'"' => in_string = false,
'\\' => {
i += 1;
if i < chars.len() {
result.push(chars[i]);
}
}
_ => {}
}
i += 1;
continue;
}
if ch == '"' {
in_string = true;
result.push(ch);
i += 1;
continue;
}
if ch == ',' {
// Look ahead past whitespace/newlines for } or ]
let mut j = i + 1;
while j < chars.len() && chars[j].is_whitespace() {
j += 1;
}
if j < chars.len() && (chars[j] == '}' || chars[j] == ']') {
// Skip the comma
i += 1;
continue;
}
}
result.push(ch);
i += 1;
}
result
}
#[cfg(test)]
mod tests {
use crate::strip_json_comments::strip_json_comments;
#[test]
fn test_no_comments() {
let input = r#"{
"foo": "bar",
"baz": 123
}"#;
assert_eq!(strip_json_comments(input), input);
}
#[test]
fn test_trailing_line_comment() {
assert_eq!(
strip_json_comments(r#"{
"foo": "bar", // this is a comment
"baz": 123
}"#),
r#"{
"foo": "bar",
"baz": 123
}"#
);
}
#[test]
fn test_whole_line_comment() {
assert_eq!(
strip_json_comments(r#"{
// this is a comment
"foo": "bar"
}"#),
r#"{
"foo": "bar"
}"#
);
}
#[test]
fn test_inline_block_comment() {
assert_eq!(
strip_json_comments(r#"{
"foo": /* a comment */ "bar"
}"#),
r#"{
"foo": "bar"
}"#
);
}
#[test]
fn test_whole_line_block_comment() {
assert_eq!(
strip_json_comments(r#"{
/* a comment */
"foo": "bar"
}"#),
r#"{
"foo": "bar"
}"#
);
}
#[test]
fn test_multiline_block_comment() {
assert_eq!(
strip_json_comments(r#"{
/**
* Hello World!
*/
"foo": "bar"
}"#),
r#"{
"foo": "bar"
}"#
);
}
#[test]
fn test_comment_inside_string_preserved() {
let input = r#"{
"foo": "// not a comment",
"bar": "/* also not */"
}"#;
assert_eq!(strip_json_comments(input), input);
}
#[test]
fn test_comment_inside_template_tag_preserved() {
let input = r#"{
"foo": ${[ fn("// hi", "/* hey */") ]}
}"#;
assert_eq!(strip_json_comments(input), input);
}
#[test]
fn test_multiple_comments() {
assert_eq!(
strip_json_comments(r#"{
// first comment
"foo": "bar", // trailing
/* block */
"baz": 123
}"#),
r#"{
"foo": "bar",
"baz": 123
}"#
);
}
#[test]
fn test_trailing_comma_after_comment_removed() {
assert_eq!(
strip_json_comments(r#"{
"a": "aaa",
// "b": "bbb"
}"#),
r#"{
"a": "aaa"
}"#
);
}
#[test]
fn test_trailing_comma_in_array() {
assert_eq!(
strip_json_comments(r#"[1, 2, /* 3 */]"#),
r#"[1, 2]"#
);
}
#[test]
fn test_comma_inside_string_preserved() {
let input = r#"{"a": "hello,}"#;
assert_eq!(strip_json_comments(input), input);
}
}

View File

@@ -14,10 +14,7 @@ url = "2"
serde_json = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["macros", "time", "test-util", "rt"] }
tokio-tungstenite = { version = "0.26.2", default-features = false, features = [
"rustls-tls-native-roots",
"connect",
] }
tokio-tungstenite = { version = "0.26.2", default-features = false, features = ["rustls-tls-native-roots", "connect"] }
yaak-http = { workspace = true }
yaak-tls = { workspace = true }
yaak-models = { workspace = true }

View File

@@ -1,8 +1,8 @@
import { invoke } from "@tauri-apps/api/core";
import { WebsocketConnection } from "@yaakapp-internal/models";
import { invoke } from '@tauri-apps/api/core';
import { WebsocketConnection } from '@yaakapp-internal/models';
export function deleteWebsocketConnections(requestId: string) {
return invoke("cmd_ws_delete_connections", {
return invoke('cmd_ws_delete_connections', {
requestId,
});
}
@@ -16,7 +16,7 @@ export function connectWebsocket({
environmentId: string | null;
cookieJarId: string | null;
}) {
return invoke("cmd_ws_connect", {
return invoke('cmd_ws_connect', {
requestId,
environmentId,
cookieJarId,
@@ -24,7 +24,7 @@ export function connectWebsocket({
}
export function closeWebsocket({ connectionId }: { connectionId: string }) {
return invoke("cmd_ws_close", {
return invoke('cmd_ws_close', {
connectionId,
});
}
@@ -36,7 +36,7 @@ export function sendWebsocket({
connectionId: string;
environmentId: string | null;
}) {
return invoke("cmd_ws_send", {
return invoke('cmd_ws_send', {
connectionId,
environmentId,
});

View File

@@ -1,6 +1,6 @@
{
"name": "@yaakapp-internal/ws",
"version": "1.0.0",
"private": true,
"version": "1.0.0",
"main": "index.ts"
}

View File

@@ -2,7 +2,7 @@ use log::info;
use serde_json::Value;
use std::collections::BTreeMap;
use yaak_http::path_placeholders::apply_path_placeholders;
use yaak_models::models::{Environment, GrpcRequest, HttpRequest, HttpRequestHeader, HttpUrlParameter};
use yaak_models::models::{Environment, HttpRequest, HttpRequestHeader, HttpUrlParameter};
use yaak_models::render::make_vars_hashmap;
use yaak_templates::{RenderOptions, TemplateCallback, parse_and_render, render_json_value_raw};
@@ -89,64 +89,6 @@ pub async fn render_http_request<T: TemplateCallback>(
Ok(HttpRequest { url, url_parameters, headers, body, authentication, ..request.to_owned() })
}
pub async fn render_grpc_request<T: TemplateCallback>(
r: &GrpcRequest,
environment_chain: Vec<Environment>,
cb: &T,
opt: &RenderOptions,
) -> yaak_templates::error::Result<GrpcRequest> {
let vars = &make_vars_hashmap(environment_chain);
let mut metadata = Vec::new();
for p in r.metadata.clone() {
if !p.enabled {
continue;
}
metadata.push(HttpRequestHeader {
enabled: p.enabled,
name: parse_and_render(p.name.as_str(), vars, cb, opt).await?,
value: parse_and_render(p.value.as_str(), vars, cb, opt).await?,
id: p.id,
})
}
let authentication = {
let mut disabled = false;
let mut auth = BTreeMap::new();
match r.authentication.get("disabled") {
Some(Value::Bool(true)) => {
disabled = true;
}
Some(Value::String(tmpl)) => {
disabled = parse_and_render(tmpl.as_str(), vars, cb, opt)
.await
.unwrap_or_default()
.is_empty();
info!(
"Rendering authentication.disabled as a template: {disabled} from \"{tmpl}\""
);
}
_ => {}
}
if disabled {
auth.insert("disabled".to_string(), Value::Bool(true));
} else {
for (k, v) in r.authentication.clone() {
if k == "disabled" {
auth.insert(k, Value::Bool(false));
} else {
auth.insert(k, render_json_value_raw(v, vars, cb, opt).await?);
}
}
}
auth
};
let url = parse_and_render(r.url.as_str(), vars, cb, opt).await?;
Ok(GrpcRequest { url, metadata, authentication, ..r.to_owned() })
}
fn strip_disabled_form_entries(v: Value) -> Value {
match v {
Value::Array(items) => Value::Array(

View File

@@ -5,10 +5,6 @@
"type": "git",
"url": "git+https://github.com/mountain-loop/yaak.git"
},
"os": [
"darwin"
],
"cpu": [
"arm64"
]
"os": ["darwin"],
"cpu": ["arm64"]
}

View File

@@ -5,10 +5,6 @@
"type": "git",
"url": "git+https://github.com/mountain-loop/yaak.git"
},
"os": [
"darwin"
],
"cpu": [
"x64"
]
"os": ["darwin"],
"cpu": ["x64"]
}

View File

@@ -5,10 +5,6 @@
"type": "git",
"url": "git+https://github.com/mountain-loop/yaak.git"
},
"os": [
"linux"
],
"cpu": [
"arm64"
]
"os": ["linux"],
"cpu": ["arm64"]
}

View File

@@ -5,10 +5,6 @@
"type": "git",
"url": "git+https://github.com/mountain-loop/yaak.git"
},
"os": [
"linux"
],
"cpu": [
"x64"
]
"os": ["linux"],
"cpu": ["x64"]
}

View File

@@ -5,10 +5,6 @@
"type": "git",
"url": "git+https://github.com/mountain-loop/yaak.git"
},
"os": [
"win32"
],
"cpu": [
"arm64"
]
"os": ["win32"],
"cpu": ["arm64"]
}

View File

@@ -5,10 +5,6 @@
"type": "git",
"url": "git+https://github.com/mountain-loop/yaak.git"
},
"os": [
"win32"
],
"cpu": [
"x64"
]
"os": ["win32"],
"cpu": ["x64"]
}

View File

@@ -4,7 +4,7 @@ const BINARY_DISTRIBUTION_PACKAGES = {
linux_arm64: "@yaakapp/cli-linux-arm64",
linux_x64: "@yaakapp/cli-linux-x64",
win32_x64: "@yaakapp/cli-win32-x64",
win32_arm64: "@yaakapp/cli-win32-arm64",
win32_arm64: "@yaakapp/cli-win32-arm64"
};
const BINARY_DISTRIBUTION_VERSION = require("./package.json").version;
@@ -16,5 +16,5 @@ module.exports = {
BINARY_DISTRIBUTION_PACKAGES,
BINARY_DISTRIBUTION_VERSION,
BINARY_NAME,
PLATFORM_SPECIFIC_PACKAGE_NAME,
PLATFORM_SPECIFIC_PACKAGE_NAME
};

View File

@@ -5,7 +5,7 @@ const https = require("node:https");
const {
BINARY_DISTRIBUTION_VERSION,
BINARY_NAME,
PLATFORM_SPECIFIC_PACKAGE_NAME,
PLATFORM_SPECIFIC_PACKAGE_NAME
} = require("./common");
const fallbackBinaryPath = path.join(__dirname, BINARY_NAME);
@@ -27,8 +27,8 @@ function makeRequest(url) {
} else {
reject(
new Error(
`npm responded with status code ${response.statusCode} when downloading package ${url}`,
),
`npm responded with status code ${response.statusCode} when downloading package ${url}`
)
);
}
})

View File

@@ -1,25 +1,25 @@
{
"name": "@yaakapp/cli",
"version": "0.0.1",
"main": "./index.js",
"repository": {
"type": "git",
"url": "git+https://github.com/mountain-loop/yaak.git"
},
"scripts": {
"postinstall": "node ./install.js",
"prepublishOnly": "node ./prepublish.js"
},
"bin": {
"yaak": "bin/cli.js",
"yaakcli": "bin/cli.js"
},
"main": "./index.js",
"scripts": {
"postinstall": "node ./install.js",
"prepublishOnly": "node ./prepublish.js"
},
"optionalDependencies": {
"@yaakapp/cli-darwin-arm64": "0.0.1",
"@yaakapp/cli-darwin-x64": "0.0.1",
"@yaakapp/cli-darwin-arm64": "0.0.1",
"@yaakapp/cli-linux-arm64": "0.0.1",
"@yaakapp/cli-linux-x64": "0.0.1",
"@yaakapp/cli-win32-arm64": "0.0.1",
"@yaakapp/cli-win32-x64": "0.0.1"
"@yaakapp/cli-win32-x64": "0.0.1",
"@yaakapp/cli-win32-arm64": "0.0.1"
}
}

View File

@@ -14,34 +14,34 @@ const packages = [
"cli-linux-arm64",
"cli-linux-x64",
"cli-win32-arm64",
"cli-win32-x64",
"cli-win32-x64"
];
const binaries = [
{
src: join(__dirname, "dist", "cli-darwin-arm64", "yaak"),
dest: join(__dirname, "cli-darwin-arm64", "bin", "yaak"),
dest: join(__dirname, "cli-darwin-arm64", "bin", "yaak")
},
{
src: join(__dirname, "dist", "cli-darwin-x64", "yaak"),
dest: join(__dirname, "cli-darwin-x64", "bin", "yaak"),
dest: join(__dirname, "cli-darwin-x64", "bin", "yaak")
},
{
src: join(__dirname, "dist", "cli-linux-arm64", "yaak"),
dest: join(__dirname, "cli-linux-arm64", "bin", "yaak"),
dest: join(__dirname, "cli-linux-arm64", "bin", "yaak")
},
{
src: join(__dirname, "dist", "cli-linux-x64", "yaak"),
dest: join(__dirname, "cli-linux-x64", "bin", "yaak"),
dest: join(__dirname, "cli-linux-x64", "bin", "yaak")
},
{
src: join(__dirname, "dist", "cli-win32-arm64", "yaak.exe"),
dest: join(__dirname, "cli-win32-arm64", "bin", "yaak.exe"),
dest: join(__dirname, "cli-win32-arm64", "bin", "yaak.exe")
},
{
src: join(__dirname, "dist", "cli-win32-x64", "yaak.exe"),
dest: join(__dirname, "cli-win32-x64", "bin", "yaak.exe"),
},
dest: join(__dirname, "cli-win32-x64", "bin", "yaak.exe")
}
];
for (const { src, dest } of binaries) {
@@ -67,7 +67,7 @@ for (const pkg of packages) {
"@yaakapp/cli-linux-arm64": version,
"@yaakapp/cli-linux-x64": version,
"@yaakapp/cli-win32-x64": version,
"@yaakapp/cli-win32-arm64": version,
"@yaakapp/cli-win32-arm64": version
};
}

3620
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "yaak-app",
"version": "0.0.0",
"private": true,
"version": "0.0.0",
"repository": {
"type": "git",
"url": "git+https://github.com/mountain-loop/yaak.git"
@@ -63,7 +63,7 @@
"src-web"
],
"scripts": {
"prepare": "vp config",
"prepare": "husky",
"init": "npm install && npm run bootstrap",
"start": "npm run app-dev",
"app-build": "tauri build",
@@ -82,36 +82,34 @@
"vendor:vendor-plugins": "node scripts/vendor-plugins.cjs",
"vendor:vendor-protoc": "node scripts/vendor-protoc.cjs",
"vendor:vendor-node": "node scripts/vendor-node.cjs",
"format": "vp fmt --ignore-path .oxfmtignore",
"lint": "run-p lint:*",
"lint:vp": "vp lint",
"lint:workspaces": "npm run --workspaces --if-present lint",
"lint:biome": "biome lint",
"lint:extra": "npm run --workspaces --if-present lint",
"format": "biome format --write .",
"replace-version": "node scripts/replace-version.cjs",
"tauri": "tauri",
"tauri-before-build": "npm run bootstrap",
"tauri-before-dev": "node scripts/run-workspaces-dev.mjs"
},
"overrides": {
"js-yaml": "^4.1.1"
},
"devDependencies": {
"@biomejs/biome": "^2.3.13",
"@tauri-apps/cli": "^2.9.6",
"@yaakapp/cli": "^0.5.1",
"dotenv-cli": "^11.0.0",
"husky": "^9.1.7",
"nodejs-file-downloader": "^4.13.0",
"npm-run-all": "^4.1.5",
"typescript": "^5.8.3",
"vitest": "^3.2.4"
},
"dependencies": {
"@codemirror/lang-go": "^6.0.1",
"@codemirror/lang-java": "^6.0.2",
"@codemirror/lang-php": "^6.0.2",
"@codemirror/lang-python": "^6.2.1",
"@codemirror/legacy-modes": "^6.5.2"
},
"devDependencies": {
"@tauri-apps/cli": "^2.9.6",
"@yaakapp/cli": "^0.5.1",
"dotenv-cli": "^11.0.0",
"nodejs-file-downloader": "^4.13.0",
"npm-run-all": "^4.1.5",
"typescript": "^5.8.3",
"vite-plus": "latest",
"vitest": "npm:@voidzero-dev/vite-plus-test@latest"
},
"overrides": {
"js-yaml": "^4.1.1",
"vite": "npm:@voidzero-dev/vite-plus-core@latest",
"vitest": "npm:@voidzero-dev/vite-plus-test@latest"
},
"packageManager": "npm@11.11.1"
}
}

View File

@@ -1,7 +1,7 @@
// oxlint-disable-next-line no-explicit-any
// biome-ignore lint/suspicious/noExplicitAny: none
export function debounce(fn: (...args: any[]) => void, delay = 500) {
let timer: ReturnType<typeof setTimeout>;
// oxlint-disable-next-line no-explicit-any
// biome-ignore lint/suspicious/noExplicitAny: none
const result = (...args: any[]) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);

View File

@@ -4,16 +4,16 @@ export function formatSize(bytes: number): string {
if (bytes > 1000 * 1000 * 1000) {
num = bytes / 1000 / 1000 / 1000;
unit = "GB";
unit = 'GB';
} else if (bytes > 1000 * 1000) {
num = bytes / 1000 / 1000;
unit = "MB";
unit = 'MB';
} else if (bytes > 1000) {
num = bytes / 1000;
unit = "KB";
unit = 'KB';
} else {
num = bytes;
unit = "B";
unit = 'B';
}
return `${Math.round(num * 10) / 10} ${unit}`;

View File

@@ -1,3 +1,3 @@
export * from "./debounce";
export * from "./formatSize";
export * from "./templateFunction";
export * from './debounce';
export * from './formatSize';
export * from './templateFunction';

View File

@@ -1,6 +1,6 @@
{
"name": "@yaakapp-internal/lib",
"version": "1.0.0",
"private": true,
"version": "1.0.0",
"main": "index.ts"
}

View File

@@ -2,20 +2,20 @@ import type {
CallTemplateFunctionArgs,
JsonPrimitive,
TemplateFunctionArg,
} from "@yaakapp-internal/plugins";
} from '@yaakapp-internal/plugins';
export function validateTemplateFunctionArgs(
fnName: string,
args: TemplateFunctionArg[],
values: CallTemplateFunctionArgs["values"],
values: CallTemplateFunctionArgs['values'],
): string | null {
for (const arg of args) {
if ("inputs" in arg && arg.inputs) {
if ('inputs' in arg && arg.inputs) {
// Recurse down
const err = validateTemplateFunctionArgs(fnName, arg.inputs, values);
if (err) return err;
}
if (!("name" in arg)) continue;
if (!('name' in arg)) continue;
if (arg.optional) continue;
if (arg.defaultValue != null) continue;
if (arg.hidden) continue;
@@ -34,14 +34,14 @@ export function applyFormInputDefaults(
) {
let newValues: { [p: string]: JsonPrimitive | undefined } = { ...values };
for (const input of inputs) {
if ("defaultValue" in input && values[input.name] === undefined) {
if ('defaultValue' in input && values[input.name] === undefined) {
newValues[input.name] = input.defaultValue;
}
if (input.type === "checkbox" && values[input.name] === undefined) {
if (input.type === 'checkbox' && values[input.name] === undefined) {
newValues[input.name] = false;
}
// Recurse down to all child inputs
if ("inputs" in input) {
if ('inputs' in input) {
newValues = applyFormInputDefaults(input.inputs ?? [], newValues);
}
}

View File

@@ -3,23 +3,23 @@
"version": "0.8.0",
"keywords": [
"api-client",
"bruno-alternative",
"insomnia-alternative",
"bruno-alternative",
"postman-alternative"
],
"homepage": "https://yaak.app",
"bugs": {
"url": "https://feedback.yaak.app"
},
"repository": {
"type": "git",
"url": "https://github.com/mountain-loop/yaak"
},
"bugs": {
"url": "https://feedback.yaak.app"
},
"homepage": "https://yaak.app",
"main": "lib/index.js",
"typings": "./lib/index.d.ts",
"files": [
"lib/**/*"
],
"main": "lib/index.js",
"typings": "./lib/index.d.ts",
"scripts": {
"bootstrap": "npm run build",
"build": "run-s build:copy-types build:tsc",

View File

@@ -18,12 +18,12 @@ export type CallHttpAuthenticationActionRequest = { index: number, pluginRefId:
export type CallHttpAuthenticationRequest = { contextId: string, values: { [key in string]?: JsonPrimitive }, method: string, url: string, headers: Array<HttpHeader>, };
export type CallHttpAuthenticationResponse = {
export type CallHttpAuthenticationResponse = {
/**
* HTTP headers to add to the request. Existing headers will be replaced, while
* new headers will be added.
*/
setHeaders?: Array<HttpHeader>,
setHeaders?: Array<HttpHeader>,
/**
* Query parameters to add to the request. Existing params will be replaced, while
* new params will be added.
@@ -78,7 +78,7 @@ export type ExportHttpRequestRequest = { httpRequest: HttpRequest, };
export type ExportHttpRequestResponse = { content: string, };
export type FileFilter = { name: string,
export type FileFilter = { name: string,
/**
* File extensions to require
*/
@@ -100,149 +100,149 @@ export type FormInputAccordion = { label: string, inputs?: Array<FormInput>, hid
export type FormInputBanner = { inputs?: Array<FormInput>, hidden?: boolean, color?: Color, };
export type FormInputBase = {
export type FormInputBase = {
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
*/
name: string,
name: string,
/**
* Whether this input is visible for the given configuration. Use this to
* make branching forms.
*/
hidden?: boolean,
hidden?: boolean,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
optional?: boolean,
/**
* The label of the input
*/
label?: string,
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean,
defaultValue?: string, disabled?: boolean,
/**
* Longer description of the input, likely shown in a tooltip
*/
description?: string, };
export type FormInputCheckbox = {
export type FormInputCheckbox = {
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
*/
name: string,
name: string,
/**
* Whether this input is visible for the given configuration. Use this to
* make branching forms.
*/
hidden?: boolean,
hidden?: boolean,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
optional?: boolean,
/**
* The label of the input
*/
label?: string,
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean,
defaultValue?: string, disabled?: boolean,
/**
* Longer description of the input, likely shown in a tooltip
*/
description?: string, };
export type FormInputEditor = {
export type FormInputEditor = {
/**
* Placeholder for the text input
*/
placeholder?: string | null,
placeholder?: string | null,
/**
* Don't show the editor gutter (line numbers, folds, etc.)
*/
hideGutter?: boolean,
hideGutter?: boolean,
/**
* Language for syntax highlighting
*/
language?: EditorLanguage, readOnly?: boolean,
language?: EditorLanguage, readOnly?: boolean,
/**
* Fixed number of visible rows
*/
rows?: number, completionOptions?: Array<GenericCompletionOption>,
rows?: number, completionOptions?: Array<GenericCompletionOption>,
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
*/
name: string,
name: string,
/**
* Whether this input is visible for the given configuration. Use this to
* make branching forms.
*/
hidden?: boolean,
hidden?: boolean,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
optional?: boolean,
/**
* The label of the input
*/
label?: string,
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean,
defaultValue?: string, disabled?: boolean,
/**
* Longer description of the input, likely shown in a tooltip
*/
description?: string, };
export type FormInputFile = {
export type FormInputFile = {
/**
* The title of the file selection window
*/
title: string,
title: string,
/**
* Allow selecting multiple files
*/
multiple?: boolean, directory?: boolean, defaultPath?: string, filters?: Array<FileFilter>,
multiple?: boolean, directory?: boolean, defaultPath?: string, filters?: Array<FileFilter>,
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
*/
name: string,
name: string,
/**
* Whether this input is visible for the given configuration. Use this to
* make branching forms.
*/
hidden?: boolean,
hidden?: boolean,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
optional?: boolean,
/**
* The label of the input
*/
label?: string,
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean,
defaultValue?: string, disabled?: boolean,
/**
* Longer description of the input, likely shown in a tooltip
*/
@@ -250,63 +250,63 @@ description?: string, };
export type FormInputHStack = { inputs?: Array<FormInput>, hidden?: boolean, };
export type FormInputHttpRequest = {
export type FormInputHttpRequest = {
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
*/
name: string,
name: string,
/**
* Whether this input is visible for the given configuration. Use this to
* make branching forms.
*/
hidden?: boolean,
hidden?: boolean,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
optional?: boolean,
/**
* The label of the input
*/
label?: string,
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean,
defaultValue?: string, disabled?: boolean,
/**
* Longer description of the input, likely shown in a tooltip
*/
description?: string, };
export type FormInputKeyValue = {
export type FormInputKeyValue = {
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
*/
name: string,
name: string,
/**
* Whether this input is visible for the given configuration. Use this to
* make branching forms.
*/
hidden?: boolean,
hidden?: boolean,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
optional?: boolean,
/**
* The label of the input
*/
label?: string,
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean,
defaultValue?: string, disabled?: boolean,
/**
* Longer description of the input, likely shown in a tooltip
*/
@@ -314,36 +314,36 @@ description?: string, };
export type FormInputMarkdown = { content: string, hidden?: boolean, };
export type FormInputSelect = {
export type FormInputSelect = {
/**
* The options that will be available in the select input
*/
options: Array<FormInputSelectOption>,
options: Array<FormInputSelectOption>,
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
*/
name: string,
name: string,
/**
* Whether this input is visible for the given configuration. Use this to
* make branching forms.
*/
hidden?: boolean,
hidden?: boolean,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
optional?: boolean,
/**
* The label of the input
*/
label?: string,
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean,
defaultValue?: string, disabled?: boolean,
/**
* Longer description of the input, likely shown in a tooltip
*/
@@ -351,44 +351,44 @@ description?: string, };
export type FormInputSelectOption = { label: string, value: string, };
export type FormInputText = {
export type FormInputText = {
/**
* Placeholder for the text input
*/
placeholder?: string | null,
placeholder?: string | null,
/**
* Placeholder for the text input
*/
password?: boolean,
password?: boolean,
/**
* Whether to allow newlines in the input, like a <textarea/>
*/
multiLine?: boolean, completionOptions?: Array<GenericCompletionOption>,
multiLine?: boolean, completionOptions?: Array<GenericCompletionOption>,
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
*/
name: string,
name: string,
/**
* Whether this input is visible for the given configuration. Use this to
* make branching forms.
*/
hidden?: boolean,
hidden?: boolean,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
optional?: boolean,
/**
* The label of the input
*/
label?: string,
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean,
defaultValue?: string, disabled?: boolean,
/**
* Longer description of the input, likely shown in a tooltip
*/
@@ -474,7 +474,7 @@ export type ListOpenWorkspacesResponse = { workspaces: Array<WorkspaceInfo>, };
export type OpenExternalUrlRequest = { url: string, };
export type OpenWindowRequest = { url: string,
export type OpenWindowRequest = { url: string,
/**
* Label for the window. If not provided, a random one will be generated.
*/
@@ -486,15 +486,15 @@ export type PromptFormRequest = { id: string, title: string, description?: strin
export type PromptFormResponse = { values: { [key in string]?: JsonPrimitive } | null, done?: boolean, };
export type PromptTextRequest = { id: string, title: string, label: string, description?: string, defaultValue?: string, placeholder?: string,
export type PromptTextRequest = { id: string, title: string, label: string, description?: string, defaultValue?: string, placeholder?: string,
/**
* Text to add to the confirmation button
*/
confirmText?: string, password?: boolean,
confirmText?: string, password?: boolean,
/**
* Text to add to the cancel button
*/
cancelText?: string,
cancelText?: string,
/**
* Require the user to enter a non-empty value
*/
@@ -524,12 +524,12 @@ export type SetKeyValueResponse = {};
export type ShowToastRequest = { message: string, color?: Color, icon?: Icon, timeout?: number, };
export type TemplateFunction = { name: string, previewType?: TemplateFunctionPreviewType, description?: string,
export type TemplateFunction = { name: string, previewType?: TemplateFunctionPreviewType, description?: string,
/**
* Also support alternative names. This is useful for not breaking existing
* tags when changing the `name` property
*/
aliases?: Array<string>, args: Array<TemplateFunctionArg>,
aliases?: Array<string>, args: Array<TemplateFunctionArg>,
/**
* A list of arg names to show in the inline preview. If not provided, none will be shown (for privacy reasons).
*/
@@ -546,23 +546,23 @@ export type TemplateRenderRequest = { data: JsonValue, purpose: RenderPurpose, }
export type TemplateRenderResponse = { data: JsonValue, };
export type Theme = {
export type Theme = {
/**
* How the theme is identified. This should never be changed
*/
id: string,
id: string,
/**
* The friendly name of the theme to be displayed to the user
*/
label: string,
label: string,
/**
* Whether the theme will be used for dark or light appearance
*/
dark: boolean,
dark: boolean,
/**
* The default top-level colors for the theme
*/
base: ThemeComponentColors,
base: ThemeComponentColors,
/**
* Optionally override theme for individual UI components for more control
*/

View File

@@ -18,12 +18,7 @@ export type EditorKeymap = "default" | "vim" | "vscode" | "emacs";
export type EncryptedKey = { encryptedKey: string, };
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, parentModel: string, parentId: string | null,
/**
* Variables defined in this environment scope.
* Child environments override parent variables by name.
*/
variables: Array<EnvironmentVariable>, color: string | null, sortPriority: number, };
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, parentModel: string, parentId: string | null, variables: Array<EnvironmentVariable>, color: string | null, sortPriority: number, };
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
@@ -39,17 +34,9 @@ export type GrpcEvent = { model: "grpc_event", id: string, createdAt: string, up
export type GrpcEventType = "info" | "error" | "client_message" | "server_message" | "connection_start" | "connection_end";
export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record<string, any>, description: string, message: string, metadata: Array<HttpRequestHeader>, method: string | null, name: string, service: string | null, sortPriority: number,
/**
* Server URL (http for plaintext or https for secure)
*/
url: string, };
export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record<string, any>, description: string, message: string, metadata: Array<HttpRequestHeader>, method: string | null, name: string, service: string | null, sortPriority: number, url: string, };
export type HttpRequest = { model: "http_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, body: Record<string, any>, bodyType: string | null, description: string, headers: Array<HttpRequestHeader>, method: string, name: string, sortPriority: number, url: string,
/**
* URL parameters used for both path placeholders (`:id`) and query string entries.
*/
urlParameters: Array<HttpUrlParameter>, };
export type HttpRequest = { model: "http_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, body: Record<string, any>, bodyType: string | null, description: string, headers: Array<HttpRequestHeader>, method: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, id?: string, };
@@ -62,24 +49,17 @@ export type HttpResponseEvent = { model: "http_response_event", id: string, crea
* This mirrors `yaak_http::sender::HttpResponseEvent` but with serde support.
* The `From` impl is in yaak-http to avoid circular dependencies.
*/
export type HttpResponseEventData = { "type": "setting", name: string, value: string, } | { "type": "info", message: string, } | { "type": "redirect", url: string, status: number, behavior: string, dropped_body: boolean, dropped_headers: Array<string>, } | { "type": "send_url", method: string, scheme: string, username: string, password: string, host: string, port: number, path: string, query: string, fragment: string, } | { "type": "receive_url", version: string, status: string, } | { "type": "header_up", name: string, value: string, } | { "type": "header_down", name: string, value: string, } | { "type": "chunk_sent", bytes: number, } | { "type": "chunk_received", bytes: number, } | { "type": "dns_resolved", hostname: string, addresses: Array<string>, duration: bigint, overridden: boolean, };
export type HttpResponseEventData = { "type": "setting", name: string, value: string, } | { "type": "info", message: string, } | { "type": "redirect", url: string, status: number, behavior: string, } | { "type": "send_url", method: string, scheme: string, username: string, password: string, host: string, port: number, path: string, query: string, fragment: string, } | { "type": "receive_url", version: string, status: string, } | { "type": "header_up", name: string, value: string, } | { "type": "header_down", name: string, value: string, } | { "type": "chunk_sent", bytes: number, } | { "type": "chunk_received", bytes: number, } | { "type": "dns_resolved", hostname: string, addresses: Array<string>, duration: bigint, overridden: boolean, };
export type HttpResponseHeader = { name: string, value: string, };
export type HttpResponseState = "initialized" | "connected" | "closed";
export type HttpUrlParameter = { enabled?: boolean,
/**
* Colon-prefixed parameters are treated as path parameters if they match, like `/users/:id`
* Other entries are appended as query parameters
*/
name: string, value: string, id?: string, };
export type HttpUrlParameter = { enabled?: boolean, name: string, value: string, id?: string, };
export type KeyValue = { model: "key_value", id: string, createdAt: string, updatedAt: string, key: string, namespace: string, value: string, };
export type Plugin = { model: "plugin", id: string, createdAt: string, updatedAt: string, checkedAt: string | null, directory: string, enabled: boolean, url: string | null, source: PluginSource, };
export type PluginSource = "bundled" | "filesystem" | "registry";
export type Plugin = { model: "plugin", id: string, createdAt: string, updatedAt: string, checkedAt: string | null, directory: string, enabled: boolean, url: string | null, };
export type ProxySetting = { "type": "enabled", http: string, https: string, auth: ProxySettingAuth | null, bypass: string, disabled: boolean, } | { "type": "disabled" };
@@ -97,11 +77,7 @@ export type WebsocketEvent = { model: "websocket_event", id: string, createdAt:
export type WebsocketEventType = "binary" | "close" | "frame" | "open" | "ping" | "pong" | "text";
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string,
/**
* URL parameters used for both path placeholders (`:id`) and query string entries.
*/
urlParameters: Array<HttpUrlParameter>, };
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, settingDnsOverrides: Array<DnsOverride>, };

Some files were not shown because too many files have changed in this diff Show More