Compare commits

...

14 Commits

Author SHA1 Message Date
Gregory Schier
5919fae739 Run oxfmt across repo, add format script and ignore config
Format all non-generated files with oxfmt via `vp fmt`. Add
.oxfmtignore to skip bindings/ and wasm-pack output. Add npm
format script and update DEVELOPMENT.md docs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 09:52:11 -07:00
Gregory Schier
a9cccb21b8 Use npm commands in DEVELOPMENT.md for contributor familiarity
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 09:49:03 -07:00
Gregory Schier
1c0435e3ff Update DEVELOPMENT.md for Vite+ toolchain
Replace Biome references with Vite+ (oxlint/oxfmt), add vp as a
prerequisite, and document pre-commit hook and VS Code extensions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 09:48:21 -07:00
Gregory Schier
a629a1fa79 Fix wasm-pack output for Vite 8/rolldown production builds
Rewrite generated yaak_templates.js to use Vite's ?init import style
instead of the ES Module Integration style that rolldown doesn't support.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 09:02:12 -07:00
Gregory Schier
dde8d61b4b Run bootstrap 2026-03-13 08:50:43 -07:00
Gregory Schier
08a64b6938 VSCode suggestion and CI 2026-03-13 08:44:52 -07:00
Gregory Schier
560c4667e4 More fixes 2026-03-13 08:32:40 -07:00
Gregory Schier
21f775741a More fixes 2026-03-13 08:29:36 -07:00
Gregory Schier
44a331929f Enable type-aware linting and replace biome-ignore with oxlint-disable
- Enable typeAware option and no-explicit-any (error) in vite.config.ts
- Ignore generated binding files from linting
- Convert all 96 biome-ignore comments to oxlint-disable equivalents
- Add suppression comments for 3 previously uncovered any usages

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 08:24:45 -07:00
Gregory Schier
7670ab007f Fix lint errors 2026-03-13 08:12:21 -07:00
Gregory Schier
be34dfe74a Fix no-base-to-string 2026-03-13 07:48:50 -07:00
Gregory Schier
e4103f1a4a Fix some lint 2026-03-13 07:36:26 -07:00
Gregory Schier
7f4eedd630 Get lint running 2026-03-13 07:32:55 -07:00
Gregory Schier
49659a3da9 Migrate to Vite+ (vite-plus) unified toolchain
Replace Vite/Vitest with vite-plus, update WASM loading to native Vite 8
?init pattern, switch React compiler to @rolldown/plugin-babel, and
migrate git hooks from husky to vite-hooks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 07:23:11 -07:00
677 changed files with 15295 additions and 14906 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

1
.node-version Normal file
View File

@@ -0,0 +1 @@
24.14.0

2
.oxfmtignore Normal file
View File

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

1
.vite-hooks/pre-commit Normal file
View File

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

View File

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

View File

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

View File

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

View File

