Compare commits

..

12 Commits

Author SHA1 Message Date
Gregory Schier
d2a3f7a669 Simplify back to ternary now that typecast is gone
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 09:28:38 -07:00
Gregory Schier
356b8ea0d6 Remove unnecessary typecast for model.name
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 09:24:50 -07:00
Gregory Schier
f283b02cfe Remove redundant resolvedModelName call for non-empty names
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 09:19:42 -07:00
Gregory Schier
df089e6f00 Use resolvedModelName for conflict detection on named requests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 09:18:17 -07:00
Gregory Schier
0979398215 Refactor ternary to if-statement for readability
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 09:15:10 -07:00
Gregory Schier
85260ab49a Fix duplicate request snapshotting URL as name
When duplicating an unnamed request, the resolved name (URL fallback)
was being set as the duplicate's explicit name. Now the raw name field
is preserved, so unnamed duplicates continue to dynamically show the URL.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 09:07:35 -07:00
Gregory Schier
b4a1c418bb Run oxfmt across repo, add format script and docs
Add .oxfmtignore to skip generated bindings and wasm-pack output.
Add npm format script, update DEVELOPMENT.md for Vite+ toolchain,
and format all non-generated files with oxfmt.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 10:15:49 -07:00
Gregory Schier
45262edfbd Migrate to Vite+ unified toolchain (#428) 2026-03-13 09:27:56 -07:00
Gregory Schier
aed7bd12ea Add react compiler 2026-03-13 06:49:14 -07:00
Gregory Schier
b5928af1d7 Bump react 2026-03-13 06:47:42 -07:00
Gregory Schier
6cc47bea38 Fixes for wasm 2026-03-13 06:46:27 -07:00
Gregory Schier
b83d9e6765 Vite 8 upgrade 2026-03-13 06:33:20 -07:00
682 changed files with 16017 additions and 14985 deletions

View File

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

View File

@@ -8,7 +8,7 @@ Generate formatted release notes for Yaak releases by analyzing git history and
## What to do
1. Identifies the version tag and previous version
2. Retrieves all commits between versions
2. Retrieves all commits between versions
- If the version is a beta version, it retrieves commits between the beta version and previous beta version
- If the version is a stable version, it retrieves commits between the stable version and the previous stable version
3. Fetches PR descriptions for linked issues to find:

View File

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

View File

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

View File

@@ -14,17 +14,20 @@ jobs:
runs-on: macos-latest
steps:
- 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: Swatinem/rust-cache@v2
with:
shared-key: ci
cache-on-failure: true
- run: npm ci
- run: vp install
- run: npm run bootstrap
- run: npm run lint
- name: Run JS Tests
run: npm test
run: vp test
- name: Run Rust Tests
run: cargo test --all

View File

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

View File

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

View File

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

1
.node-version Normal file
View File

@@ -0,0 +1 @@
24.14.0

2
.npmrc Normal file
View File

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

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,
"biome.enabled": true,
"biome.lint.format.enable": true
"editor.formatOnSaveMode": "file",
"editor.codeActionsOnSave": {
"source.fixAll.oxc": "explicit"
}
}

1
Cargo.lock generated
View File