@@ -11,14 +11,16 @@ begin.
Make sure you have the following tools installed: Make sure you have the following tools installed:
- [Node.js](https://nodejs.org/en/download/package-manager) - [Node.js](https://nodejs.org/en/download/package-manager) (v24+)
- [Rust](https://www.rust-lang.org/tools/install) - [Rust](https://www.rust-lang.org/tools/install)
- [Vite+](https://vite.dev/guide/vite-plus) (`vp` CLI)
Check the installations with the following commands: Check the installations with the following commands:
```shell ```shell
node -v node -v
npm -v npm -v
vp --version
rustc --version rustc --version
``` ```
@@ -61,9 +63,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 lezer-generator components/core/Editor/<LANG>/<LANG>.grammar > components/core/Editor/<LANG>/<LANG>.ts
``` ```
## Linting & Formatting ## Linting and Formatting
This repo uses Biome for linting and formatting (replacing ESLint + Prettier). This repo uses [Vite+](https://vite.dev/guide/vite-plus) for linting (oxlint) and formatting (oxfmt).
- Lint the entire repo: - Lint the entire repo:
@@ -71,12 +73,6 @@ This repo uses Biome for linting and formatting (replacing ESLint + Prettier).
npm run lint npm run lint
``` ```
- Auto-fix lint issues where possible:
```sh
npm run lint:fix
```
- Format code: - Format code:
```sh ```sh
@@ -84,5 +80,7 @@ npm run format
``` ```
Notes: Notes:
- 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. - 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.

View File

@@ -16,8 +16,6 @@
</p> </p>
<br> <br>
<p align="center"> <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 --> <!-- 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> </p>
@@ -27,13 +25,11 @@
![Yaak API Client](https://yaak.app/static/screenshot.png) ![Yaak API Client](https://yaak.app/static/screenshot.png)
## Features ## 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. 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. 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 ### 🌐 Work with any API
- Import collections from Postman, Insomnia, OpenAPI, Swagger, or Curl. - Import collections from Postman, Insomnia, OpenAPI, Swagger, or Curl.
@@ -41,21 +37,23 @@ Built with [Tauri](https://tauri.app), Rust, and React, its fast, lightweight
- Filter and inspect responses with JSONPath or XPath. - Filter and inspect responses with JSONPath or XPath.
### 🔐 Stay secure ### 🔐 Stay secure
- Use OAuth 2.0, JWT, Basic Auth, or custom plugins for authentication. - 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. - Store secrets in your OS keychain.
### ☁️ Organize & collaborate ### ☁️ Organize & collaborate
- Group requests into workspaces and nested folders. - Group requests into workspaces and nested folders.
- Use environment variables to switch between dev, staging, and prod. - Use environment variables to switch between dev, staging, and prod.
- Mirror workspaces to your filesystem for versioning in Git or syncing with Dropbox. - Mirror workspaces to your filesystem for versioning in Git or syncing with Dropbox.
### 🧩 Extend & customize ### 🧩 Extend & customize
- Insert dynamic values like UUIDs or timestamps with template tags. - Insert dynamic values like UUIDs or timestamps with template tags.
- Pick from built-in themes or build your own. - Pick from built-in themes or build your own.
- Create plugins to extend authentication, template tags, or the UI. - Create plugins to extend authentication, template tags, or the UI.
## Contribution Policy ## Contribution Policy
> [!IMPORTANT] > [!IMPORTANT]

View File

@@ -1,55 +0,0 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.13/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",
"!npm"
]
}
}

View File

@@ -29,7 +29,14 @@ schemars = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
sha2 = { 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" walkdir = "2"
webbrowser = "1" webbrowser = "1"
zip = "4" zip = "4"

View File

@@ -35,7 +35,16 @@ r2d2 = "0.8.10"
r2d2_sqlite = "0.25.0" r2d2_sqlite = "0.25.0"
mime_guess = "2.0.5" mime_guess = "2.0.5"
rand = "0.9.0" 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 = { workspace = true, features = ["derive"] }
serde_json = { workspace = true, features = ["raw_value"] } serde_json = { workspace = true, features = ["raw_value"] }
tauri = { workspace = true, features = ["devtools", "protocol-asset"] } tauri = { workspace = true, features = ["devtools", "protocol-asset"] }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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) { export function enableEncryption(workspaceId: string) {
return invoke<void>('cmd_enable_encryption', { workspaceId }); return invoke<void>("cmd_enable_encryption", { workspaceId });
} }
export function revealWorkspaceKey(workspaceId: string) { 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 }) { 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) { 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", "name": "@yaakapp-internal/crypto",
"private": true,
"version": "1.0.0", "version": "1.0.0",
"private": true,
"main": "index.ts" "main": "index.ts"
} }

View File

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

View File

@@ -19,7 +19,12 @@ hyper-util = { version = "0.1.17", default-features = false, features = ["client
log = { workspace = true } log = { workspace = true }
mime_guess = "2.0.5" mime_guess = "2.0.5"
regex = "1.11.1" 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 = { workspace = true, features = ["derive"] }
serde_json = { workspace = true } serde_json = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }

View File

@@ -1,35 +1,39 @@
import { atom } from 'jotai'; import { atom } from "jotai";
import { selectAtom } from 'jotai/utils'; import { selectAtom } from "jotai/utils";
import type { AnyModel } from '../bindings/gen_models'; import type { AnyModel } from "../bindings/gen_models";
import { ExtractModel } from './types'; import { ExtractModel } from "./types";
import { newStoreData } from './util'; import { newStoreData } from "./util";
export const modelStoreDataAtom = atom(newStoreData()); export const modelStoreDataAtom = atom(newStoreData());
export const cookieJarsAtom = createOrderedModelAtom('cookie_jar', 'name', 'asc'); export const cookieJarsAtom = createOrderedModelAtom("cookie_jar", "name", "asc");
export const environmentsAtom = createOrderedModelAtom('environment', 'sortPriority', 'asc'); export const environmentsAtom = createOrderedModelAtom("environment", "sortPriority", "asc");
export const foldersAtom = createModelAtom('folder'); export const foldersAtom = createModelAtom("folder");
export const grpcConnectionsAtom = createOrderedModelAtom('grpc_connection', 'createdAt', 'desc'); export const grpcConnectionsAtom = createOrderedModelAtom("grpc_connection", "createdAt", "desc");
export const grpcEventsAtom = createOrderedModelAtom('grpc_event', 'createdAt', 'asc'); export const grpcEventsAtom = createOrderedModelAtom("grpc_event", "createdAt", "asc");
export const grpcRequestsAtom = createModelAtom('grpc_request'); export const grpcRequestsAtom = createModelAtom("grpc_request");
export const httpRequestsAtom = createModelAtom('http_request'); export const httpRequestsAtom = createModelAtom("http_request");
export const httpResponsesAtom = createOrderedModelAtom('http_response', 'createdAt', 'desc'); export const httpResponsesAtom = createOrderedModelAtom("http_response", "createdAt", "desc");
export const httpResponseEventsAtom = createOrderedModelAtom('http_response_event', 'createdAt', 'asc'); export const httpResponseEventsAtom = createOrderedModelAtom(
export const keyValuesAtom = createModelAtom('key_value'); "http_response_event",
export const pluginsAtom = createModelAtom('plugin'); "createdAt",
export const settingsAtom = createSingularModelAtom('settings'); "asc",
export const websocketRequestsAtom = createModelAtom('websocket_request');
export const websocketEventsAtom = createOrderedModelAtom('websocket_event', 'createdAt', 'asc');
export const websocketConnectionsAtom = createOrderedModelAtom(
'websocket_connection',
'createdAt',
'desc',
); );
export const workspaceMetasAtom = createModelAtom('workspace_meta'); export const keyValuesAtom = createModelAtom("key_value");
export const workspacesAtom = createOrderedModelAtom('workspace', 'name', 'asc'); 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",
);
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( return selectAtom(
modelStoreDataAtom, modelStoreDataAtom,
(data) => Object.values(data[modelType] ?? {}), (data) => Object.values(data[modelType] ?? {}),
@@ -37,19 +41,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) => { return selectAtom(modelStoreDataAtom, (data) => {
const modelData = Object.values(data[modelType] ?? {}); const modelData = Object.values(data[modelType] ?? {});
const item = modelData[0]; 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; return item;
}); });
} }
export function createOrderedModelAtom<M extends AnyModel['model']>( export function createOrderedModelAtom<M extends AnyModel["model"]>(
modelType: M, modelType: M,
field: keyof ExtractModel<AnyModel, M>, field: keyof ExtractModel<AnyModel, M>,
order: 'asc' | 'desc', order: "asc" | "desc",
) { ) {
return selectAtom( return selectAtom(
modelStoreDataAtom, modelStoreDataAtom,
@@ -58,7 +62,7 @@ export function createOrderedModelAtom<M extends AnyModel['model']>(
return Object.values(modelData).sort( return Object.values(modelData).sort(
(a: ExtractModel<AnyModel, M>, b: ExtractModel<AnyModel, M>) => { (a: ExtractModel<AnyModel, M>, b: ExtractModel<AnyModel, M>) => {
const n = a[field] > b[field] ? 1 : -1; 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_models";
export * from '../bindings/gen_util'; export * from "../bindings/gen_util";
export * from './store'; export * from "./store";
export * from './atoms'; export * from "./atoms";
export function modelTypeLabel(m: AnyModel): string { export function modelTypeLabel(m: AnyModel): string {
const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1); 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 { invoke } from "@tauri-apps/api/core";
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'; import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
import { resolvedModelName } from '@yaakapp/app/lib/resolvedModelName'; import { resolvedModelName } from "@yaakapp/app/lib/resolvedModelName";
import { AnyModel, ModelPayload } from '../bindings/gen_models'; import { AnyModel, ModelPayload } from "../bindings/gen_models";
import { modelStoreDataAtom } from './atoms'; import { modelStoreDataAtom } from "./atoms";
import { ExtractModel, JotaiStore, ModelStoreData } from './types'; import { ExtractModel, JotaiStore, ModelStoreData } from "./types";
import { newStoreData } from './util'; import { newStoreData } from "./util";
let _store: JotaiStore | null = null; let _store: JotaiStore | null = null;
@@ -12,11 +12,11 @@ export function initModelStore(store: JotaiStore) {
_store = store; _store = store;
getCurrentWebviewWindow() getCurrentWebviewWindow()
.listen<ModelPayload>('model_write', ({ payload }) => { .listen<ModelPayload>("model_write", ({ payload }) => {
if (shouldIgnoreModel(payload)) return; if (shouldIgnoreModel(payload)) return;
mustStore().set(modelStoreDataAtom, (prev: ModelStoreData) => { mustStore().set(modelStoreDataAtom, (prev: ModelStoreData) => {
if (payload.change.type === 'upsert') { if (payload.change.type === "upsert") {
return { return {
...prev, ...prev,
[payload.model.model]: { [payload.model.model]: {
@@ -36,7 +36,7 @@ export function initModelStore(store: JotaiStore) {
function mustStore(): JotaiStore { function mustStore(): JotaiStore {
if (_store == null) { if (_store == null) {
throw new Error('Model store was not initialized'); throw new Error("Model store was not initialized");
} }
return _store; return _store;
@@ -45,8 +45,8 @@ function mustStore(): JotaiStore {
let _activeWorkspaceId: string | null = null; let _activeWorkspaceId: string | null = null;
export async function changeModelStoreWorkspace(workspaceId: string | null) { export async function changeModelStoreWorkspace(workspaceId: string | null) {
console.log('Syncing models with new workspace', workspaceId); console.log("Syncing models with new workspace", workspaceId);
const workspaceModelsStr = await invoke<string>('models_workspace_models', { const workspaceModelsStr = await invoke<string>("models_workspace_models", {
workspaceId, // NOTE: if no workspace id provided, it will just fetch global models workspaceId, // NOTE: if no workspace id provided, it will just fetch global models
}); });
const workspaceModels = JSON.parse(workspaceModelsStr) as AnyModel[]; const workspaceModels = JSON.parse(workspaceModelsStr) as AnyModel[];
@@ -57,12 +57,12 @@ export async function changeModelStoreWorkspace(workspaceId: string | null) {
mustStore().set(modelStoreDataAtom, data); mustStore().set(modelStoreDataAtom, data);
console.log('Synced model store with workspace', workspaceId, data); console.log("Synced model store with workspace", workspaceId, data);
_activeWorkspaceId = workspaceId; _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>, modelType: M | ReadonlyArray<M>,
): T[] { ): T[] {
let data = mustStore().get(modelStoreDataAtom); 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[]); 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>, modelType: M | ReadonlyArray<M>,
id: string, id: string,
): T | null { ): T | null {
@@ -83,18 +83,17 @@ export function getModel<M extends AnyModel['model'], T extends ExtractModel<Any
return null; return null;
} }
export function getAnyModel( export function getAnyModel(id: string): AnyModel | null {
id: string,
): AnyModel | null {
let data = mustStore().get(modelStoreDataAtom); let data = mustStore().get(modelStoreDataAtom);
for (const t of Object.keys(data)) { for (const t of Object.keys(data)) {
// oxlint-disable-next-line no-explicit-any
let v = (data as any)[t]?.[id]; let v = (data as any)[t]?.[id];
if (v?.model === t) return v; if (v?.model === t) return v;
} }
return null; 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, model: M,
id: string, id: string,
patch: Partial<T> | ((prev: T) => T), patch: Partial<T> | ((prev: T) => T),
@@ -104,54 +103,54 @@ export function patchModelById<M extends AnyModel['model'], T extends ExtractMod
throw new Error(`Failed to get model to patch id=${id} model=${model}`); 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); return updateModel(newModel);
} }
export async function patchModel<M extends AnyModel['model'], T extends ExtractModel<AnyModel, M>>( export async function patchModel<M extends AnyModel["model"], T extends ExtractModel<AnyModel, M>>(
base: Pick<T, 'id' | 'model'>, base: Pick<T, "id" | "model">,
patch: Partial<T>, patch: Partial<T>,
): Promise<string> { ): Promise<string> {
return patchModelById<M, T>(base.model, base.id, patch); 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, model: T,
): Promise<string> { ): Promise<string> {
return invoke<string>('models_upsert', { model }); return invoke<string>("models_upsert", { model });
} }
export async function deleteModelById< export async function deleteModelById<
M extends AnyModel['model'], M extends AnyModel["model"],
T extends ExtractModel<AnyModel, M>, T extends ExtractModel<AnyModel, M>,
>(modelType: M | M[], id: string) { >(modelType: M | M[], id: string) {
let model = getModel<M, T>(modelType, id); let model = getModel<M, T>(modelType, id);
await deleteModel(model); 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, model: T | null,
) { ) {
if (model == 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, model: T | null,
) { ) {
if (model == 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 // 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) { if (name != null) {
const existingModels = listModels(model.model); const existingModels = listModels(model.model);
for (let i = 0; i < 100; i++) { for (let i = 0; i < 100; i++) {
const hasConflict = existingModels.some((m) => { 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; return false;
} else if (resolvedModelName(m) !== name) { } else if (resolvedModelName(m) !== name) {
return false; return false;
@@ -165,7 +164,7 @@ export function duplicateModel<M extends AnyModel['model'], T extends ExtractMod
// Name conflict. Try another one // Name conflict. Try another one
const m: RegExpMatchArray | null = name.match(/ Copy( (?<n>\d+))?$/); const m: RegExpMatchArray | null = name.match(/ Copy( (?<n>\d+))?$/);
if (m != null && m.groups?.n == null) { 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) { } else if (m != null && m.groups?.n != null) {
name = name.substring(0, m.index) + ` Copy ${parseInt(m.groups.n) + 1}`; name = name.substring(0, m.index) + ` Copy ${parseInt(m.groups.n) + 1}`;
} else { } else {
@@ -174,23 +173,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 }>>( export async function createGlobalModel<T extends Exclude<AnyModel, { workspaceId: string }>>(
patch: Partial<T> & Pick<T, 'model'>, patch: Partial<T> & Pick<T, "model">,
): Promise<string> { ): 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 }>>( 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> { ): Promise<string> {
return invoke<string>('models_upsert', { model: patch }); return invoke<string>("models_upsert", { model: patch });
} }
export function replaceModelsInStore< export function replaceModelsInStore<
M extends AnyModel['model'], M extends AnyModel["model"],
T extends Extract<AnyModel, { model: M }>, T extends Extract<AnyModel, { model: M }>,
>(model: M, models: T[]) { >(model: M, models: T[]) {
const newModels: Record<string, T> = {}; const newModels: Record<string, T> = {};
@@ -207,7 +206,7 @@ export function replaceModelsInStore<
} }
export function mergeModelsInStore< export function mergeModelsInStore<
M extends AnyModel['model'], M extends AnyModel["model"],
T extends Extract<AnyModel, { model: M }>, T extends Extract<AnyModel, { model: M }>,
>(model: M, models: T[], filter?: (model: T) => boolean) { >(model: M, models: T[], filter?: (model: T) => boolean) {
mustStore().set(modelStoreDataAtom, (prev: ModelStoreData) => { mustStore().set(modelStoreDataAtom, (prev: ModelStoreData) => {
@@ -236,7 +235,7 @@ export function mergeModelsInStore<
function shouldIgnoreModel({ model, updateSource }: ModelPayload) { function shouldIgnoreModel({ model, updateSource }: ModelPayload) {
// Never ignore updates from non-user sources // Never ignore updates from non-user sources
if (updateSource.type !== 'window') { if (updateSource.type !== "window") {
return false; return false;
} }
@@ -246,11 +245,11 @@ function shouldIgnoreModel({ model, updateSource }: ModelPayload) {
} }
// Only sync models that belong to this workspace, if a workspace ID is present // 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; return true;
} }
if (model.model === 'key_value' && model.namespace === 'no_sync') { if (model.model === "key_value" && model.namespace === "no_sync") {
return true; return true;
} }

View File

@@ -1,8 +1,8 @@
import { createStore } from 'jotai'; import { createStore } from "jotai";
import { AnyModel } from '../bindings/gen_models'; import { AnyModel } from "../bindings/gen_models";
export type ExtractModel<T, M> = T extends { model: M } ? T : never; export type ExtractModel<T, M> = T extends { model: M } ? T : never;
export type ModelStoreData<T extends AnyModel = AnyModel> = { 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>; export type JotaiStore = ReturnType<typeof createStore>;

View File

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

View File

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

View File

@@ -1,30 +1,30 @@
import { invoke } from '@tauri-apps/api/core'; import { invoke } from "@tauri-apps/api/core";
import { PluginNameVersion, PluginSearchResponse, PluginUpdatesResponse } from './bindings/gen_api'; import { PluginNameVersion, PluginSearchResponse, PluginUpdatesResponse } from "./bindings/gen_api";
export * from './bindings/gen_models'; export * from "./bindings/gen_models";
export * from './bindings/gen_events'; export * from "./bindings/gen_events";
export * from './bindings/gen_search'; export * from "./bindings/gen_search";
export async function searchPlugins(query: string) { 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) { 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) { export async function uninstallPlugin(pluginId: string) {
return invoke<void>('cmd_plugins_uninstall', { pluginId }); return invoke<void>("cmd_plugins_uninstall", { pluginId });
} }
export async function checkPluginUpdates() { export async function checkPluginUpdates() {
return invoke<PluginUpdatesResponse>('cmd_plugins_updates', {}); return invoke<PluginUpdatesResponse>("cmd_plugins_updates", {});
} }
export async function updateAllPlugins() { export async function updateAllPlugins() {
return invoke<PluginNameVersion[]>('cmd_plugins_update_all', {}); return invoke<PluginNameVersion[]>("cmd_plugins_update_all", {});
} }
export async function installPluginFromDirectory(directory: string) { 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", "name": "@yaakapp-internal/plugins",
"private": true,
"version": "1.0.0", "version": "1.0.0",
"private": true,
"main": "index.ts" "main": "index.ts"
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,27 @@
const { execSync } = require('node:child_process'); const { execSync } = require("node:child_process");
const fs = require("node:fs");
const path = require("node:path");
if (process.env.SKIP_WASM_BUILD === '1') { if (process.env.SKIP_WASM_BUILD === "1") {
console.log('Skipping wasm-pack build (SKIP_WASM_BUILD=1)'); console.log("Skipping wasm-pack build (SKIP_WASM_BUILD=1)");
return; return;
} }
execSync('wasm-pack build --target bundler', { stdio: 'inherit' }); 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"),
);

View File

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

View File

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

View File

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

View File

@@ -14,7 +14,10 @@ url = "2"
serde_json = { workspace = true } serde_json = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
tokio = { workspace = true, features = ["macros", "time", "test-util", "rt"] } 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-http = { workspace = true }
yaak-tls = { workspace = true } yaak-tls = { workspace = true }
yaak-models = { workspace = true } yaak-models = { workspace = true }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,7 @@ const https = require("node:https");
const { const {
BINARY_DISTRIBUTION_VERSION, BINARY_DISTRIBUTION_VERSION,
BINARY_NAME, BINARY_NAME,
PLATFORM_SPECIFIC_PACKAGE_NAME PLATFORM_SPECIFIC_PACKAGE_NAME,
} = require("./common"); } = require("./common");
const fallbackBinaryPath = path.join(__dirname, BINARY_NAME); const fallbackBinaryPath = path.join(__dirname, BINARY_NAME);
@@ -27,8 +27,8 @@ function makeRequest(url) {
} else { } else {
reject( reject(
new Error( 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", "name": "@yaakapp/cli",
"version": "0.0.1", "version": "0.0.1",
"main": "./index.js",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/mountain-loop/yaak.git" "url": "git+https://github.com/mountain-loop/yaak.git"
}, },
"scripts": {
"postinstall": "node ./install.js",
"prepublishOnly": "node ./prepublish.js"
},
"bin": { "bin": {
"yaak": "bin/cli.js", "yaak": "bin/cli.js",
"yaakcli": "bin/cli.js" "yaakcli": "bin/cli.js"
}, },
"main": "./index.js",
"scripts": {
"postinstall": "node ./install.js",
"prepublishOnly": "node ./prepublish.js"
},
"optionalDependencies": { "optionalDependencies": {
"@yaakapp/cli-darwin-x64": "0.0.1",
"@yaakapp/cli-darwin-arm64": "0.0.1", "@yaakapp/cli-darwin-arm64": "0.0.1",
"@yaakapp/cli-darwin-x64": "0.0.1",
"@yaakapp/cli-linux-arm64": "0.0.1", "@yaakapp/cli-linux-arm64": "0.0.1",
"@yaakapp/cli-linux-x64": "0.0.1", "@yaakapp/cli-linux-x64": "0.0.1",
"@yaakapp/cli-win32-x64": "0.0.1", "@yaakapp/cli-win32-arm64": "0.0.1",
"@yaakapp/cli-win32-arm64": "0.0.1" "@yaakapp/cli-win32-x64": "0.0.1"
} }
} }

View File

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

2576
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{ {
"name": "yaak-app", "name": "yaak-app",
"private": true,
"version": "0.0.0", "version": "0.0.0",
"private": true,
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/mountain-loop/yaak.git" "url": "git+https://github.com/mountain-loop/yaak.git"
@@ -63,7 +63,7 @@
"src-web" "src-web"
], ],
"scripts": { "scripts": {
"prepare": "husky", "prepare": "vp config",
"init": "npm install && npm run bootstrap", "init": "npm install && npm run bootstrap",
"start": "npm run app-dev", "start": "npm run app-dev",
"app-build": "tauri build", "app-build": "tauri build",
@@ -82,34 +82,36 @@
"vendor:vendor-plugins": "node scripts/vendor-plugins.cjs", "vendor:vendor-plugins": "node scripts/vendor-plugins.cjs",
"vendor:vendor-protoc": "node scripts/vendor-protoc.cjs", "vendor:vendor-protoc": "node scripts/vendor-protoc.cjs",
"vendor:vendor-node": "node scripts/vendor-node.cjs", "vendor:vendor-node": "node scripts/vendor-node.cjs",
"format": "vp fmt --ignore-path .oxfmtignore",
"lint": "run-p lint:*", "lint": "run-p lint:*",
"lint:biome": "biome lint", "lint:vp": "vp lint",
"lint:extra": "npm run --workspaces --if-present lint", "lint:workspaces": "npm run --workspaces --if-present lint",
"format": "biome format --write .",
"replace-version": "node scripts/replace-version.cjs", "replace-version": "node scripts/replace-version.cjs",
"tauri": "tauri", "tauri": "tauri",
"tauri-before-build": "npm run bootstrap", "tauri-before-build": "npm run bootstrap",
"tauri-before-dev": "node scripts/run-workspaces-dev.mjs" "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": { "dependencies": {
"@codemirror/lang-go": "^6.0.1", "@codemirror/lang-go": "^6.0.1",
"@codemirror/lang-java": "^6.0.2", "@codemirror/lang-java": "^6.0.2",
"@codemirror/lang-php": "^6.0.2", "@codemirror/lang-php": "^6.0.2",
"@codemirror/lang-python": "^6.2.1", "@codemirror/lang-python": "^6.2.1",
"@codemirror/legacy-modes": "^6.5.2" "@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 @@
// biome-ignore lint/suspicious/noExplicitAny: none // oxlint-disable-next-line no-explicit-any
export function debounce(fn: (...args: any[]) => void, delay = 500) { export function debounce(fn: (...args: any[]) => void, delay = 500) {
let timer: ReturnType<typeof setTimeout>; let timer: ReturnType<typeof setTimeout>;
// biome-ignore lint/suspicious/noExplicitAny: none // oxlint-disable-next-line no-explicit-any
const result = (...args: any[]) => { const result = (...args: any[]) => {
clearTimeout(timer); clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay); timer = setTimeout(() => fn(...args), delay);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
export type * from './plugins'; export type * from "./plugins";
export type * from './themes'; export type * from "./themes";
export * from './bindings/gen_models'; export * from "./bindings/gen_models";
export * from './bindings/gen_events'; export * from "./bindings/gen_events";
// Some extras for utility // Some extras for utility
export type { PartialImportResources } from './plugins/ImporterPlugin'; export type { PartialImportResources } from "./plugins/ImporterPlugin";

View File

@@ -5,9 +5,9 @@ import type {
FormInput, FormInput,
GetHttpAuthenticationSummaryResponse, GetHttpAuthenticationSummaryResponse,
HttpAuthenticationAction, HttpAuthenticationAction,
} from '../bindings/gen_events'; } from "../bindings/gen_events";
import type { MaybePromise } from '../helpers'; import type { MaybePromise } from "../helpers";
import type { Context } from './Context'; import type { Context } from "./Context";
type AddDynamicMethod<T> = { type AddDynamicMethod<T> = {
dynamic?: ( dynamic?: (
@@ -16,16 +16,16 @@ type AddDynamicMethod<T> = {
) => MaybePromise<Partial<T> | null | undefined>; ) => MaybePromise<Partial<T> | null | undefined>;
}; };
// biome-ignore lint/suspicious/noExplicitAny: distributive conditional type pattern // oxlint-disable-next-line no-explicit-any -- distributive conditional type pattern
type AddDynamic<T> = T extends any type AddDynamic<T> = T extends any
? T extends { inputs?: FormInput[] } ? T extends { inputs?: FormInput[] }
? Omit<T, 'inputs'> & { ? Omit<T, "inputs"> & {
inputs: Array<AddDynamic<FormInput>>; inputs: Array<AddDynamic<FormInput>>;
dynamic?: ( dynamic?: (
ctx: Context, ctx: Context,
args: CallHttpAuthenticationActionArgs, args: CallHttpAuthenticationActionArgs,
) => MaybePromise< ) => MaybePromise<
Partial<Omit<T, 'inputs'> & { inputs: Array<AddDynamic<FormInput>> }> | null | undefined Partial<Omit<T, "inputs"> & { inputs: Array<AddDynamic<FormInput>> }> | null | undefined
>; >;
} }
: T & AddDynamicMethod<T> : T & AddDynamicMethod<T>

View File

@@ -26,10 +26,10 @@ import type {
ShowToastRequest, ShowToastRequest,
TemplateRenderRequest, TemplateRenderRequest,
WorkspaceInfo, WorkspaceInfo,
} from '../bindings/gen_events.ts'; } from "../bindings/gen_events.ts";
import type { Folder, HttpRequest } from '../bindings/gen_models.ts'; import type { Folder, HttpRequest } from "../bindings/gen_models.ts";
import type { JsonValue } from '../bindings/serde_json/JsonValue'; import type { JsonValue } from "../bindings/serde_json/JsonValue";
import type { MaybePromise } from '../helpers'; import type { MaybePromise } from "../helpers";
export type CallPromptFormDynamicArgs = { export type CallPromptFormDynamicArgs = {
values: { [key in string]?: JsonPrimitive }; values: { [key in string]?: JsonPrimitive };
@@ -42,16 +42,16 @@ type AddDynamicMethod<T> = {
) => MaybePromise<Partial<T> | null | undefined>; ) => MaybePromise<Partial<T> | null | undefined>;
}; };
// biome-ignore lint/suspicious/noExplicitAny: distributive conditional type pattern // oxlint-disable-next-line no-explicit-any -- distributive conditional type pattern
type AddDynamic<T> = T extends any type AddDynamic<T> = T extends any
? T extends { inputs?: FormInput[] } ? T extends { inputs?: FormInput[] }
? Omit<T, 'inputs'> & { ? Omit<T, "inputs"> & {
inputs: Array<AddDynamic<FormInput>>; inputs: Array<AddDynamic<FormInput>>;
dynamic?: ( dynamic?: (
ctx: Context, ctx: Context,
args: CallPromptFormDynamicArgs, args: CallPromptFormDynamicArgs,
) => MaybePromise< ) => MaybePromise<
Partial<Omit<T, 'inputs'> & { inputs: Array<AddDynamic<FormInput>> }> | null | undefined Partial<Omit<T, "inputs"> & { inputs: Array<AddDynamic<FormInput>> }> | null | undefined
>; >;
} }
: T & AddDynamicMethod<T> : T & AddDynamicMethod<T>
@@ -59,11 +59,11 @@ type AddDynamic<T> = T extends any
export type DynamicPromptFormArg = AddDynamic<FormInput>; export type DynamicPromptFormArg = AddDynamic<FormInput>;
type DynamicPromptFormRequest = Omit<PromptFormRequest, 'inputs'> & { type DynamicPromptFormRequest = Omit<PromptFormRequest, "inputs"> & {
inputs: DynamicPromptFormArg[]; inputs: DynamicPromptFormArg[];
}; };
export type WorkspaceHandle = Pick<WorkspaceInfo, 'id' | 'name'>; export type WorkspaceHandle = Pick<WorkspaceInfo, "id" | "name">;
export interface Context { export interface Context {
clipboard: { clipboard: {
@@ -73,8 +73,8 @@ export interface Context {
show(args: ShowToastRequest): Promise<void>; show(args: ShowToastRequest): Promise<void>;
}; };
prompt: { prompt: {
text(args: PromptTextRequest): Promise<PromptTextResponse['value']>; text(args: PromptTextRequest): Promise<PromptTextResponse["value"]>;
form(args: DynamicPromptFormRequest): Promise<PromptFormResponse['values']>; form(args: DynamicPromptFormRequest): Promise<PromptFormResponse["values"]>;
}; };
store: { store: {
set<T>(key: string, value: T): Promise<void>; set<T>(key: string, value: T): Promise<void>;
@@ -94,41 +94,41 @@ export interface Context {
openExternalUrl(url: string): Promise<void>; openExternalUrl(url: string): Promise<void>;
}; };
cookies: { cookies: {
listNames(): Promise<ListCookieNamesResponse['names']>; listNames(): Promise<ListCookieNamesResponse["names"]>;
getValue(args: GetCookieValueRequest): Promise<GetCookieValueResponse['value']>; getValue(args: GetCookieValueRequest): Promise<GetCookieValueResponse["value"]>;
}; };
grpcRequest: { grpcRequest: {
render(args: RenderGrpcRequestRequest): Promise<RenderGrpcRequestResponse['grpcRequest']>; render(args: RenderGrpcRequestRequest): Promise<RenderGrpcRequestResponse["grpcRequest"]>;
}; };
httpRequest: { httpRequest: {
send(args: SendHttpRequestRequest): Promise<SendHttpRequestResponse['httpResponse']>; send(args: SendHttpRequestRequest): Promise<SendHttpRequestResponse["httpResponse"]>;
getById(args: GetHttpRequestByIdRequest): Promise<GetHttpRequestByIdResponse['httpRequest']>; getById(args: GetHttpRequestByIdRequest): Promise<GetHttpRequestByIdResponse["httpRequest"]>;
render(args: RenderHttpRequestRequest): Promise<RenderHttpRequestResponse['httpRequest']>; render(args: RenderHttpRequestRequest): Promise<RenderHttpRequestResponse["httpRequest"]>;
list(args?: ListHttpRequestsRequest): Promise<ListHttpRequestsResponse['httpRequests']>; list(args?: ListHttpRequestsRequest): Promise<ListHttpRequestsResponse["httpRequests"]>;
create( create(
args: Omit<Partial<HttpRequest>, 'id' | 'model' | 'createdAt' | 'updatedAt'> & args: Omit<Partial<HttpRequest>, "id" | "model" | "createdAt" | "updatedAt"> &
Pick<HttpRequest, 'workspaceId' | 'url'>, Pick<HttpRequest, "workspaceId" | "url">,
): Promise<HttpRequest>; ): Promise<HttpRequest>;
update( update(
args: Omit<Partial<HttpRequest>, 'model' | 'createdAt' | 'updatedAt'> & args: Omit<Partial<HttpRequest>, "model" | "createdAt" | "updatedAt"> &
Pick<HttpRequest, 'id'>, Pick<HttpRequest, "id">,
): Promise<HttpRequest>; ): Promise<HttpRequest>;
delete(args: { id: string }): Promise<HttpRequest>; delete(args: { id: string }): Promise<HttpRequest>;
}; };
folder: { folder: {
list(args?: ListFoldersRequest): Promise<ListFoldersResponse['folders']>; list(args?: ListFoldersRequest): Promise<ListFoldersResponse["folders"]>;
getById(args: { id: string }): Promise<Folder | null>; getById(args: { id: string }): Promise<Folder | null>;
create( create(
args: Omit<Partial<Folder>, 'id' | 'model' | 'createdAt' | 'updatedAt'> & args: Omit<Partial<Folder>, "id" | "model" | "createdAt" | "updatedAt"> &
Pick<Folder, 'workspaceId' | 'name'>, Pick<Folder, "workspaceId" | "name">,
): Promise<Folder>; ): Promise<Folder>;
update( update(
args: Omit<Partial<Folder>, 'model' | 'createdAt' | 'updatedAt'> & Pick<Folder, 'id'>, args: Omit<Partial<Folder>, "model" | "createdAt" | "updatedAt"> & Pick<Folder, "id">,
): Promise<Folder>; ): Promise<Folder>;
delete(args: { id: string }): Promise<Folder>; delete(args: { id: string }): Promise<Folder>;
}; };
httpResponse: { httpResponse: {
find(args: FindHttpResponsesRequest): Promise<FindHttpResponsesResponse['httpResponses']>; find(args: FindHttpResponsesRequest): Promise<FindHttpResponsesResponse["httpResponses"]>;
}; };
templates: { templates: {
render<T extends JsonValue>(args: TemplateRenderRequest & { data: T }): Promise<T>; render<T extends JsonValue>(args: TemplateRenderRequest & { data: T }): Promise<T>;

View File

@@ -1,5 +1,5 @@
import type { FilterResponse } from '../bindings/gen_events'; import type { FilterResponse } from "../bindings/gen_events";
import type { Context } from './Context'; import type { Context } from "./Context";
export type FilterPlugin = { export type FilterPlugin = {
name: string; name: string;

View File

@@ -1,5 +1,5 @@
import type { CallFolderActionArgs, FolderAction } from '../bindings/gen_events'; import type { CallFolderActionArgs, FolderAction } from "../bindings/gen_events";
import type { Context } from './Context'; import type { Context } from "./Context";
export type FolderActionPlugin = FolderAction & { export type FolderActionPlugin = FolderAction & {
onSelect(ctx: Context, args: CallFolderActionArgs): Promise<void> | void; onSelect(ctx: Context, args: CallFolderActionArgs): Promise<void> | void;

View File

@@ -1,5 +1,5 @@
import type { CallGrpcRequestActionArgs, GrpcRequestAction } from '../bindings/gen_events'; import type { CallGrpcRequestActionArgs, GrpcRequestAction } from "../bindings/gen_events";
import type { Context } from './Context'; import type { Context } from "./Context";
export type GrpcRequestActionPlugin = GrpcRequestAction & { export type GrpcRequestActionPlugin = GrpcRequestAction & {
onSelect(ctx: Context, args: CallGrpcRequestActionArgs): Promise<void> | void; onSelect(ctx: Context, args: CallGrpcRequestActionArgs): Promise<void> | void;

View File

@@ -1,5 +1,5 @@
import type { CallHttpRequestActionArgs, HttpRequestAction } from '../bindings/gen_events'; import type { CallHttpRequestActionArgs, HttpRequestAction } from "../bindings/gen_events";
import type { Context } from './Context'; import type { Context } from "./Context";
export type HttpRequestActionPlugin = HttpRequestAction & { export type HttpRequestActionPlugin = HttpRequestAction & {
onSelect(ctx: Context, args: CallHttpRequestActionArgs): Promise<void> | void; onSelect(ctx: Context, args: CallHttpRequestActionArgs): Promise<void> | void;

View File

@@ -1,17 +1,17 @@
import type { ImportResources } from '../bindings/gen_events'; import type { ImportResources } from "../bindings/gen_events";
import type { AtLeast, MaybePromise } from '../helpers'; import type { AtLeast, MaybePromise } from "../helpers";
import type { Context } from './Context'; import type { Context } from "./Context";
type RootFields = 'name' | 'id' | 'model'; type RootFields = "name" | "id" | "model";
type CommonFields = RootFields | 'workspaceId'; type CommonFields = RootFields | "workspaceId";
export type PartialImportResources = { export type PartialImportResources = {
workspaces: Array<AtLeast<ImportResources['workspaces'][0], RootFields>>; workspaces: Array<AtLeast<ImportResources["workspaces"][0], RootFields>>;
environments: Array<AtLeast<ImportResources['environments'][0], CommonFields>>; environments: Array<AtLeast<ImportResources["environments"][0], CommonFields>>;
folders: Array<AtLeast<ImportResources['folders'][0], CommonFields>>; folders: Array<AtLeast<ImportResources["folders"][0], CommonFields>>;
httpRequests: Array<AtLeast<ImportResources['httpRequests'][0], CommonFields>>; httpRequests: Array<AtLeast<ImportResources["httpRequests"][0], CommonFields>>;
grpcRequests: Array<AtLeast<ImportResources['grpcRequests'][0], CommonFields>>; grpcRequests: Array<AtLeast<ImportResources["grpcRequests"][0], CommonFields>>;
websocketRequests: Array<AtLeast<ImportResources['websocketRequests'][0], CommonFields>>; websocketRequests: Array<AtLeast<ImportResources["websocketRequests"][0], CommonFields>>;
}; };
export type ImportPluginResponse = null | { export type ImportPluginResponse = null | {

View File

@@ -1,6 +1,6 @@
import type { CallTemplateFunctionArgs, FormInput, TemplateFunction } from '../bindings/gen_events'; import type { CallTemplateFunctionArgs, FormInput, TemplateFunction } from "../bindings/gen_events";
import type { MaybePromise } from '../helpers'; import type { MaybePromise } from "../helpers";
import type { Context } from './Context'; import type { Context } from "./Context";
type AddDynamicMethod<T> = { type AddDynamicMethod<T> = {
dynamic?: ( dynamic?: (
@@ -9,16 +9,16 @@ type AddDynamicMethod<T> = {
) => MaybePromise<Partial<T> | null | undefined>; ) => MaybePromise<Partial<T> | null | undefined>;
}; };
// biome-ignore lint/suspicious/noExplicitAny: distributive conditional type pattern // oxlint-disable-next-line no-explicit-any -- distributive conditional type pattern
type AddDynamic<T> = T extends any type AddDynamic<T> = T extends any
? T extends { inputs?: FormInput[] } ? T extends { inputs?: FormInput[] }
? Omit<T, 'inputs'> & { ? Omit<T, "inputs"> & {
inputs: Array<AddDynamic<FormInput>>; inputs: Array<AddDynamic<FormInput>>;
dynamic?: ( dynamic?: (
ctx: Context, ctx: Context,
args: CallTemplateFunctionArgs, args: CallTemplateFunctionArgs,
) => MaybePromise< ) => MaybePromise<
Partial<Omit<T, 'inputs'> & { inputs: Array<AddDynamic<FormInput>> }> | null | undefined Partial<Omit<T, "inputs"> & { inputs: Array<AddDynamic<FormInput>> }> | null | undefined
>; >;
} }
: T & AddDynamicMethod<T> : T & AddDynamicMethod<T>
@@ -26,7 +26,7 @@ type AddDynamic<T> = T extends any
export type DynamicTemplateFunctionArg = AddDynamic<FormInput>; export type DynamicTemplateFunctionArg = AddDynamic<FormInput>;
export type TemplateFunctionPlugin = Omit<TemplateFunction, 'args'> & { export type TemplateFunctionPlugin = Omit<TemplateFunction, "args"> & {
args: DynamicTemplateFunctionArg[]; args: DynamicTemplateFunctionArg[];
onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null>; onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null>;
}; };

View File

@@ -1,3 +1,3 @@
import type { Theme } from '../bindings/gen_events'; import type { Theme } from "../bindings/gen_events";
export type ThemePlugin = Theme; export type ThemePlugin = Theme;

View File

@@ -1,8 +1,8 @@
import type { import type {
CallWebsocketRequestActionArgs, CallWebsocketRequestActionArgs,
WebsocketRequestAction, WebsocketRequestAction,
} from '../bindings/gen_events'; } from "../bindings/gen_events";
import type { Context } from './Context'; import type { Context } from "./Context";
export type WebsocketRequestActionPlugin = WebsocketRequestAction & { export type WebsocketRequestActionPlugin = WebsocketRequestAction & {
onSelect(ctx: Context, args: CallWebsocketRequestActionArgs): Promise<void> | void; onSelect(ctx: Context, args: CallWebsocketRequestActionArgs): Promise<void> | void;

View File

@@ -1,5 +1,5 @@
import type { CallWorkspaceActionArgs, WorkspaceAction } from '../bindings/gen_events'; import type { CallWorkspaceActionArgs, WorkspaceAction } from "../bindings/gen_events";
import type { Context } from './Context'; import type { Context } from "./Context";
export type WorkspaceActionPlugin = WorkspaceAction & { export type WorkspaceActionPlugin = WorkspaceAction & {
onSelect(ctx: Context, args: CallWorkspaceActionArgs): Promise<void> | void; onSelect(ctx: Context, args: CallWorkspaceActionArgs): Promise<void> | void;

View File

@@ -1,23 +1,23 @@
import type { AuthenticationPlugin } from './AuthenticationPlugin'; import type { AuthenticationPlugin } from "./AuthenticationPlugin";
import type { Context } from './Context'; import type { Context } from "./Context";
import type { FilterPlugin } from './FilterPlugin'; import type { FilterPlugin } from "./FilterPlugin";
import type { FolderActionPlugin } from './FolderActionPlugin'; import type { FolderActionPlugin } from "./FolderActionPlugin";
import type { GrpcRequestActionPlugin } from './GrpcRequestActionPlugin'; import type { GrpcRequestActionPlugin } from "./GrpcRequestActionPlugin";
import type { HttpRequestActionPlugin } from './HttpRequestActionPlugin'; import type { HttpRequestActionPlugin } from "./HttpRequestActionPlugin";
import type { ImporterPlugin } from './ImporterPlugin'; import type { ImporterPlugin } from "./ImporterPlugin";
import type { TemplateFunctionPlugin } from './TemplateFunctionPlugin'; import type { TemplateFunctionPlugin } from "./TemplateFunctionPlugin";
import type { ThemePlugin } from './ThemePlugin'; import type { ThemePlugin } from "./ThemePlugin";
import type { WebsocketRequestActionPlugin } from './WebsocketRequestActionPlugin'; import type { WebsocketRequestActionPlugin } from "./WebsocketRequestActionPlugin";
import type { WorkspaceActionPlugin } from './WorkspaceActionPlugin'; import type { WorkspaceActionPlugin } from "./WorkspaceActionPlugin";
export type { Context }; export type { Context };
export type { DynamicAuthenticationArg } from './AuthenticationPlugin'; export type { DynamicAuthenticationArg } from "./AuthenticationPlugin";
export type { CallPromptFormDynamicArgs, DynamicPromptFormArg } from './Context'; export type { CallPromptFormDynamicArgs, DynamicPromptFormArg } from "./Context";
export type { DynamicTemplateFunctionArg } from './TemplateFunctionPlugin'; export type { DynamicTemplateFunctionArg } from "./TemplateFunctionPlugin";
export type { TemplateFunctionPlugin }; export type { TemplateFunctionPlugin };
export type { FolderActionPlugin } from './FolderActionPlugin'; export type { FolderActionPlugin } from "./FolderActionPlugin";
export type { WorkspaceActionPlugin } from './WorkspaceActionPlugin'; export type { WorkspaceActionPlugin } from "./WorkspaceActionPlugin";
/** /**
* The global structure of a Yaak plugin * The global structure of a Yaak plugin

View File

@@ -1,4 +1,4 @@
import type { InternalEvent } from '@yaakapp/api'; import type { InternalEvent } from "@yaakapp/api";
export class EventChannel { export class EventChannel {
#listeners = new Set<(event: InternalEvent) => void>(); #listeners = new Set<(event: InternalEvent) => void>();

View File

@@ -1,7 +1,7 @@
import type { BootRequest, InternalEvent } from '@yaakapp/api'; import type { BootRequest, InternalEvent } from "@yaakapp/api";
import type { PluginContext } from '@yaakapp-internal/plugins'; import type { PluginContext } from "@yaakapp-internal/plugins";
import type { EventChannel } from './EventChannel'; import type { EventChannel } from "./EventChannel";
import { PluginInstance, type PluginWorkerData } from './PluginInstance'; import { PluginInstance, type PluginWorkerData } from "./PluginInstance";
export class PluginHandle { export class PluginHandle {
#instance: PluginInstance; #instance: PluginInstance;

View File

@@ -1,16 +1,16 @@
import console from 'node:console'; import console from "node:console";
import { type Stats, statSync, watch } from 'node:fs'; import { type Stats, statSync, watch } from "node:fs";
import path from 'node:path'; import path from "node:path";
import type { import type {
CallPromptFormDynamicArgs, CallPromptFormDynamicArgs,
Context, Context,
DynamicPromptFormArg, DynamicPromptFormArg,
PluginDefinition, PluginDefinition,
} from '@yaakapp/api'; } from "@yaakapp/api";
import { import {
applyFormInputDefaults, applyFormInputDefaults,
validateTemplateFunctionArgs, validateTemplateFunctionArgs,
} from '@yaakapp-internal/lib/templateFunction'; } from "@yaakapp-internal/lib/templateFunction";
import type { import type {
BootRequest, BootRequest,
DeleteKeyValueResponse, DeleteKeyValueResponse,
@@ -45,10 +45,10 @@ import type {
TemplateRenderResponse, TemplateRenderResponse,
UpsertModelResponse, UpsertModelResponse,
WindowInfoResponse, WindowInfoResponse,
} from '@yaakapp-internal/plugins'; } from "@yaakapp-internal/plugins";
import { applyDynamicFormInput } from './common'; import { applyDynamicFormInput } from "./common";
import { EventChannel } from './EventChannel'; import { EventChannel } from "./EventChannel";
import { migrateTemplateFunctionSelectOptions } from './migrations'; import { migrateTemplateFunctionSelectOptions } from "./migrations";
export interface PluginWorkerData { export interface PluginWorkerData {
bootRequest: BootRequest; bootRequest: BootRequest;
@@ -84,16 +84,16 @@ export class PluginInstance {
this.#sendPayload( this.#sendPayload(
workerData.context, workerData.context,
{ {
type: 'reload_response', type: "reload_response",
silent: false, silent: false,
}, },
null, null,
); );
} catch (err: unknown) { } catch (err: unknown) {
await ctx.toast.show({ await ctx.toast.show({
message: `Failed to initialize plugin ${this.#workerData.bootRequest.dir.split('/').pop()}: ${err}`, message: `Failed to initialize plugin ${this.#workerData.bootRequest.dir.split("/").pop()}: ${err instanceof Error ? err.message : String(err)}`,
color: 'notice', color: "notice",
icon: 'alert_triangle', icon: "alert_triangle",
timeout: 30000, timeout: 30000,
}); });
} }
@@ -123,15 +123,15 @@ export class PluginInstance {
const { context, payload, id: replyId } = event; const { context, payload, id: replyId } = event;
try { try {
if (payload.type === 'boot_request') { if (payload.type === "boot_request") {
await this.#mod?.init?.(ctx); await this.#mod?.init?.(ctx);
this.#sendPayload(context, { type: 'boot_response' }, replyId); this.#sendPayload(context, { type: "boot_response" }, replyId);
return; return;
} }
if (payload.type === 'terminate_request') { if (payload.type === "terminate_request") {
const payload: InternalEventPayload = { const payload: InternalEventPayload = {
type: 'terminate_response', type: "terminate_response",
}; };
await this.terminate(); await this.terminate();
this.#sendPayload(context, payload, replyId); this.#sendPayload(context, payload, replyId);
@@ -139,15 +139,15 @@ export class PluginInstance {
} }
if ( if (
payload.type === 'import_request' && payload.type === "import_request" &&
typeof this.#mod?.importer?.onImport === 'function' typeof this.#mod?.importer?.onImport === "function"
) { ) {
const reply = await this.#mod.importer.onImport(ctx, { const reply = await this.#mod.importer.onImport(ctx, {
text: payload.content, text: payload.content,
}); });
if (reply != null) { if (reply != null) {
const replyPayload: InternalEventPayload = { const replyPayload: InternalEventPayload = {
type: 'import_response', type: "import_response",
resources: reply.resources as ImportResources, resources: reply.resources as ImportResources,
}; };
this.#sendPayload(context, replyPayload, replyId); this.#sendPayload(context, replyPayload, replyId);
@@ -157,18 +157,18 @@ export class PluginInstance {
} }
} }
if (payload.type === 'filter_request' && typeof this.#mod?.filter?.onFilter === 'function') { if (payload.type === "filter_request" && typeof this.#mod?.filter?.onFilter === "function") {
const reply = await this.#mod.filter.onFilter(ctx, { const reply = await this.#mod.filter.onFilter(ctx, {
filter: payload.filter, filter: payload.filter,
payload: payload.content, payload: payload.content,
mimeType: payload.type, mimeType: payload.type,
}); });
this.#sendPayload(context, { type: 'filter_response', ...reply }, replyId); this.#sendPayload(context, { type: "filter_response", ...reply }, replyId);
return; return;
} }
if ( if (
payload.type === 'get_grpc_request_actions_request' && payload.type === "get_grpc_request_actions_request" &&
Array.isArray(this.#mod?.grpcRequestActions) Array.isArray(this.#mod?.grpcRequestActions)
) { ) {
const reply: GrpcRequestAction[] = this.#mod.grpcRequestActions.map((a) => ({ const reply: GrpcRequestAction[] = this.#mod.grpcRequestActions.map((a) => ({
@@ -177,7 +177,7 @@ export class PluginInstance {
onSelect: undefined, onSelect: undefined,
})); }));
const replyPayload: InternalEventPayload = { const replyPayload: InternalEventPayload = {
type: 'get_grpc_request_actions_response', type: "get_grpc_request_actions_response",
pluginRefId: this.#workerData.pluginRefId, pluginRefId: this.#workerData.pluginRefId,
actions: reply, actions: reply,
}; };
@@ -186,7 +186,7 @@ export class PluginInstance {
} }
if ( if (
payload.type === 'get_http_request_actions_request' && payload.type === "get_http_request_actions_request" &&
Array.isArray(this.#mod?.httpRequestActions) Array.isArray(this.#mod?.httpRequestActions)
) { ) {
const reply: HttpRequestAction[] = this.#mod.httpRequestActions.map((a) => ({ const reply: HttpRequestAction[] = this.#mod.httpRequestActions.map((a) => ({
@@ -195,7 +195,7 @@ export class PluginInstance {
onSelect: undefined, onSelect: undefined,
})); }));
const replyPayload: InternalEventPayload = { const replyPayload: InternalEventPayload = {
type: 'get_http_request_actions_response', type: "get_http_request_actions_response",
pluginRefId: this.#workerData.pluginRefId, pluginRefId: this.#workerData.pluginRefId,
actions: reply, actions: reply,
}; };
@@ -204,7 +204,7 @@ export class PluginInstance {
} }
if ( if (
payload.type === 'get_websocket_request_actions_request' && payload.type === "get_websocket_request_actions_request" &&
Array.isArray(this.#mod?.websocketRequestActions) Array.isArray(this.#mod?.websocketRequestActions)
) { ) {
const reply = this.#mod.websocketRequestActions.map((a) => ({ const reply = this.#mod.websocketRequestActions.map((a) => ({
@@ -212,7 +212,7 @@ export class PluginInstance {
onSelect: undefined, onSelect: undefined,
})); }));
const replyPayload: InternalEventPayload = { const replyPayload: InternalEventPayload = {
type: 'get_websocket_request_actions_response', type: "get_websocket_request_actions_response",
pluginRefId: this.#workerData.pluginRefId, pluginRefId: this.#workerData.pluginRefId,
actions: reply, actions: reply,
}; };
@@ -221,7 +221,7 @@ export class PluginInstance {
} }
if ( if (
payload.type === 'get_workspace_actions_request' && payload.type === "get_workspace_actions_request" &&
Array.isArray(this.#mod?.workspaceActions) Array.isArray(this.#mod?.workspaceActions)
) { ) {
const reply = this.#mod.workspaceActions.map((a) => ({ const reply = this.#mod.workspaceActions.map((a) => ({
@@ -229,7 +229,7 @@ export class PluginInstance {
onSelect: undefined, onSelect: undefined,
})); }));
const replyPayload: InternalEventPayload = { const replyPayload: InternalEventPayload = {
type: 'get_workspace_actions_response', type: "get_workspace_actions_response",
pluginRefId: this.#workerData.pluginRefId, pluginRefId: this.#workerData.pluginRefId,
actions: reply, actions: reply,
}; };
@@ -238,7 +238,7 @@ export class PluginInstance {
} }
if ( if (
payload.type === 'get_folder_actions_request' && payload.type === "get_folder_actions_request" &&
Array.isArray(this.#mod?.folderActions) Array.isArray(this.#mod?.folderActions)
) { ) {
const reply = this.#mod.folderActions.map((a) => ({ const reply = this.#mod.folderActions.map((a) => ({
@@ -246,7 +246,7 @@ export class PluginInstance {
onSelect: undefined, onSelect: undefined,
})); }));
const replyPayload: InternalEventPayload = { const replyPayload: InternalEventPayload = {
type: 'get_folder_actions_response', type: "get_folder_actions_response",
pluginRefId: this.#workerData.pluginRefId, pluginRefId: this.#workerData.pluginRefId,
actions: reply, actions: reply,
}; };
@@ -254,9 +254,9 @@ export class PluginInstance {
return; return;
} }
if (payload.type === 'get_themes_request' && Array.isArray(this.#mod?.themes)) { if (payload.type === "get_themes_request" && Array.isArray(this.#mod?.themes)) {
const replyPayload: InternalEventPayload = { const replyPayload: InternalEventPayload = {
type: 'get_themes_response', type: "get_themes_response",
themes: this.#mod.themes, themes: this.#mod.themes,
}; };
this.#sendPayload(context, replyPayload, replyId); this.#sendPayload(context, replyPayload, replyId);
@@ -264,7 +264,7 @@ export class PluginInstance {
} }
if ( if (
payload.type === 'get_template_function_summary_request' && payload.type === "get_template_function_summary_request" &&
Array.isArray(this.#mod?.templateFunctions) Array.isArray(this.#mod?.templateFunctions)
) { ) {
const functions: TemplateFunction[] = this.#mod.templateFunctions.map( const functions: TemplateFunction[] = this.#mod.templateFunctions.map(
@@ -277,7 +277,7 @@ export class PluginInstance {
}, },
); );
const replyPayload: InternalEventPayload = { const replyPayload: InternalEventPayload = {
type: 'get_template_function_summary_response', type: "get_template_function_summary_response",
pluginRefId: this.#workerData.pluginRefId, pluginRefId: this.#workerData.pluginRefId,
functions, functions,
}; };
@@ -286,7 +286,7 @@ export class PluginInstance {
} }
if ( if (
payload.type === 'get_template_function_config_request' && payload.type === "get_template_function_config_request" &&
Array.isArray(this.#mod?.templateFunctions) Array.isArray(this.#mod?.templateFunctions)
) { ) {
const templateFunction = this.#mod.templateFunctions.find((f) => f.name === payload.name); const templateFunction = this.#mod.templateFunctions.find((f) => f.name === payload.name);
@@ -301,11 +301,11 @@ export class PluginInstance {
}; };
payload.values = applyFormInputDefaults(fn.args, payload.values); payload.values = applyFormInputDefaults(fn.args, payload.values);
const p = { ...payload, purpose: 'preview' } as const; const p = { ...payload, purpose: "preview" } as const;
const resolvedArgs = await applyDynamicFormInput(ctx, fn.args, p); const resolvedArgs = await applyDynamicFormInput(ctx, fn.args, p);
const replyPayload: InternalEventPayload = { const replyPayload: InternalEventPayload = {
type: 'get_template_function_config_response', type: "get_template_function_config_response",
pluginRefId: this.#workerData.pluginRefId, pluginRefId: this.#workerData.pluginRefId,
function: { ...fn, args: stripDynamicCallbacks(resolvedArgs) }, function: { ...fn, args: stripDynamicCallbacks(resolvedArgs) },
}; };
@@ -313,9 +313,9 @@ export class PluginInstance {
return; return;
} }
if (payload.type === 'get_http_authentication_summary_request' && this.#mod?.authentication) { if (payload.type === "get_http_authentication_summary_request" && this.#mod?.authentication) {
const replyPayload: InternalEventPayload = { const replyPayload: InternalEventPayload = {
type: 'get_http_authentication_summary_response', type: "get_http_authentication_summary_response",
...this.#mod.authentication, ...this.#mod.authentication,
}; };
@@ -323,17 +323,18 @@ export class PluginInstance {
return; return;
} }
if (payload.type === 'get_http_authentication_config_request' && this.#mod?.authentication) { if (payload.type === "get_http_authentication_config_request" && this.#mod?.authentication) {
const { args, actions } = this.#mod.authentication; const { args, actions } = this.#mod.authentication;
payload.values = applyFormInputDefaults(args, payload.values); payload.values = applyFormInputDefaults(args, payload.values);
const resolvedArgs = await applyDynamicFormInput(ctx, args, payload); const resolvedArgs = await applyDynamicFormInput(ctx, args, payload);
const resolvedActions: HttpAuthenticationAction[] = []; const resolvedActions: HttpAuthenticationAction[] = [];
for (const { onSelect, ...action } of actions ?? []) { // oxlint-disable-next-line unbound-method
for (const { onSelect: _onSelect, ...action } of actions ?? []) {
resolvedActions.push(action); resolvedActions.push(action);
} }
const replyPayload: InternalEventPayload = { const replyPayload: InternalEventPayload = {
type: 'get_http_authentication_config_response', type: "get_http_authentication_config_response",
args: stripDynamicCallbacks(resolvedArgs), args: stripDynamicCallbacks(resolvedArgs),
actions: resolvedActions, actions: resolvedActions,
pluginRefId: this.#workerData.pluginRefId, pluginRefId: this.#workerData.pluginRefId,
@@ -343,15 +344,15 @@ export class PluginInstance {
return; return;
} }
if (payload.type === 'call_http_authentication_request' && this.#mod?.authentication) { if (payload.type === "call_http_authentication_request" && this.#mod?.authentication) {
const auth = this.#mod.authentication; const auth = this.#mod.authentication;
if (typeof auth?.onApply === 'function') { if (typeof auth?.onApply === "function") {
const resolvedArgs = await applyDynamicFormInput(ctx, auth.args, payload); const resolvedArgs = await applyDynamicFormInput(ctx, auth.args, payload);
payload.values = applyFormInputDefaults(resolvedArgs, payload.values); payload.values = applyFormInputDefaults(resolvedArgs, payload.values);
this.#sendPayload( this.#sendPayload(
context, context,
{ {
type: 'call_http_authentication_response', type: "call_http_authentication_response",
...(await auth.onApply(ctx, payload)), ...(await auth.onApply(ctx, payload)),
}, },
replyId, replyId,
@@ -361,11 +362,11 @@ export class PluginInstance {
} }
if ( if (
payload.type === 'call_http_authentication_action_request' && payload.type === "call_http_authentication_action_request" &&
this.#mod.authentication != null this.#mod.authentication != null
) { ) {
const action = this.#mod.authentication.actions?.[payload.index]; const action = this.#mod.authentication.actions?.[payload.index];
if (typeof action?.onSelect === 'function') { if (typeof action?.onSelect === "function") {
await action.onSelect(ctx, payload.args); await action.onSelect(ctx, payload.args);
this.#sendEmpty(context, replyId); this.#sendEmpty(context, replyId);
return; return;
@@ -373,11 +374,11 @@ export class PluginInstance {
} }
if ( if (
payload.type === 'call_http_request_action_request' && payload.type === "call_http_request_action_request" &&
Array.isArray(this.#mod.httpRequestActions) Array.isArray(this.#mod.httpRequestActions)
) { ) {
const action = this.#mod.httpRequestActions[payload.index]; const action = this.#mod.httpRequestActions[payload.index];
if (typeof action?.onSelect === 'function') { if (typeof action?.onSelect === "function") {
await action.onSelect(ctx, payload.args); await action.onSelect(ctx, payload.args);
this.#sendEmpty(context, replyId); this.#sendEmpty(context, replyId);
return; return;
@@ -385,11 +386,11 @@ export class PluginInstance {
} }
if ( if (
payload.type === 'call_websocket_request_action_request' && payload.type === "call_websocket_request_action_request" &&
Array.isArray(this.#mod.websocketRequestActions) Array.isArray(this.#mod.websocketRequestActions)
) { ) {
const action = this.#mod.websocketRequestActions[payload.index]; const action = this.#mod.websocketRequestActions[payload.index];
if (typeof action?.onSelect === 'function') { if (typeof action?.onSelect === "function") {
await action.onSelect(ctx, payload.args); await action.onSelect(ctx, payload.args);
this.#sendEmpty(context, replyId); this.#sendEmpty(context, replyId);
return; return;
@@ -397,20 +398,20 @@ export class PluginInstance {
} }
if ( if (
payload.type === 'call_workspace_action_request' && payload.type === "call_workspace_action_request" &&
Array.isArray(this.#mod.workspaceActions) Array.isArray(this.#mod.workspaceActions)
) { ) {
const action = this.#mod.workspaceActions[payload.index]; const action = this.#mod.workspaceActions[payload.index];
if (typeof action?.onSelect === 'function') { if (typeof action?.onSelect === "function") {
await action.onSelect(ctx, payload.args); await action.onSelect(ctx, payload.args);
this.#sendEmpty(context, replyId); this.#sendEmpty(context, replyId);
return; return;
} }
} }
if (payload.type === 'call_folder_action_request' && Array.isArray(this.#mod.folderActions)) { if (payload.type === "call_folder_action_request" && Array.isArray(this.#mod.folderActions)) {
const action = this.#mod.folderActions[payload.index]; const action = this.#mod.folderActions[payload.index];
if (typeof action?.onSelect === 'function') { if (typeof action?.onSelect === "function") {
await action.onSelect(ctx, payload.args); await action.onSelect(ctx, payload.args);
this.#sendEmpty(context, replyId); this.#sendEmpty(context, replyId);
return; return;
@@ -418,11 +419,11 @@ export class PluginInstance {
} }
if ( if (
payload.type === 'call_grpc_request_action_request' && payload.type === "call_grpc_request_action_request" &&
Array.isArray(this.#mod.grpcRequestActions) Array.isArray(this.#mod.grpcRequestActions)
) { ) {
const action = this.#mod.grpcRequestActions[payload.index]; const action = this.#mod.grpcRequestActions[payload.index];
if (typeof action?.onSelect === 'function') { if (typeof action?.onSelect === "function") {
await action.onSelect(ctx, payload.args); await action.onSelect(ctx, payload.args);
this.#sendEmpty(context, replyId); this.#sendEmpty(context, replyId);
return; return;
@@ -430,32 +431,32 @@ export class PluginInstance {
} }
if ( if (
payload.type === 'call_template_function_request' && payload.type === "call_template_function_request" &&
Array.isArray(this.#mod?.templateFunctions) Array.isArray(this.#mod?.templateFunctions)
) { ) {
const fn = this.#mod.templateFunctions.find((a) => a.name === payload.name); const fn = this.#mod.templateFunctions.find((a) => a.name === payload.name);
if ( if (
payload.args.purpose === 'preview' && payload.args.purpose === "preview" &&
(fn?.previewType === 'click' || fn?.previewType === 'none') (fn?.previewType === "click" || fn?.previewType === "none")
) { ) {
// Send empty render response // Send empty render response
this.#sendPayload( this.#sendPayload(
context, context,
{ {
type: 'call_template_function_response', type: "call_template_function_response",
value: null, value: null,
error: 'Live preview disabled for this function', error: "Live preview disabled for this function",
}, },
replyId, replyId,
); );
} else if (typeof fn?.onRender === 'function') { } else if (typeof fn?.onRender === "function") {
const resolvedArgs = await applyDynamicFormInput(ctx, fn.args, payload.args); const resolvedArgs = await applyDynamicFormInput(ctx, fn.args, payload.args);
const values = applyFormInputDefaults(resolvedArgs, payload.args.values); const values = applyFormInputDefaults(resolvedArgs, payload.args.values);
const error = validateTemplateFunctionArgs(fn.name, resolvedArgs, values); const error = validateTemplateFunctionArgs(fn.name, resolvedArgs, values);
if (error && payload.args.purpose !== 'preview') { if (error && payload.args.purpose !== "preview") {
this.#sendPayload( this.#sendPayload(
context, context,
{ type: 'call_template_function_response', value: null, error }, { type: "call_template_function_response", value: null, error },
replyId, replyId,
); );
return; return;
@@ -465,16 +466,19 @@ export class PluginInstance {
const result = await fn.onRender(ctx, { ...payload.args, values }); const result = await fn.onRender(ctx, { ...payload.args, values });
this.#sendPayload( this.#sendPayload(
context, context,
{ type: 'call_template_function_response', value: result ?? null }, { type: "call_template_function_response", value: result ?? null },
replyId, replyId,
); );
} catch (err) { } catch (err) {
this.#sendPayload( this.#sendPayload(
context, context,
{ {
type: 'call_template_function_response', type: "call_template_function_response",
value: null, value: null,
error: `${err}`.replace(/^Error:\s*/g, ''), error: (err instanceof Error ? err.message : String(err)).replace(
/^Error:\s*/g,
"",
),
}, },
replyId, replyId,
); );
@@ -483,9 +487,9 @@ export class PluginInstance {
} }
} }
} catch (err) { } catch (err) {
const error = `${err}`.replace(/^Error:\s*/g, ''); const error = (err instanceof Error ? err.message : String(err)).replace(/^Error:\s*/g, "");
console.log('Plugin call threw exception', payload.type, '→', error); console.log("Plugin call threw exception", payload.type, "→", error);
this.#sendPayload(context, { type: 'error_response', error }, replyId); this.#sendPayload(context, { type: "error_response", error }, replyId);
return; return;
} }
@@ -494,11 +498,11 @@ export class PluginInstance {
} }
#pathMod() { #pathMod() {
return path.posix.join(this.#workerData.bootRequest.dir, 'build', 'index.js'); return path.posix.join(this.#workerData.bootRequest.dir, "build", "index.js");
} }
#pathPkg() { #pathPkg() {
return path.join(this.#workerData.bootRequest.dir, 'package.json'); return path.join(this.#workerData.bootRequest.dir, "package.json");
} }
#unimportModule() { #unimportModule() {
@@ -545,10 +549,10 @@ export class PluginInstance {
} }
#sendEmpty(context: PluginContext, replyId: string | null = null): string { #sendEmpty(context: PluginContext, replyId: string | null = null): string {
return this.#sendPayload(context, { type: 'empty_response' }, replyId); return this.#sendPayload(context, { type: "empty_response" }, replyId);
} }
#sendForReply<T extends Omit<InternalEventPayload, 'type'>>( #sendForReply<T extends Omit<InternalEventPayload, "type">>(
context: PluginContext, context: PluginContext,
payload: InternalEventPayload, payload: InternalEventPayload,
): Promise<T> { ): Promise<T> {
@@ -599,7 +603,7 @@ export class PluginInstance {
throw new Error("Can't get window context without an active window"); throw new Error("Can't get window context without an active window");
} }
const payload: InternalEventPayload = { const payload: InternalEventPayload = {
type: 'window_info_request', type: "window_info_request",
label: context.label, label: context.label,
}; };
@@ -610,7 +614,7 @@ export class PluginInstance {
clipboard: { clipboard: {
copyText: async (text) => { copyText: async (text) => {
await this.#sendForReply(context, { await this.#sendForReply(context, {
type: 'copy_text_request', type: "copy_text_request",
text, text,
}); });
}, },
@@ -618,7 +622,7 @@ export class PluginInstance {
toast: { toast: {
show: async (args) => { show: async (args) => {
await this.#sendForReply(context, { await this.#sendForReply(context, {
type: 'show_toast_request', type: "show_toast_request",
// Handle default here because null/undefined both convert to None in Rust translation // Handle default here because null/undefined both convert to None in Rust translation
timeout: args.timeout === undefined ? 5000 : args.timeout, timeout: args.timeout === undefined ? 5000 : args.timeout,
...args, ...args,
@@ -637,11 +641,11 @@ export class PluginInstance {
}, },
openUrl: async ({ onNavigate, onClose, ...args }) => { openUrl: async ({ onNavigate, onClose, ...args }) => {
args.label = args.label || `${Math.random()}`; args.label = args.label || `${Math.random()}`;
const payload: InternalEventPayload = { type: 'open_window_request', ...args }; const payload: InternalEventPayload = { type: "open_window_request", ...args };
const onEvent = (event: InternalEventPayload) => { const onEvent = (event: InternalEventPayload) => {
if (event.type === 'window_navigate_event') { if (event.type === "window_navigate_event") {
onNavigate?.(event); onNavigate?.(event);
} else if (event.type === 'window_close_event') { } else if (event.type === "window_close_event") {
onClose?.(); onClose?.();
} }
}; };
@@ -649,7 +653,7 @@ export class PluginInstance {
return { return {
close: () => { close: () => {
const closePayload: InternalEventPayload = { const closePayload: InternalEventPayload = {
type: 'close_window_request', type: "close_window_request",
label: args.label, label: args.label,
}; };
this.#sendPayload(context, closePayload, null); this.#sendPayload(context, closePayload, null);
@@ -658,7 +662,7 @@ export class PluginInstance {
}, },
openExternalUrl: async (url) => { openExternalUrl: async (url) => {
await this.#sendForReply(context, { await this.#sendForReply(context, {
type: 'open_external_url_request', type: "open_external_url_request",
url, url,
}); });
}, },
@@ -666,7 +670,7 @@ export class PluginInstance {
prompt: { prompt: {
text: async (args) => { text: async (args) => {
const reply: PromptTextResponse = await this.#sendForReply(context, { const reply: PromptTextResponse = await this.#sendForReply(context, {
type: 'prompt_text_request', type: "prompt_text_request",
...args, ...args,
}); });
return reply.value; return reply.value;
@@ -685,7 +689,7 @@ export class PluginInstance {
// Build the event manually so we can get the event ID for keying // Build the event manually so we can get the event ID for keying
const eventToSend = this.#buildEventToSend( const eventToSend = this.#buildEventToSend(
context, context,
{ type: 'prompt_form_request', ...args, inputs: strippedInputs }, { type: "prompt_form_request", ...args, inputs: strippedInputs },
null, null,
); );
@@ -696,7 +700,7 @@ export class PluginInstance {
const cb = (event: InternalEvent) => { const cb = (event: InternalEvent) => {
if (event.replyId !== eventToSend.id) return; if (event.replyId !== eventToSend.id) return;
if (event.payload.type === 'prompt_form_response') { if (event.payload.type === "prompt_form_response") {
const { done, values } = event.payload as PromptFormResponse; const { done, values } = event.payload as PromptFormResponse;
if (done) { if (done) {
// Final response — resolve the promise and clean up // Final response — resolve the promise and clean up
@@ -715,12 +719,12 @@ export class PluginInstance {
const stripped = stripDynamicCallbacks(resolvedInputs); const stripped = stripDynamicCallbacks(resolvedInputs);
this.#sendPayload( this.#sendPayload(
context, context,
{ type: 'prompt_form_request', ...args, inputs: stripped }, { type: "prompt_form_request", ...args, inputs: stripped },
eventToSend.id, eventToSend.id,
); );
}) })
.catch((err) => { .catch((err) => {
console.error('Failed to resolve dynamic form inputs', err); console.error("Failed to resolve dynamic form inputs", err);
}); });
} }
} }
@@ -738,7 +742,7 @@ export class PluginInstance {
httpResponse: { httpResponse: {
find: async (args) => { find: async (args) => {
const payload = { const payload = {
type: 'find_http_responses_request', type: "find_http_responses_request",
...args, ...args,
} as const; } as const;
const { httpResponses } = await this.#sendForReply<FindHttpResponsesResponse>( const { httpResponses } = await this.#sendForReply<FindHttpResponsesResponse>(
@@ -751,7 +755,7 @@ export class PluginInstance {
grpcRequest: { grpcRequest: {
render: async (args) => { render: async (args) => {
const payload = { const payload = {
type: 'render_grpc_request_request', type: "render_grpc_request_request",
...args, ...args,
} as const; } as const;
const { grpcRequest } = await this.#sendForReply<RenderGrpcRequestResponse>( const { grpcRequest } = await this.#sendForReply<RenderGrpcRequestResponse>(
@@ -764,7 +768,7 @@ export class PluginInstance {
httpRequest: { httpRequest: {
getById: async (args) => { getById: async (args) => {
const payload = { const payload = {
type: 'get_http_request_by_id_request', type: "get_http_request_by_id_request",
...args, ...args,
} as const; } as const;
const { httpRequest } = await this.#sendForReply<GetHttpRequestByIdResponse>( const { httpRequest } = await this.#sendForReply<GetHttpRequestByIdResponse>(
@@ -775,7 +779,7 @@ export class PluginInstance {
}, },
send: async (args) => { send: async (args) => {
const payload = { const payload = {
type: 'send_http_request_request', type: "send_http_request_request",
...args, ...args,
} as const; } as const;
const { httpResponse } = await this.#sendForReply<SendHttpRequestResponse>( const { httpResponse } = await this.#sendForReply<SendHttpRequestResponse>(
@@ -786,7 +790,7 @@ export class PluginInstance {
}, },
render: async (args) => { render: async (args) => {
const payload = { const payload = {
type: 'render_http_request_request', type: "render_http_request_request",
...args, ...args,
} as const; } as const;
const { httpRequest } = await this.#sendForReply<RenderHttpRequestResponse>( const { httpRequest } = await this.#sendForReply<RenderHttpRequestResponse>(
@@ -797,9 +801,9 @@ export class PluginInstance {
}, },
list: async (args?: { folderId?: string }) => { list: async (args?: { folderId?: string }) => {
const payload: InternalEventPayload = { const payload: InternalEventPayload = {
type: 'list_http_requests_request', type: "list_http_requests_request",
folderId: args?.folderId, folderId: args?.folderId,
} satisfies ListHttpRequestsRequest & { type: 'list_http_requests_request' }; } satisfies ListHttpRequestsRequest & { type: "list_http_requests_request" };
const { httpRequests } = await this.#sendForReply<ListHttpRequestsResponse>( const { httpRequests } = await this.#sendForReply<ListHttpRequestsResponse>(
context, context,
payload, payload,
@@ -808,13 +812,13 @@ export class PluginInstance {
}, },
create: async (args) => { create: async (args) => {
const payload = { const payload = {
type: 'upsert_model_request', type: "upsert_model_request",
model: { model: {
name: '', name: "",
method: 'GET', method: "GET",
...args, ...args,
id: '', id: "",
model: 'http_request', model: "http_request",
}, },
} as InternalEventPayload; } as InternalEventPayload;
const response = await this.#sendForReply<UpsertModelResponse>(context, payload); const response = await this.#sendForReply<UpsertModelResponse>(context, payload);
@@ -822,9 +826,9 @@ export class PluginInstance {
}, },
update: async (args) => { update: async (args) => {
const payload = { const payload = {
type: 'upsert_model_request', type: "upsert_model_request",
model: { model: {
model: 'http_request', model: "http_request",
...args, ...args,
}, },
} as InternalEventPayload; } as InternalEventPayload;
@@ -833,8 +837,8 @@ export class PluginInstance {
}, },
delete: async (args) => { delete: async (args) => {
const payload = { const payload = {
type: 'delete_model_request', type: "delete_model_request",
model: 'http_request', model: "http_request",
id: args.id, id: args.id,
} as InternalEventPayload; } as InternalEventPayload;
const response = await this.#sendForReply<DeleteModelResponse>(context, payload); const response = await this.#sendForReply<DeleteModelResponse>(context, payload);
@@ -843,23 +847,23 @@ export class PluginInstance {
}, },
folder: { folder: {
list: async () => { list: async () => {
const payload = { type: 'list_folders_request' } as const; const payload = { type: "list_folders_request" } as const;
const { folders } = await this.#sendForReply<ListFoldersResponse>(context, payload); const { folders } = await this.#sendForReply<ListFoldersResponse>(context, payload);
return folders; return folders;
}, },
getById: async (args: { id: string }) => { getById: async (args: { id: string }) => {
const payload = { type: 'list_folders_request' } as const; const payload = { type: "list_folders_request" } as const;
const { folders } = await this.#sendForReply<ListFoldersResponse>(context, payload); const { folders } = await this.#sendForReply<ListFoldersResponse>(context, payload);
return folders.find((f) => f.id === args.id) ?? null; return folders.find((f) => f.id === args.id) ?? null;
}, },
create: async ({ name, ...args }) => { create: async ({ name, ...args }) => {
const payload = { const payload = {
type: 'upsert_model_request', type: "upsert_model_request",
model: { model: {
...args, ...args,
name: name ?? '', name: name ?? "",
id: '', id: "",
model: 'folder', model: "folder",
}, },
} as InternalEventPayload; } as InternalEventPayload;
const response = await this.#sendForReply<UpsertModelResponse>(context, payload); const response = await this.#sendForReply<UpsertModelResponse>(context, payload);
@@ -867,9 +871,9 @@ export class PluginInstance {
}, },
update: async (args) => { update: async (args) => {
const payload = { const payload = {
type: 'upsert_model_request', type: "upsert_model_request",
model: { model: {
model: 'folder', model: "folder",
...args, ...args,
}, },
} as InternalEventPayload; } as InternalEventPayload;
@@ -878,8 +882,8 @@ export class PluginInstance {
}, },
delete: async (args: { id: string }) => { delete: async (args: { id: string }) => {
const payload = { const payload = {
type: 'delete_model_request', type: "delete_model_request",
model: 'folder', model: "folder",
id: args.id, id: args.id,
} as InternalEventPayload; } as InternalEventPayload;
const response = await this.#sendForReply<DeleteModelResponse>(context, payload); const response = await this.#sendForReply<DeleteModelResponse>(context, payload);
@@ -889,14 +893,14 @@ export class PluginInstance {
cookies: { cookies: {
getValue: async (args: GetCookieValueRequest) => { getValue: async (args: GetCookieValueRequest) => {
const payload = { const payload = {
type: 'get_cookie_value_request', type: "get_cookie_value_request",
...args, ...args,
} as const; } as const;
const { value } = await this.#sendForReply<GetCookieValueResponse>(context, payload); const { value } = await this.#sendForReply<GetCookieValueResponse>(context, payload);
return value; return value;
}, },
listNames: async () => { listNames: async () => {
const payload = { type: 'list_cookie_names_request' } as const; const payload = { type: "list_cookie_names_request" } as const;
const { names } = await this.#sendForReply<ListCookieNamesResponse>(context, payload); const { names } = await this.#sendForReply<ListCookieNamesResponse>(context, payload);
return names; return names;
}, },
@@ -907,42 +911,42 @@ export class PluginInstance {
* (eg. object), it will be recursively rendered. * (eg. object), it will be recursively rendered.
*/ */
render: async (args: TemplateRenderRequest) => { render: async (args: TemplateRenderRequest) => {
const payload = { type: 'template_render_request', ...args } as const; const payload = { type: "template_render_request", ...args } as const;
const result = await this.#sendForReply<TemplateRenderResponse>(context, payload); const result = await this.#sendForReply<TemplateRenderResponse>(context, payload);
// biome-ignore lint/suspicious/noExplicitAny: That's okay // oxlint-disable-next-line no-explicit-any -- That's okay
return result.data as any; return result.data as any;
}, },
}, },
store: { store: {
get: async <T>(key: string) => { get: async <T>(key: string) => {
const payload = { type: 'get_key_value_request', key } as const; const payload = { type: "get_key_value_request", key } as const;
const result = await this.#sendForReply<GetKeyValueResponse>(context, payload); const result = await this.#sendForReply<GetKeyValueResponse>(context, payload);
return result.value ? (JSON.parse(result.value) as T) : undefined; return result.value ? (JSON.parse(result.value) as T) : undefined;
}, },
set: async <T>(key: string, value: T) => { set: async <T>(key: string, value: T) => {
const valueStr = JSON.stringify(value); const valueStr = JSON.stringify(value);
const payload: InternalEventPayload = { const payload: InternalEventPayload = {
type: 'set_key_value_request', type: "set_key_value_request",
key, key,
value: valueStr, value: valueStr,
}; };
await this.#sendForReply<GetKeyValueResponse>(context, payload); await this.#sendForReply<GetKeyValueResponse>(context, payload);
}, },
delete: async (key: string) => { delete: async (key: string) => {
const payload = { type: 'delete_key_value_request', key } as const; const payload = { type: "delete_key_value_request", key } as const;
const result = await this.#sendForReply<DeleteKeyValueResponse>(context, payload); const result = await this.#sendForReply<DeleteKeyValueResponse>(context, payload);
return result.deleted; return result.deleted;
}, },
}, },
plugin: { plugin: {
reload: () => { reload: () => {
this.#sendPayload(context, { type: 'reload_response', silent: true }, null); this.#sendPayload(context, { type: "reload_response", silent: true }, null);
}, },
}, },
workspace: { workspace: {
list: async () => { list: async () => {
const payload = { const payload = {
type: 'list_open_workspaces_request', type: "list_open_workspaces_request",
} as InternalEventPayload; } as InternalEventPayload;
const response = await this.#sendForReply<ListOpenWorkspacesResponse>(context, payload); const response = await this.#sendForReply<ListOpenWorkspacesResponse>(context, payload);
return response.workspaces.map((w) => { return response.workspaces.map((w) => {
@@ -972,9 +976,9 @@ export class PluginInstance {
function stripDynamicCallbacks(inputs: { dynamic?: unknown }[]): FormInput[] { function stripDynamicCallbacks(inputs: { dynamic?: unknown }[]): FormInput[] {
return inputs.map((input) => { return inputs.map((input) => {
// biome-ignore lint/suspicious/noExplicitAny: stripping dynamic from union type // oxlint-disable-next-line no-explicit-any -- stripping dynamic from union type
const { dynamic, ...rest } = input as any; const { dynamic: _dynamic, ...rest } = input as any;
if ('inputs' in rest && Array.isArray(rest.inputs)) { if ("inputs" in rest && Array.isArray(rest.inputs)) {
rest.inputs = stripDynamicCallbacks(rest.inputs); rest.inputs = stripDynamicCallbacks(rest.inputs);
} }
return rest as FormInput; return rest as FormInput;
@@ -982,8 +986,8 @@ function stripDynamicCallbacks(inputs: { dynamic?: unknown }[]): FormInput[] {
} }
function genId(len = 5): string { function genId(len = 5): string {
const alphabet = '01234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; const alphabet = "01234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
let id = ''; let id = "";
for (let i = 0; i < len; i++) { for (let i = 0; i < len; i++) {
id += alphabet[Math.floor(Math.random() * alphabet.length)]; id += alphabet[Math.floor(Math.random() * alphabet.length)];
} }
@@ -1003,7 +1007,7 @@ function watchFile(filepath: string, cb: () => void) {
const stat = statSync(filepath, { throwIfNoEntry: false }); const stat = statSync(filepath, { throwIfNoEntry: false });
if (stat == null || stat.mtimeMs !== watchedFiles[filepath]?.mtimeMs) { if (stat == null || stat.mtimeMs !== watchedFiles[filepath]?.mtimeMs) {
watchedFiles[filepath] = stat ?? null; watchedFiles[filepath] = stat ?? null;
console.log('[plugin-runtime] watchFile triggered', filepath); console.log("[plugin-runtime] watchFile triggered", filepath);
cb(); cb();
} }
}); });

View File

@@ -4,11 +4,11 @@ import type {
DynamicAuthenticationArg, DynamicAuthenticationArg,
DynamicPromptFormArg, DynamicPromptFormArg,
DynamicTemplateFunctionArg, DynamicTemplateFunctionArg,
} from '@yaakapp/api'; } from "@yaakapp/api";
import type { import type {
CallHttpAuthenticationActionArgs, CallHttpAuthenticationActionArgs,
CallTemplateFunctionArgs, CallTemplateFunctionArgs,
} from '@yaakapp-internal/plugins'; } from "@yaakapp-internal/plugins";
type AnyDynamicArg = DynamicTemplateFunctionArg | DynamicAuthenticationArg | DynamicPromptFormArg; type AnyDynamicArg = DynamicTemplateFunctionArg | DynamicAuthenticationArg | DynamicPromptFormArg;
type AnyCallArgs = type AnyCallArgs =
@@ -42,7 +42,7 @@ export async function applyDynamicFormInput(
const resolvedArgs: AnyDynamicArg[] = []; const resolvedArgs: AnyDynamicArg[] = [];
for (const { dynamic, ...arg } of args) { for (const { dynamic, ...arg } of args) {
const dynamicResult = const dynamicResult =
typeof dynamic === 'function' typeof dynamic === "function"
? await dynamic( ? await dynamic(
ctx, ctx,
callArgs as CallTemplateFunctionArgs & callArgs as CallTemplateFunctionArgs &
@@ -56,7 +56,7 @@ export async function applyDynamicFormInput(
...dynamicResult, ...dynamicResult,
} as AnyDynamicArg; } as AnyDynamicArg;
if ('inputs' in newArg && Array.isArray(newArg.inputs)) { if ("inputs" in newArg && Array.isArray(newArg.inputs)) {
try { try {
newArg.inputs = await applyDynamicFormInput( newArg.inputs = await applyDynamicFormInput(
ctx, ctx,
@@ -66,7 +66,7 @@ export async function applyDynamicFormInput(
CallPromptFormDynamicArgs, CallPromptFormDynamicArgs,
); );
} catch (e) { } catch (e) {
console.error('Failed to apply dynamic form input', e); console.error("Failed to apply dynamic form input", e);
} }
} }
resolvedArgs.push(newArg); resolvedArgs.push(newArg);

View File

@@ -1,16 +1,16 @@
import type { InternalEvent } from '@yaakapp/api'; import type { InternalEvent } from "@yaakapp/api";
import WebSocket from 'ws'; import WebSocket from "ws";
import { EventChannel } from './EventChannel'; import { EventChannel } from "./EventChannel";
import { PluginHandle } from './PluginHandle'; import { PluginHandle } from "./PluginHandle";
const port = process.env.PORT; const port = process.env.PORT;
if (!port) { if (!port) {
throw new Error('Plugin runtime missing PORT'); throw new Error("Plugin runtime missing PORT");
} }
const host = process.env.HOST; const host = process.env.HOST;
if (!host) { if (!host) {
throw new Error('Plugin runtime missing HOST'); throw new Error("Plugin runtime missing HOST");
} }
const pluginToAppEvents = new EventChannel(); const pluginToAppEvents = new EventChannel();
@@ -18,16 +18,16 @@ const plugins: Record<string, PluginHandle> = {};
const ws = new WebSocket(`ws://${host}:${port}`); const ws = new WebSocket(`ws://${host}:${port}`);
ws.on('message', async (e: Buffer) => { ws.on("message", async (e: Buffer) => {
try { try {
await handleIncoming(e.toString()); await handleIncoming(e.toString());
} catch (err) { } catch (err) {
console.log('Failed to handle incoming plugin event', err); console.log("Failed to handle incoming plugin event", err);
} }
}); });
ws.on('open', () => console.log('Plugin runtime connected to websocket')); ws.on("open", () => console.log("Plugin runtime connected to websocket"));
ws.on('error', (err: unknown) => console.error('Plugin runtime websocket error', err)); ws.on("error", (err: unknown) => console.error("Plugin runtime websocket error", err));
ws.on('close', (code: number) => console.log('Plugin runtime websocket closed', code)); ws.on("close", (code: number) => console.log("Plugin runtime websocket closed", code));
// Listen for incoming events from plugins // Listen for incoming events from plugins
pluginToAppEvents.listen((e) => { pluginToAppEvents.listen((e) => {
@@ -38,7 +38,7 @@ pluginToAppEvents.listen((e) => {
async function handleIncoming(msg: string) { async function handleIncoming(msg: string) {
const pluginEvent: InternalEvent = JSON.parse(msg); const pluginEvent: InternalEvent = JSON.parse(msg);
// Handle special event to bootstrap plugin // Handle special event to bootstrap plugin
if (pluginEvent.payload.type === 'boot_request') { if (pluginEvent.payload.type === "boot_request") {
const plugin = new PluginHandle( const plugin = new PluginHandle(
pluginEvent.pluginRefId, pluginEvent.pluginRefId,
pluginEvent.context, pluginEvent.context,
@@ -51,23 +51,23 @@ async function handleIncoming(msg: string) {
// Once booted, forward all events to the plugin worker // Once booted, forward all events to the plugin worker
const plugin = plugins[pluginEvent.pluginRefId]; const plugin = plugins[pluginEvent.pluginRefId];
if (!plugin) { if (!plugin) {
console.warn('Failed to get plugin for event by', pluginEvent.pluginRefId); console.warn("Failed to get plugin for event by", pluginEvent.pluginRefId);
return; return;
} }
if (pluginEvent.payload.type === 'terminate_request') { if (pluginEvent.payload.type === "terminate_request") {
await plugin.terminate(); await plugin.terminate();
console.log('Terminated plugin worker', pluginEvent.pluginRefId); console.log("Terminated plugin worker", pluginEvent.pluginRefId);
delete plugins[pluginEvent.pluginRefId]; delete plugins[pluginEvent.pluginRefId];
} }
plugin.sendToWorker(pluginEvent); plugin.sendToWorker(pluginEvent);
} }
process.on('unhandledRejection', (reason, promise) => { process.on("unhandledRejection", (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason); console.error("Unhandled Rejection at:", promise, "reason:", reason);
}); });
process.on('uncaughtException', (error) => { process.on("uncaughtException", (error) => {
console.error('Uncaught Exception:', error); console.error("Uncaught Exception:", error);
}); });

View File

@@ -1,4 +1,5 @@
import process from 'node:process'; /* oxlint-disable unbound-method */
import process from "node:process";
export function interceptStdout(intercept: (text: string) => string) { export function interceptStdout(intercept: (text: string) => string) {
const old_stdout_write = process.stdout.write; const old_stdout_write = process.stdout.write;
@@ -24,5 +25,5 @@ export function interceptStdout(intercept: (text: string) => string) {
} }
function interceptor(text: string, fn: (text: string) => string) { function interceptor(text: string, fn: (text: string) => string) {
return fn(text).replace(/\n$/, '') + (fn(text) && /\n$/.test(text) ? '\n' : ''); return fn(text).replace(/\n$/, "") + (fn(text) && text.endsWith("\n") ? "\n" : "");
} }

View File

@@ -1,16 +1,16 @@
import type { TemplateFunctionPlugin } from '@yaakapp/api'; import type { TemplateFunctionPlugin } from "@yaakapp/api";
export function migrateTemplateFunctionSelectOptions( export function migrateTemplateFunctionSelectOptions(
f: TemplateFunctionPlugin, f: TemplateFunctionPlugin,
): TemplateFunctionPlugin { ): TemplateFunctionPlugin {
const migratedArgs = f.args.map((a) => { const migratedArgs = f.args.map((a) => {
if (a.type === 'select') { if (a.type === "select") {
// Migrate old options that had 'name' instead of 'label' // Migrate old options that had 'name' instead of 'label'
type LegacyOption = { label?: string; value: string; name?: string }; type LegacyOption = { label?: string; value: string; name?: string };
a.options = a.options.map((o) => { a.options = a.options.map((o) => {
const legacy = o as LegacyOption; const legacy = o as LegacyOption;
return { return {
label: legacy.label ?? legacy.name ?? '', label: legacy.label ?? legacy.name ?? "",
value: legacy.value, value: legacy.value,
}; };
}); });

View File

@@ -1,106 +1,106 @@
import { applyFormInputDefaults } from '@yaakapp-internal/lib/templateFunction'; import { applyFormInputDefaults } from "@yaakapp-internal/lib/templateFunction";
import type { CallTemplateFunctionArgs } from '@yaakapp-internal/plugins'; import type { CallTemplateFunctionArgs } from "@yaakapp-internal/plugins";
import type { Context, DynamicTemplateFunctionArg } from '@yaakapp/api'; import type { Context, DynamicTemplateFunctionArg } from "@yaakapp/api";
import { describe, expect, test } from 'vitest'; import { describe, expect, test } from "vite-plus/test";
import { applyDynamicFormInput } from '../src/common'; import { applyDynamicFormInput } from "../src/common";
describe('applyFormInputDefaults', () => { describe("applyFormInputDefaults", () => {
test('Works with top-level select', () => { test("Works with top-level select", () => {
const args: DynamicTemplateFunctionArg[] = [ const args: DynamicTemplateFunctionArg[] = [
{ {
type: 'select', type: "select",
name: 'test', name: "test",
options: [{ label: 'Option 1', value: 'one' }], options: [{ label: "Option 1", value: "one" }],
defaultValue: 'one', defaultValue: "one",
}, },
]; ];
expect(applyFormInputDefaults(args, {})).toEqual({ expect(applyFormInputDefaults(args, {})).toEqual({
test: 'one', test: "one",
}); });
}); });
test('Works with existing value', () => { test("Works with existing value", () => {
const args: DynamicTemplateFunctionArg[] = [ const args: DynamicTemplateFunctionArg[] = [
{ {
type: 'select', type: "select",
name: 'test', name: "test",
options: [{ label: 'Option 1', value: 'one' }], options: [{ label: "Option 1", value: "one" }],
defaultValue: 'one', defaultValue: "one",
}, },
]; ];
expect(applyFormInputDefaults(args, { test: 'explicit' })).toEqual({ expect(applyFormInputDefaults(args, { test: "explicit" })).toEqual({
test: 'explicit', test: "explicit",
}); });
}); });
test('Works with recursive select', () => { test("Works with recursive select", () => {
const args: DynamicTemplateFunctionArg[] = [ const args: DynamicTemplateFunctionArg[] = [
{ type: 'text', name: 'dummy', defaultValue: 'top' }, { type: "text", name: "dummy", defaultValue: "top" },
{ {
type: 'accordion', type: "accordion",
label: 'Test', label: "Test",
inputs: [ inputs: [
{ type: 'text', name: 'name', defaultValue: 'hello' }, { type: "text", name: "name", defaultValue: "hello" },
{ {
type: 'select', type: "select",
name: 'test', name: "test",
options: [{ label: 'Option 1', value: 'one' }], options: [{ label: "Option 1", value: "one" }],
defaultValue: 'one', defaultValue: "one",
}, },
], ],
}, },
]; ];
expect(applyFormInputDefaults(args, {})).toEqual({ expect(applyFormInputDefaults(args, {})).toEqual({
dummy: 'top', dummy: "top",
test: 'one', test: "one",
name: 'hello', name: "hello",
}); });
}); });
test('Works with dynamic options', () => { test("Works with dynamic options", () => {
const args: DynamicTemplateFunctionArg[] = [ const args: DynamicTemplateFunctionArg[] = [
{ {
type: 'select', type: "select",
name: 'test', name: "test",
defaultValue: 'one', defaultValue: "one",
options: [], options: [],
dynamic() { dynamic() {
return { options: [{ label: 'Option 1', value: 'one' }] }; return { options: [{ label: "Option 1", value: "one" }] };
}, },
}, },
]; ];
expect(applyFormInputDefaults(args, {})).toEqual({ expect(applyFormInputDefaults(args, {})).toEqual({
test: 'one', test: "one",
}); });
expect(applyFormInputDefaults(args, {})).toEqual({ expect(applyFormInputDefaults(args, {})).toEqual({
test: 'one', test: "one",
}); });
}); });
}); });
describe('applyDynamicFormInput', () => { describe("applyDynamicFormInput", () => {
test('Works with plain input', async () => { test("Works with plain input", async () => {
const ctx = {} as Context; const ctx = {} as Context;
const args: DynamicTemplateFunctionArg[] = [ const args: DynamicTemplateFunctionArg[] = [
{ type: 'text', name: 'name' }, { type: "text", name: "name" },
{ type: 'checkbox', name: 'checked' }, { type: "checkbox", name: "checked" },
]; ];
const callArgs: CallTemplateFunctionArgs = { const callArgs: CallTemplateFunctionArgs = {
values: {}, values: {},
purpose: 'preview', purpose: "preview",
}; };
expect(await applyDynamicFormInput(ctx, args, callArgs)).toEqual([ expect(await applyDynamicFormInput(ctx, args, callArgs)).toEqual([
{ type: 'text', name: 'name' }, { type: "text", name: "name" },
{ type: 'checkbox', name: 'checked' }, { type: "checkbox", name: "checked" },
]); ]);
}); });
test('Works with dynamic input', async () => { test("Works with dynamic input", async () => {
const ctx = {} as Context; const ctx = {} as Context;
const args: DynamicTemplateFunctionArg[] = [ const args: DynamicTemplateFunctionArg[] = [
{ {
type: 'text', type: "text",
name: 'name', name: "name",
async dynamic(_ctx, _args) { async dynamic(_ctx, _args) {
return { hidden: true }; return { hidden: true };
}, },
@@ -108,28 +108,28 @@ describe('applyDynamicFormInput', () => {
]; ];
const callArgs: CallTemplateFunctionArgs = { const callArgs: CallTemplateFunctionArgs = {
values: {}, values: {},
purpose: 'preview', purpose: "preview",
}; };
expect(await applyDynamicFormInput(ctx, args, callArgs)).toEqual([ expect(await applyDynamicFormInput(ctx, args, callArgs)).toEqual([
{ type: 'text', name: 'name', hidden: true }, { type: "text", name: "name", hidden: true },
]); ]);
}); });
test('Works with recursive dynamic input', async () => { test("Works with recursive dynamic input", async () => {
const ctx = {} as Context; const ctx = {} as Context;
const callArgs: CallTemplateFunctionArgs = { const callArgs: CallTemplateFunctionArgs = {
values: { hello: 'world' }, values: { hello: "world" },
purpose: 'preview', purpose: "preview",
}; };
const args: DynamicTemplateFunctionArg[] = [ const args: DynamicTemplateFunctionArg[] = [
{ {
type: 'banner', type: "banner",
inputs: [ inputs: [
{ {
type: 'text', type: "text",
name: 'name', name: "name",
async dynamic(_ctx, args) { async dynamic(_ctx, args) {
return { hidden: args.values.hello === 'world' }; return { hidden: args.values.hello === "world" };
}, },
}, },
], ],
@@ -137,11 +137,11 @@ describe('applyDynamicFormInput', () => {
]; ];
expect(await applyDynamicFormInput(ctx, args, callArgs)).toEqual([ expect(await applyDynamicFormInput(ctx, args, callArgs)).toEqual([
{ {
type: 'banner', type: "banner",
inputs: [ inputs: [
{ {
type: 'text', type: "text",
name: 'name', name: "name",
hidden: true, hidden: true,
}, },
], ],

View File

@@ -10,11 +10,7 @@
"moduleResolution": "node16", "moduleResolution": "node16",
"resolveJsonModule": true, "resolveJsonModule": true,
"sourceMap": true, "sourceMap": true,
"outDir": "build", "outDir": "build"
"baseUrl": ".",
"paths": {
"*": ["node_modules/*", "src/types/*"]
}
}, },
"include": ["src"] "include": ["src"]
} }

View File

@@ -50,7 +50,7 @@ This will generate a random JSON body on every request:
The plugin provides access to all FakerJS modules and their methods: The plugin provides access to all FakerJS modules and their methods:
| Category | Description | Example Methods | | Category | Description | Example Methods |
|------------|---------------------------|--------------------------------------------| | ---------- | ------------------------- | ------------------------------------------ |
| `airline` | Airline-related data | `aircraftType`, `airline`, `airplane` | | `airline` | Airline-related data | `aircraftType`, `airline`, `airplane` |
| `animal` | Animal names and types | `bear`, `bird`, `cat`, `dog`, `fish` | | `animal` | Animal names and types | `bear`, `bird`, `cat`, `dog`, `fish` |
| `color` | Colors in various formats | `human`, `rgb`, `hex`, `hsl` | | `color` | Colors in various formats | `human`, `rgb`, `hex`, `hsl` |

View File

@@ -1,8 +1,8 @@
{ {
"name": "@yaak/faker", "name": "@yaak/faker",
"private": true,
"version": "1.1.1",
"displayName": "Faker", "displayName": "Faker",
"version": "1.1.1",
"private": true,
"description": "Template functions for generating fake data using FakerJS", "description": "Template functions for generating fake data using FakerJS",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -12,7 +12,7 @@
"scripts": { "scripts": {
"build": "yaakcli build", "build": "yaakcli build",
"dev": "yaakcli dev", "dev": "yaakcli dev",
"test": "vitest --run tests" "test": "vp test --run tests"
}, },
"dependencies": { "dependencies": {
"@faker-js/faker": "^10.1.0" "@faker-js/faker": "^10.1.0"

View File

@@ -1,34 +1,34 @@
import { faker } from '@faker-js/faker'; import { faker } from "@faker-js/faker";
import type { DynamicTemplateFunctionArg, PluginDefinition } from '@yaakapp/api'; import type { DynamicTemplateFunctionArg, PluginDefinition } from "@yaakapp/api";
const modules = [ const modules = [
'airline', "airline",
'animal', "animal",
'color', "color",
'commerce', "commerce",
'company', "company",
'database', "database",
'date', "date",
'finance', "finance",
'git', "git",
'hacker', "hacker",
'image', "image",
'internet', "internet",
'location', "location",
'lorem', "lorem",
'person', "person",
'music', "music",
'number', "number",
'phone', "phone",
'science', "science",
'string', "string",
'system', "system",
'vehicle', "vehicle",
'word', "word",
]; ];
function normalizeResult(result: unknown): string { function normalizeResult(result: unknown): string {
if (typeof result === 'string') return result; if (typeof result === "string") return result;
if (result instanceof Date) return result.toISOString(); if (result instanceof Date) return result.toISOString();
return JSON.stringify(result); return JSON.stringify(result);
} }
@@ -37,20 +37,20 @@ function normalizeResult(result: unknown): string {
function args(modName: string, fnName: string): DynamicTemplateFunctionArg[] { function args(modName: string, fnName: string): DynamicTemplateFunctionArg[] {
return [ return [
{ {
type: 'banner', type: "banner",
color: 'info', color: "info",
inputs: [ inputs: [
{ {
type: 'markdown', type: "markdown",
content: `Need help? View documentation for [\`${modName}.${fnName}(…)\`](https://fakerjs.dev/api/${encodeURIComponent(modName)}.html#${encodeURIComponent(fnName)})`, content: `Need help? View documentation for [\`${modName}.${fnName}(…)\`](https://fakerjs.dev/api/${encodeURIComponent(modName)}.html#${encodeURIComponent(fnName)})`,
}, },
], ],
}, },
{ {
name: 'options', name: "options",
label: 'Arguments', label: "Arguments",
type: 'editor', type: "editor",
language: 'json', language: "json",
optional: true, optional: true,
placeholder: 'e.g. { "min": 1, "max": 10 } or 10 or ["en","US"]', placeholder: 'e.g. { "min": 1, "max": 10 } or 10 or ["en","US"]',
}, },
@@ -61,22 +61,22 @@ export const plugin: PluginDefinition = {
templateFunctions: modules.flatMap((modName) => { templateFunctions: modules.flatMap((modName) => {
const mod = faker[modName as keyof typeof faker]; const mod = faker[modName as keyof typeof faker];
return Object.keys(mod) return Object.keys(mod)
.filter((n) => n !== 'faker') .filter((n) => n !== "faker")
.map((fnName) => ({ .map((fnName) => ({
name: ['faker', modName, fnName].join('.'), name: ["faker", modName, fnName].join("."),
args: args(modName, fnName), args: args(modName, fnName),
async onRender(_ctx, args) { async onRender(_ctx, args) {
const fn = mod[fnName as keyof typeof mod] as (...a: unknown[]) => unknown; const fn = mod[fnName as keyof typeof mod] as (...a: unknown[]) => unknown;
const options = args.values.options; const options = args.values.options;
// No options supplied // No options supplied
if (options == null || options === '') { if (options == null || options === "") {
return normalizeResult(fn()); return normalizeResult(fn());
} }
// Try JSON first // Try JSON first
let parsed: unknown = options; let parsed: unknown = options;
if (typeof options === 'string') { if (typeof options === "string") {
try { try {
parsed = JSON.parse(options); parsed = JSON.parse(options);
} catch { } catch {

View File

@@ -1,8 +1,8 @@
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from "vite-plus/test";
describe('template-function-faker', () => { describe("template-function-faker", () => {
it('exports all expected template functions', async () => { it("exports all expected template functions", async () => {
const { plugin } = await import('../src/index'); const { plugin } = await import("../src/index");
const names = plugin.templateFunctions?.map((fn) => fn.name).sort() ?? []; const names = plugin.templateFunctions?.map((fn) => fn.name).sort() ?? [];
// Snapshot the full list of exported function names so we catch any // Snapshot the full list of exported function names so we catch any
@@ -10,12 +10,13 @@ describe('template-function-faker', () => {
expect(names).toMatchSnapshot(); expect(names).toMatchSnapshot();
}); });
it('renders date results as unquoted ISO strings', async () => { it("renders date results as unquoted ISO strings", async () => {
const { plugin } = await import('../src/index'); const { plugin } = await import("../src/index");
const fn = plugin.templateFunctions?.find((fn) => fn.name === 'faker.date.future'); const fn = plugin.templateFunctions?.find((fn) => fn.name === "faker.date.future");
// oxlint-disable-next-line unbound-method
const onRender = fn?.onRender; const onRender = fn?.onRender;
expect(onRender).toBeTypeOf('function'); expect(onRender).toBeTypeOf("function");
if (onRender == null) { if (onRender == null) {
throw new Error("Expected template function 'faker.date.future' to define onRender"); throw new Error("Expected template function 'faker.date.future' to define onRender");
} }

View File

@@ -16,26 +16,26 @@ remembered for next time.
Each language supports one or more libraries: Each language supports one or more libraries:
| Language | Libraries | | Language | Libraries |
|---|---| | ----------- | ------------------------------------ |
| C | libcurl | | C | libcurl |
| Clojure | clj-http | | Clojure | clj-http |
| C# | HttpClient, RestSharp | | C# | HttpClient, RestSharp |
| Go | Native | | Go | Native |
| HTTP | HTTP/1.1 | | HTTP | HTTP/1.1 |
| Java | AsyncHttp, NetHttp, OkHttp, Unirest | | Java | AsyncHttp, NetHttp, OkHttp, Unirest |
| JavaScript | Axios, fetch, jQuery, XHR | | JavaScript | Axios, fetch, jQuery, XHR |
| Kotlin | OkHttp | | Kotlin | OkHttp |
| Node.js | Axios, fetch, HTTP, Request, Unirest | | Node.js | Axios, fetch, HTTP, Request, Unirest |
| Objective-C | NSURLSession | | Objective-C | NSURLSession |
| OCaml | CoHTTP | | OCaml | CoHTTP |
| PHP | cURL, Guzzle, HTTP v1, HTTP v2 | | PHP | cURL, Guzzle, HTTP v1, HTTP v2 |
| PowerShell | Invoke-WebRequest, RestMethod | | PowerShell | Invoke-WebRequest, RestMethod |
| Python | http.client, Requests | | Python | http.client, Requests |
| R | httr | | R | httr |
| Ruby | Native | | Ruby | Native |
| Shell | cURL, HTTPie, Wget | | Shell | cURL, HTTPie, Wget |
| Swift | URLSession | | Swift | URLSession |
## Features ## Features

View File

@@ -1,10 +1,9 @@
{ {
"name": "@yaak/httpsnippet", "name": "@yaak/httpsnippet",
"private": true,
"version": "1.0.3",
"displayName": "HTTP Snippet", "displayName": "HTTP Snippet",
"version": "1.0.3",
"private": true,
"description": "Generate code snippets for HTTP requests in various languages and frameworks", "description": "Generate code snippets for HTTP requests in various languages and frameworks",
"minYaakVersion": "2026.2.0-beta.10",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/mountain-loop/yaak.git", "url": "https://github.com/mountain-loop/yaak.git",
@@ -20,5 +19,6 @@
"devDependencies": { "devDependencies": {
"@types/node": "^22.0.0", "@types/node": "^22.0.0",
"typescript": "^5.9.3" "typescript": "^5.9.3"
} },
"minYaakVersion": "2026.2.0-beta.10"
} }

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