@@ -10447,7 +10447,6 @@ dependencies = [
"hyper-util",
"log 0.4.29",
"mime_guess",
"native-tls",
"regex 1.11.1",
"reqwest",
"serde",

View File

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

View File

@@ -1,24 +1,26 @@
# Developer Setup
Yaak is a combined Node.js and Rust monorepo. It is a [Tauri](https://tauri.app) project, so
Yaak is a combined Node.js and Rust monorepo. It is a [Tauri](https://tauri.app) project, so
uses Rust and HTML/CSS/JS for the main application but there is also a plugin system powered
by a Node.js sidecar that communicates to the app over gRPC.
Because of the moving parts, there are a few setup steps required before development can
Because of the moving parts, there are a few setup steps required before development can
begin.
## Prerequisites
Make sure you have the following tools installed:
- [Node.js](https://nodejs.org/en/download/package-manager)
- [Node.js](https://nodejs.org/en/download/package-manager) (v24+)
- [Rust](https://www.rust-lang.org/tools/install)
- [Vite+](https://vite.dev/guide/vite-plus) (`vp` CLI)
Check the installations with the following commands:
```shell
node -v
npm -v
vp --version
rustc --version
```
@@ -45,12 +47,12 @@ npm start
## SQLite Migrations
New migrations can be created from the `src-tauri/` directory:
```shell
npm run migration
```
Rerun the app to apply the migrations.
Rerun the app to apply the migrations.
_Note: For safety, development builds use a separate database location from production builds._
@@ -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
```
## 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:
@@ -71,12 +73,6 @@ This repo uses Biome for linting and formatting (replacing ESLint + Prettier).
npm run lint
```
- Auto-fix lint issues where possible:
```sh
npm run lint:fix
```
- Format code:
```sh
@@ -84,5 +80,7 @@ npm run format
```
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>
<br>
<p align="center">
<!-- sponsors-premium --><a href="https://github.com/MVST-Solutions"><img src="https:&#x2F;&#x2F;github.com&#x2F;MVST-Solutions.png" width="80px" alt="User avatar: MVST-Solutions" /></a>&nbsp;&nbsp;<a href="https://github.com/dharsanb"><img src="https:&#x2F;&#x2F;github.com&#x2F;dharsanb.png" width="80px" alt="User avatar: dharsanb" /></a>&nbsp;&nbsp;<a href="https://github.com/railwayapp"><img src="https:&#x2F;&#x2F;github.com&#x2F;railwayapp.png" width="80px" alt="User avatar: railwayapp" /></a>&nbsp;&nbsp;<a href="https://github.com/caseyamcl"><img src="https:&#x2F;&#x2F;github.com&#x2F;caseyamcl.png" width="80px" alt="User avatar: caseyamcl" /></a>&nbsp;&nbsp;<a href="https://github.com/bytebase"><img src="https:&#x2F;&#x2F;github.com&#x2F;bytebase.png" width="80px" alt="User avatar: bytebase" /></a>&nbsp;&nbsp;<a href="https://github.com/"><img src="https:&#x2F;&#x2F;raw.githubusercontent.com&#x2F;JamesIves&#x2F;github-sponsors-readme-action&#x2F;dev&#x2F;.github&#x2F;assets&#x2F;placeholder.png" width="80px" alt="User avatar: " /></a>&nbsp;&nbsp;<!-- sponsors-premium -->
</p>
@@ -27,12 +25,10 @@
![Yaak API Client](https://yaak.app/static/screenshot.png)
## Features
Yaak is an offline-first API client designed to stay out of your way while giving you everything you need when you need it.
Built with [Tauri](https://tauri.app), Rust, and React, its fast, lightweight, and private. No telemetry, no VC funding, and no cloud lock-in.
Yaak is an offline-first API client designed to stay out of your way while giving you everything you need when you need it.
Built with [Tauri](https://tauri.app), Rust, and React, its fast, lightweight, and private. No telemetry, no VC funding, and no cloud lock-in.
### 🌐 Work with any API
@@ -41,21 +37,23 @@ Built with [Tauri](https://tauri.app), Rust, and React, its fast, lightweight
- Filter and inspect responses with JSONPath or XPath.
### 🔐 Stay secure
- Use OAuth 2.0, JWT, Basic Auth, or custom plugins for authentication.
- Secure sensitive values with encrypted secrets.
- Secure sensitive values with encrypted secrets.
- Store secrets in your OS keychain.
### ☁️ Organize & collaborate
- Group requests into workspaces and nested folders.
- Use environment variables to switch between dev, staging, and prod.
- Mirror workspaces to your filesystem for versioning in Git or syncing with Dropbox.
### 🧩 Extend & customize
- Insert dynamic values like UUIDs or timestamps with template tags.
- Pick from built-in themes or build your own.
- Create plugins to extend authentication, template tags, or the UI.
## Contribution Policy
> [!IMPORTANT]

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_json = { workspace = true }
sha2 = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread", "macros", "io-util", "net", "signal", "time"] }
tokio = { workspace = true, features = [
"rt-multi-thread",
"macros",
"io-util",
"net",
"signal",
"time",
] }
walkdir = "2"
webbrowser = "1"
zip = "4"

View File

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

View File

@@ -35,7 +35,16 @@ r2d2 = "0.8.10"
r2d2_sqlite = "0.25.0"
mime_guess = "2.0.5"
rand = "0.9.0"
reqwest = { workspace = true, features = ["multipart", "gzip", "brotli", "deflate", "json", "rustls-tls-manual-roots-no-provider", "socks", "http2"] }
reqwest = { workspace = true, features = [
"multipart",
"gzip",
"brotli",
"deflate",
"json",
"rustls-tls-manual-roots-no-provider",
"socks",
"http2",
] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true, features = ["raw_value"] }
tauri = { workspace = true, features = ["devtools", "protocol-asset"] }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,3 @@
[default]
description = "Default permissions for the plugin"
permissions = [
"allow-set-title",
"allow-set-theme",
]
permissions = ["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) {
return invoke<void>('cmd_enable_encryption', { workspaceId });
return invoke<void>("cmd_enable_encryption", { workspaceId });
}
export function revealWorkspaceKey(workspaceId: string) {
return invoke<string>('cmd_reveal_workspace_key', { workspaceId });
return invoke<string>("cmd_reveal_workspace_key", { workspaceId });
}
export function setWorkspaceKey(args: { workspaceId: string; key: string }) {
return invoke<void>('cmd_set_workspace_key', args);
return invoke<void>("cmd_set_workspace_key", args);
}
export function disableEncryption(workspaceId: string) {
return invoke<void>('cmd_disable_encryption', { workspaceId });
return invoke<void>("cmd_disable_encryption", { workspaceId });
}

View File

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

View File

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

View File

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

View File

@@ -18,9 +18,13 @@ zstd = "0.13"
hyper-util = { version = "0.1.17", default-features = false, features = ["client-legacy"] }
log = { workspace = true }
mime_guess = "2.0.5"
native-tls = "0.2"
regex = "1.11.1"
reqwest = { workspace = true, features = ["rustls-tls-manual-roots-no-provider", "native-tls", "socks", "http2", "stream"] }
reqwest = { workspace = true, features = [
"rustls-tls-manual-roots-no-provider",
"socks",
"http2",
"stream",
] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
thiserror = { workspace = true }

View File

@@ -6,56 +6,6 @@ use std::sync::Arc;
use yaak_models::models::DnsOverride;
use yaak_tls::{ClientCertificateConfig, get_tls_config};
/// Build a native-tls connector for maximum compatibility when certificate
/// validation is disabled. Unlike rustls, native-tls uses the OS TLS stack
/// (Secure Transport on macOS, SChannel on Windows, OpenSSL on Linux) which
/// supports TLS 1.0+ for legacy servers.
fn build_native_tls_connector(
client_cert: Option<ClientCertificateConfig>,
) -> Result<native_tls::TlsConnector> {
let mut builder = native_tls::TlsConnector::builder();
builder.danger_accept_invalid_certs(true);
builder.danger_accept_invalid_hostnames(true);
builder.min_protocol_version(Some(native_tls::Protocol::Tlsv10));
if let Some(identity) = build_native_tls_identity(client_cert)? {
builder.identity(identity);
}
Ok(builder.build()?)
}
fn build_native_tls_identity(
client_cert: Option<ClientCertificateConfig>,
) -> Result<Option<native_tls::Identity>> {
let config = match client_cert {
None => return Ok(None),
Some(c) => c,
};
// Try PFX/PKCS12 first
if let Some(pfx_path) = &config.pfx_file {
if !pfx_path.is_empty() {
let pfx_data = std::fs::read(pfx_path)?;
let password = config.passphrase.as_deref().unwrap_or("");
let identity = native_tls::Identity::from_pkcs12(&pfx_data, password)?;
return Ok(Some(identity));
}
}
// Try CRT + KEY files
if let (Some(crt_path), Some(key_path)) = (&config.crt_file, &config.key_file) {
if !crt_path.is_empty() && !key_path.is_empty() {
let crt_data = std::fs::read(crt_path)?;
let key_data = std::fs::read(key_path)?;
let identity = native_tls::Identity::from_pkcs8(&crt_data, &key_data)?;
return Ok(Some(identity));
}
}
Ok(None)
}
#[derive(Clone)]
pub struct HttpConnectionProxySettingAuth {
pub user: String,
@@ -101,17 +51,10 @@ impl HttpConnectionOptions {
// This is needed so we can emit DNS timing events for each request
.pool_max_idle_per_host(0);
// Configure TLS
if self.validate_certificates {
// Use rustls with platform certificate verification (TLS 1.2+ only)
let config = get_tls_config(true, true, self.client_certificate.clone())?;
client = client.use_preconfigured_tls(config);
} else {
// Use native TLS for maximum compatibility (supports TLS 1.0+)
let connector =
build_native_tls_connector(self.client_certificate.clone())?;
client = client.use_preconfigured_tls(connector);
}
// Configure TLS with optional client certificate
let config =
get_tls_config(self.validate_certificates, true, self.client_certificate.clone())?;
client = client.use_preconfigured_tls(config);
// Configure DNS resolver - keep a reference to configure per-request
let resolver = LocalhostResolver::new(self.dns_overrides.clone());

View File

@@ -9,12 +9,6 @@ pub enum Error {
#[error(transparent)]
TlsError(#[from] yaak_tls::error::Error),
#[error("Native TLS error: {0}")]
NativeTlsError(#[from] native_tls::Error),
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
#[error("Request failed with {0:?}")]
RequestError(String),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@yaakapp-internal/plugins",
"private": true,
"version": "1.0.0",
"private": true,
"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",
"private": true,
"version": "1.0.0",
"private": true,
"main": "index.ts"
}

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@yaakapp-internal/sync",
"private": true,
"version": "1.0.0",
"private": true,
"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') {
console.log('Skipping wasm-pack build (SKIP_WASM_BUILD=1)');
if (process.env.SKIP_WASM_BUILD === "1") {
console.log("Skipping wasm-pack build (SKIP_WASM_BUILD=1)");
return;
}
execSync('wasm-pack build --target bundler', { stdio: 'inherit' });
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';
import { Tokens } from './bindings/parser';
import { escape_template, parse_template, unescape_template } from './pkg';
export * from "./bindings/parser";
import { Tokens } from "./bindings/parser";
import { escape_template, parse_template, unescape_template } from "./pkg";
export function parseTemplate(template: string) {
return parse_template(template) as Tokens;

View File

@@ -1,7 +1,7 @@
{
"name": "@yaakapp-internal/templates",
"private": true,
"version": "1.0.0",
"private": true,
"main": "index.ts",
"scripts": {
"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";
import { __wbg_set_wasm } from "./yaak_templates_bg.js";
__wbg_set_wasm(wasm);
wasm.__wbindgen_start();
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();

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,25 +1,25 @@
{
"name": "@yaakapp/cli",
"version": "0.0.1",
"main": "./index.js",
"repository": {
"type": "git",
"url": "git+https://github.com/mountain-loop/yaak.git"
},
"scripts": {
"postinstall": "node ./install.js",
"prepublishOnly": "node ./prepublish.js"
},
"bin": {
"yaak": "bin/cli.js",
"yaakcli": "bin/cli.js"
},
"main": "./index.js",
"scripts": {
"postinstall": "node ./install.js",
"prepublishOnly": "node ./prepublish.js"
},
"optionalDependencies": {
"@yaakapp/cli-darwin-x64": "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-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-x64",
"cli-win32-arm64",
"cli-win32-x64"
"cli-win32-x64",
];
const binaries = [
{
src: join(__dirname, "dist", "cli-darwin-arm64", "yaak"),
dest: join(__dirname, "cli-darwin-arm64", "bin", "yaak")
dest: join(__dirname, "cli-darwin-arm64", "bin", "yaak"),
},
{
src: join(__dirname, "dist", "cli-darwin-x64", "yaak"),
dest: join(__dirname, "cli-darwin-x64", "bin", "yaak")
dest: join(__dirname, "cli-darwin-x64", "bin", "yaak"),
},
{
src: join(__dirname, "dist", "cli-linux-arm64", "yaak"),
dest: join(__dirname, "cli-linux-arm64", "bin", "yaak")
dest: join(__dirname, "cli-linux-arm64", "bin", "yaak"),
},
{
src: join(__dirname, "dist", "cli-linux-x64", "yaak"),
dest: join(__dirname, "cli-linux-x64", "bin", "yaak")
dest: join(__dirname, "cli-linux-x64", "bin", "yaak"),
},
{
src: join(__dirname, "dist", "cli-win32-arm64", "yaak.exe"),
dest: join(__dirname, "cli-win32-arm64", "bin", "yaak.exe")
dest: join(__dirname, "cli-win32-arm64", "bin", "yaak.exe"),
},
{
src: join(__dirname, "dist", "cli-win32-x64", "yaak.exe"),
dest: join(__dirname, "cli-win32-x64", "bin", "yaak.exe")
}
dest: join(__dirname, "cli-win32-x64", "bin", "yaak.exe"),
},
];
for (const { src, dest } of binaries) {
@@ -67,7 +67,7 @@ for (const pkg of packages) {
"@yaakapp/cli-linux-arm64": version,
"@yaakapp/cli-linux-x64": version,
"@yaakapp/cli-win32-x64": version,
"@yaakapp/cli-win32-arm64": version
"@yaakapp/cli-win32-arm64": version,
};
}

3283
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
export type * from './plugins';
export type * from './themes';
export type * from "./plugins";
export type * from "./themes";
export * from './bindings/gen_models';
export * from './bindings/gen_events';
export * from "./bindings/gen_models";
export * from "./bindings/gen_events";
// 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,
GetHttpAuthenticationSummaryResponse,
HttpAuthenticationAction,
} from '../bindings/gen_events';
import type { MaybePromise } from '../helpers';
import type { Context } from './Context';
} from "../bindings/gen_events";
import type { MaybePromise } from "../helpers";
import type { Context } from "./Context";
type AddDynamicMethod<T> = {
dynamic?: (
@@ -16,16 +16,16 @@ type AddDynamicMethod<T> = {
) => 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
? T extends { inputs?: FormInput[] }
? Omit<T, 'inputs'> & {
? Omit<T, "inputs"> & {
inputs: Array<AddDynamic<FormInput>>;
dynamic?: (
ctx: Context,
args: CallHttpAuthenticationActionArgs,
) => MaybePromise<
Partial<Omit<T, 'inputs'> & { inputs: Array<AddDynamic<FormInput>> }> | null | undefined
Partial<Omit<T, "inputs"> & { inputs: Array<AddDynamic<FormInput>> }> | null | undefined
>;
}
: T & AddDynamicMethod<T>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import type { CallTemplateFunctionArgs, FormInput, TemplateFunction } from '../bindings/gen_events';
import type { MaybePromise } from '../helpers';
import type { Context } from './Context';
import type { CallTemplateFunctionArgs, FormInput, TemplateFunction } from "../bindings/gen_events";
import type { MaybePromise } from "../helpers";
import type { Context } from "./Context";
type AddDynamicMethod<T> = {
dynamic?: (
@@ -9,16 +9,16 @@ type AddDynamicMethod<T> = {
) => 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
? T extends { inputs?: FormInput[] }
? Omit<T, 'inputs'> & {
? Omit<T, "inputs"> & {
inputs: Array<AddDynamic<FormInput>>;
dynamic?: (
ctx: Context,
args: CallTemplateFunctionArgs,
) => MaybePromise<
Partial<Omit<T, 'inputs'> & { inputs: Array<AddDynamic<FormInput>> }> | null | undefined
Partial<Omit<T, "inputs"> & { inputs: Array<AddDynamic<FormInput>> }> | null | undefined
>;
}
: T & AddDynamicMethod<T>
@@ -26,7 +26,7 @@ type AddDynamic<T> = T extends any
export type DynamicTemplateFunctionArg = AddDynamic<FormInput>;
export type TemplateFunctionPlugin = Omit<TemplateFunction, 'args'> & {
export type TemplateFunctionPlugin = Omit<TemplateFunction, "args"> & {
args: DynamicTemplateFunctionArg[];
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;

View File

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

View File

@@ -1,5 +1,5 @@
import type { CallWorkspaceActionArgs, WorkspaceAction } from '../bindings/gen_events';
import type { Context } from './Context';
import type { CallWorkspaceActionArgs, WorkspaceAction } from "../bindings/gen_events";
import type { Context } from "./Context";
export type WorkspaceActionPlugin = WorkspaceAction & {
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 { FilterPlugin } from './FilterPlugin';
import type { FolderActionPlugin } from './FolderActionPlugin';
import type { GrpcRequestActionPlugin } from './GrpcRequestActionPlugin';
import type { HttpRequestActionPlugin } from './HttpRequestActionPlugin';
import type { ImporterPlugin } from './ImporterPlugin';
import type { TemplateFunctionPlugin } from './TemplateFunctionPlugin';
import type { ThemePlugin } from './ThemePlugin';
import type { WebsocketRequestActionPlugin } from './WebsocketRequestActionPlugin';
import type { WorkspaceActionPlugin } from './WorkspaceActionPlugin';
import type { Context } from "./Context";
import type { FilterPlugin } from "./FilterPlugin";
import type { FolderActionPlugin } from "./FolderActionPlugin";
import type { GrpcRequestActionPlugin } from "./GrpcRequestActionPlugin";
import type { HttpRequestActionPlugin } from "./HttpRequestActionPlugin";
import type { ImporterPlugin } from "./ImporterPlugin";
import type { TemplateFunctionPlugin } from "./TemplateFunctionPlugin";
import type { ThemePlugin } from "./ThemePlugin";
import type { WebsocketRequestActionPlugin } from "./WebsocketRequestActionPlugin";
import type { WorkspaceActionPlugin } from "./WorkspaceActionPlugin";
export type { Context };
export type { DynamicAuthenticationArg } from './AuthenticationPlugin';
export type { CallPromptFormDynamicArgs, DynamicPromptFormArg } from './Context';
export type { DynamicTemplateFunctionArg } from './TemplateFunctionPlugin';
export type { DynamicAuthenticationArg } from "./AuthenticationPlugin";
export type { CallPromptFormDynamicArgs, DynamicPromptFormArg } from "./Context";
export type { DynamicTemplateFunctionArg } from "./TemplateFunctionPlugin";
export type { TemplateFunctionPlugin };
export type { FolderActionPlugin } from './FolderActionPlugin';
export type { WorkspaceActionPlugin } from './WorkspaceActionPlugin';
export type { FolderActionPlugin } from "./FolderActionPlugin";
export type { WorkspaceActionPlugin } from "./WorkspaceActionPlugin";
/**
* 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 {
#listeners = new Set<(event: InternalEvent) => void>();

View File

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

View File

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

View File

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

View File

@@ -1,16 +1,16 @@
import type { InternalEvent } from '@yaakapp/api';
import WebSocket from 'ws';
import { EventChannel } from './EventChannel';
import { PluginHandle } from './PluginHandle';
import type { InternalEvent } from "@yaakapp/api";
import WebSocket from "ws";
import { EventChannel } from "./EventChannel";
import { PluginHandle } from "./PluginHandle";
const port = process.env.PORT;
if (!port) {
throw new Error('Plugin runtime missing PORT');
throw new Error("Plugin runtime missing PORT");
}
const host = process.env.HOST;
if (!host) {
throw new Error('Plugin runtime missing HOST');
throw new Error("Plugin runtime missing HOST");
}
const pluginToAppEvents = new EventChannel();
@@ -18,16 +18,16 @@ const plugins: Record<string, PluginHandle> = {};
const ws = new WebSocket(`ws://${host}:${port}`);
ws.on('message', async (e: Buffer) => {
ws.on("message", async (e: Buffer) => {
try {
await handleIncoming(e.toString());
} 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('error', (err: unknown) => console.error('Plugin runtime websocket error', err));
ws.on('close', (code: number) => console.log('Plugin runtime websocket closed', code));
ws.on("open", () => console.log("Plugin runtime connected to websocket"));
ws.on("error", (err: unknown) => console.error("Plugin runtime websocket error", err));
ws.on("close", (code: number) => console.log("Plugin runtime websocket closed", code));
// Listen for incoming events from plugins
pluginToAppEvents.listen((e) => {
@@ -38,7 +38,7 @@ pluginToAppEvents.listen((e) => {
async function handleIncoming(msg: string) {
const pluginEvent: InternalEvent = JSON.parse(msg);
// Handle special event to bootstrap plugin
if (pluginEvent.payload.type === 'boot_request') {
if (pluginEvent.payload.type === "boot_request") {
const plugin = new PluginHandle(
pluginEvent.pluginRefId,
pluginEvent.context,
@@ -51,23 +51,23 @@ async function handleIncoming(msg: string) {
// Once booted, forward all events to the plugin worker
const plugin = plugins[pluginEvent.pluginRefId];
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;
}
if (pluginEvent.payload.type === 'terminate_request') {
if (pluginEvent.payload.type === "terminate_request") {
await plugin.terminate();
console.log('Terminated plugin worker', pluginEvent.pluginRefId);
console.log("Terminated plugin worker", pluginEvent.pluginRefId);
delete plugins[pluginEvent.pluginRefId];
}
plugin.sendToWorker(pluginEvent);
}
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
process.on("unhandledRejection", (reason, promise) => {
console.error("Unhandled Rejection at:", promise, "reason:", reason);
});
process.on('uncaughtException', (error) => {
console.error('Uncaught Exception:', error);
process.on("uncaughtException", (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) {
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) {
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(
f: TemplateFunctionPlugin,
): TemplateFunctionPlugin {
const migratedArgs = f.args.map((a) => {
if (a.type === 'select') {
if (a.type === "select") {
// Migrate old options that had 'name' instead of 'label'
type LegacyOption = { label?: string; value: string; name?: string };
a.options = a.options.map((o) => {
const legacy = o as LegacyOption;
return {
label: legacy.label ?? legacy.name ?? '',
label: legacy.label ?? legacy.name ?? "",
value: legacy.value,
};
});

View File

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

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