Compare commits

..

No commits in common. "develop" and "Kunkun-v0.1.27" have entirely different histories.

246 changed files with 4108 additions and 12007 deletions

View File

@ -11,9 +11,11 @@
"jarvis",
"form-view",
"@kksh/desktop",
"@kksh/supabase",
"@kksh/utils",
"@kksh/extension",
"@kksh/schema",
"@kksh/supabase",
"@kksh/ui"
]
}

View File

@ -205,15 +205,12 @@ jobs:
run: pnpm prepare
- name: Build Packages
env:
NODE_OPTIONS: --max-old-space-size=4096
run: pnpm build
- name: Build the App
working-directory: apps/desktop
env:
CI: false
NODE_OPTIONS: --max-old-space-size=4096
run: pnpm tauri build ${{ env.BUILD_MODE}} ${{ matrix.os == 'windows-latest' && '-b nsis' || '' }}
- name: Rename macos-aarch64

View File

@ -52,8 +52,6 @@ jobs:
- name: Setup
run: pnpm prepare
- name: Build
env:
NODE_OPTIONS: --max-old-space-size=4096
run: pnpm build
- name: JS Test
if: matrix.os == 'ubuntu-24.04'

View File

@ -87,8 +87,6 @@ jobs:
# pnpm --filter=@kksh/ci run ci-env-check
bun packages/ci/scripts/ci-env-check.ts
- name: Build Packages
env:
NODE_OPTIONS: --max-old-space-size=4096
run: pnpm build
- name: Get App Version
if: matrix.settings.platform == 'windows-latest'
@ -100,7 +98,6 @@ jobs:
CI: false
KUNKUN_PUBLISH: true
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NODE_OPTIONS: --max-old-space-size=4096
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}

View File

@ -1,121 +0,0 @@
name: "Desktop Test build"
on:
push:
branches:
- "test-build"
tags:
- "v*"
workflow_dispatch:
jobs:
publish-tauri:
permissions:
contents: write
strategy:
fail-fast: false
matrix:
settings:
- platform: "macos-14" # for Arm based macs (M1 and above).
args: "--target aarch64-apple-darwin --verbose --config src-tauri/tauri.conf.publish.json"
- platform: "macos-13" # for Intel based macs.
args: "--target x86_64-apple-darwin --verbose --config src-tauri/tauri.conf.publish.json"
# Universal Build no longer supported after adding openssl, which is not cross-compilable.
- platform: "macos-14" # for Both Arm and Intel based macs.
args: "--target universal-apple-darwin --verbose --config src-tauri/tauri.conf.publish.json"
- platform: "ubuntu-22.04" # for Tauri v1 you could replace this with ubuntu-20.04.
args: "--verbose --config src-tauri/tauri.conf.publish.json"
- platform: "windows-latest"
args: "--verbose --config src-tauri/tauri.conf.publish.json"
runs-on: ${{ matrix.settings.platform }}
steps:
- uses: actions/checkout@v4
with:
submodules: "true"
- name: Install Dependencies (ubuntu only)
if: matrix.settings.platform == 'ubuntu-22.04' # This must match the platform value defined above.
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libxdo-dev
# You can remove the one that doesn't apply to your app to speed up the workflow a bit.
- name: Install protobuf (Mac)
if: startsWith(matrix.settings.platform, 'macos')
run: |
brew install protobuf
brew install openssl
- name: Install Protobuf (Ubuntu)
if: matrix.settings.platform == 'ubuntu-22.04'
run: |
sudo apt install -y protobuf-compiler
- uses: pnpm/action-setup@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: "pnpm" # Set this to npm, yarn or pnpm.
cache-dependency-path: ./pnpm-lock.yaml
- name: Install protoc and openssl for windows
if: matrix.settings.platform == 'windows-latest'
run: |
choco install protoc
choco install openssl
echo OPENSSL_DIR='C:\Program Files\OpenSSL' >> $env:GITHUB_ENV
echo OPENSSL_INCLUDE_DIR='C:\Program Files\OpenSSL\include' >> $env:GITHUB_ENV
echo OPENSSL_LIB_DIR='C:\Program Files\OpenSSL\lib\VC\x64\MDd' >> $env:GITHUB_ENV
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
# Those targets are only used on macos runners so it's in an `if` to slightly speed up windows and linux builds.
targets: ${{ matrix.settings.platform == 'macos-14' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
- name: Add rust target (macos only)
if: matrix.settings.platform == 'macos-14'
run: |
rustup target add aarch64-apple-darwin
rustup target add x86_64-apple-darwin
- name: Rust Cache
uses: swatinem/rust-cache@v2
with:
workspaces: ". -> target"
- uses: oven-sh/setup-bun@v1
with:
bun-version: latest
- name: Install Dependencies
run: pnpm install
- name: Environment Check
run: |
# pnpm --filter=@kksh/ci run ci-env-check
bun packages/ci/scripts/ci-env-check.ts
- name: Build Packages
env:
NODE_OPTIONS: --max-old-space-size=4096
run: pnpm build
- name: Get App Version
if: matrix.settings.platform == 'windows-latest'
id: appversion
run: |
echo "version=$(node -p "require('./apps/desktop/package.json').version")" >> $env:GITHUB_OUTPUT
- uses: tauri-apps/tauri-action@v0
env:
CI: false
KUNKUN_PUBLISH: true
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NODE_OPTIONS: --max-old-space-size=4096
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
with:
tagName: Kunkun-v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version.
releaseName: "Kunkun v__VERSION__"
releaseBody: "See the assets to download this version and install."
releaseDraft: false
prerelease: false
args: ${{ matrix.settings.args }} ${{ contains(steps.appversion.outputs.version, 'beta') && matrix.settings.platform == 'windows-latest' && '-b nsis' || '' }}
projectPath: "./apps/desktop"

2
.gitmodules vendored
View File

@ -12,4 +12,4 @@
url = https://github.com/kunkunsh/tauri-plugin-user-input.git
[submodule "vendors/tauri-plugin-keyring"]
path = vendors/tauri-plugin-keyring
url = https://github.com/HuakunShen/tauri-plugin-keyring.git
url = https://github.com/HuakunShen/tauri-plugin-keyring.git

View File

@ -1,5 +1,4 @@
.svelte-kit/
target/
vendors/**
vendors
.nuxt/

View File

@ -10,10 +10,5 @@
"titleBar.activeForeground": "#FFFBFC"
},
"svelte.enable-ts-plugin": true,
"deno.enable": false,
"search.exclude": {
"**/node_modules": true,
"**/bower_components": true,
"**/*.code-search": true
}
"deno.enable": false
}

View File

@ -34,7 +34,7 @@ If you are interested in contributing to the project, please read the following
```bash
git clone https://github.com/kunkunsh/kunkun.git --recursive
pnpm install
pnpm build # build submodules
pnpm prepare
```
### Run Desktop App

704
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -20,8 +20,7 @@ tokio-util = "0.7.12"
mdns-sd = "0.11.1"
tauri-plugin-network = { path = "./vendors/tauri-plugin-network" }
tauri-plugin-keyring = { path = "./vendors/tauri-plugin-keyring" }
tauri-plugin-shellx = { version = "2.0.16" }
tauri-plugin-clipboard = "2.1.11"
tauri-plugin-clipboard = "2.1.8"
mac-security-rs = { path = "./packages/mac-security-rs" }
log = "0.4.22"
strum = "0.26"

View File

@ -1,19 +1,5 @@
# kksh
## 0.1.3
### Patch Changes
- Updated dependencies
- @kksh/api@0.1.5
## 0.1.2
### Patch Changes
- Updated dependencies
- @kksh/api@0.1.4
## 0.1.1
### Patch Changes

View File

@ -1,7 +1,7 @@
{
"name": "kksh",
"module": "dist/cli.js",
"version": "0.1.3",
"version": "0.1.1",
"type": "module",
"bin": {
"kksh": "./dist/cli.js",
@ -31,7 +31,7 @@
"debug": "^4.4.0",
"fs-extra": "^11.2.0",
"inquirer": "^10.1.2",
"valibot": "^1.0.0"
"valibot": "^1.0.0-beta.10"
},
"files": [
"dist"

View File

@ -1,19 +1,5 @@
# create-kunkun
## 0.1.49
### Patch Changes
- Updated dependencies
- @kksh/api@0.1.5
## 0.1.48
### Patch Changes
- Updated dependencies
- @kksh/api@0.1.4
## 0.1.45
### Patch Changes

View File

@ -1,7 +1,7 @@
{
"name": "create-kunkun",
"type": "module",
"version": "0.1.49",
"version": "0.1.47",
"bin": {
"create-kunkun": "dist/index.mjs"
},
@ -27,7 +27,7 @@
"commander": "^12.1.0",
"fs-extra": "^11.2.0",
"handlebars": "^4.7.8",
"valibot": "^1.0.0"
"valibot": "^1.0.0-beta.10"
},
"files": [
"dist"

View File

@ -1,13 +0,0 @@
import { IconType } from "@kksh/api/models"
import { getExtensionsLatestPublishByIdentifier } from "@kksh/sdk"
const latestPublish = await getExtensionsLatestPublishByIdentifier({
path: {
identifier: "RAG1"
}
})
console.log(latestPublish)
// latestPublish
// console.log(typeof IconEnum.Iconify)
console.log(IconType.options)

View File

@ -1,91 +0,0 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"app_name": "KunKun",
"secondary_app_name": "KunKun",
"common_edit": "Bearbeiten",
"common_clear": "Löschen",
"common_check": "Prüfen",
"common_install": "Installieren",
"home_command_input_placeholder": "Suchen…",
"home_command_input_dropdown_quit": "Beenden",
"home_command_input_dropdown_developer_title": "Entwickler",
"home_command_input_dropdown_close_window": "Fenster schließen",
"home_command_input_dropdown_toggle_devtools": "Entwicklertools umschalten",
"home_command_input_dropdown_reload_window": "Fenster neu laden",
"home_command_input_dropdown_open_preference": "Einstellungen öffnen",
"home_command_input_dropdown_toggle_dev_extension_hmr": "Entwicklererweiterungen HMR umschalten",
"command_group_heading_dev_ext": "Entwicklererweiterungen",
"command_group_heading_ext": "Erweiterungen",
"command_group_heading_quick_links": "Quick Links",
"settings_menu_settings": "Einstellungen",
"settings_menu_general": "Allgemein",
"settings_menu_app_search_paths": "Verzeichnisse für Programme",
"settings_menu_developer": "Entwickler",
"settings_menu_extensions": "Erweiterungen",
"settings_menu_set_dev_ext": "Dev-Erweiterung festlegen",
"settings_menu_add_dev_ext": "Dev-Erweiterung hinzufügen",
"settings_menu_about": "Über",
"settings_general_launch_at_login": "Beim Systemstart öffnen",
"settings_general_hotkey": "Tastenkombination",
"settings_general_menu_bar_icon": "Menüleiste-Symbol",
"settings_general_hide_on_blur": "Automatisch ausblenden",
"settings_general_extension_auto_upgrade": "Erweiterungen automatisch aktualisieren",
"settings_general_dev_extension_hmr": "Entwicklererweiterungen HMR",
"settings_general_join_beta_updates": "Beta-Updates nutzen",
"settings_general_developer_mode": "Entwickler-Modus",
"settings_general_language": "Sprache",
"settings_general_loading_animation": "Ladeanimation",
"settings_app_search_paths_title": "Zusätzliche Verzeichnisse für die Programm-Suche",
"settings_app_search_paths_add_app_search_path": "Verzeichnis für Programm-Suche hinzufügen",
"settings_app_search_paths_table_col_search_path": "Suchpfad",
"settings_app_search_paths_table_col_depth": "Tiefe",
"settings_app_search_paths_table_col_actions": "Aktionen",
"settings_about_version": "Version",
"settings_about_author": "Autor",
"settings_about_source_code": "Quellcode",
"settings_about_extensions_source_code": "Quellcode für Erweiterungen",
"settings_about_check_for_updates": "Nach Updates suchen",
"settings_set_dev_ext_title": "Verzeichnis der Entwicklererweiterungen",
"settings_set_dev_ext_description": "Hier werden Entwicklererweiterungen installiert.",
"settings_set_dev_ext_enter_path": "Verzeichnis eingeben",
"settings_extensions_title": "Deine Erweiterungen",
"settings_extensions_table_col_name": "Name",
"settings_extensions_table_col_identifier": "Identifikator",
"settings_extensions_table_col_type": "Typ",
"settings_extensions_table_col_version": "Version",
"settings_extensions_table_col_uninstall": "Deinstallieren",
"settings_add_dev_ext_title": "Entwicklererweiterung hinzufügen",
"settings_add_dev_ext_description": "Es gibt vier Möglichkeiten, eine Erweiterung als Entwicklererweiterung zu installieren. Tarball-Archiv, lokales Verzeichnis, URL zu Tarball-Archiv oder NPM-Paketnamen.",
"settings_add_dev_ext_install_from_ext_folders": "Verzeichnis",
"settings_add_dev_ext_install_from_ext_files": "Tarball-Archiv",
"settings_add_dev_ext_drag_and_drop": "Drag and Drop",
"settings_add_dev_ext_drag_and_drop_strike": "Drag and Drop",
"settings_add_dev_ext_drag_and_drop2": "Verzeichnis oder Tarball-Archiv",
"settings_add_dev_ext_install_tarball_from_url": "Tarball-Archiv aus URL installieren",
"troubleshooters_sidebar_title": "Fehlerbehebung",
"troubleshooters_sidebar_extension_loading_title": "Ladevorgang",
"troubleshooters_sidebar_extension_window_title": "Darstellung",
"troubleshooters_sidebar_mdns_debugger_title": "MDNS-Debugger",
"troubleshooters_extension_window_title": "Fehlerbehebung für die Darstellung von Erweiterungen",
"troubleshooters_extension_window_refresh_every_second": "Jede Sekunde neu laden",
"troubleshooters_extension_window_refresh": "Neu laden",
"troubleshooters_extension_window_refreshed": "{count}x neu geladen",
"troubleshooters_extension_loading_title": "Fehlerbehebung für den Ladevorgang von Erweiterungen",
"troubleshooters_extension_loading_table_col_identifier": "Identifikator",
"troubleshooters_extension_loading_table_col_path": "Verzeichnis",
"troubleshooters_extension_loading_table_col_error": "Fehler"
}

View File

@ -24,7 +24,6 @@
"settings_menu_settings": "Settings",
"settings_menu_general": "General",
"settings_menu_app_search_paths": "App Search Paths",
"settings_menu_developer": "Developer",
"settings_menu_extensions": "Extensions",
"settings_menu_set_dev_ext": "Set Dev Extension",
@ -40,13 +39,6 @@
"settings_general_join_beta_updates": "Join Beta Updates",
"settings_general_developer_mode": "Developer Mode",
"settings_general_language": "Language",
"settings_general_loading_animation": "Loading Animation",
"settings_app_search_paths_title": "Extra App Search Paths",
"settings_app_search_paths_add_app_search_path": "Add App Search Path",
"settings_app_search_paths_table_col_search_path": "Search Path",
"settings_app_search_paths_table_col_depth": "Depth",
"settings_app_search_paths_table_col_actions": "Actions",
"settings_about_version": "Version",
"settings_about_author": "Author",

View File

@ -39,7 +39,6 @@
"settings_general_join_beta_updates": "Participar das Atualizações Beta",
"settings_general_developer_mode": "Modo Desenvolvedor",
"settings_general_language": "Idioma",
"settings_general_loading_animation": "Animação de Carregamento",
"settings_about_version": "Versão",
"settings_about_author": "Autor",

View File

@ -39,7 +39,6 @@
"settings_general_join_beta_updates": "Получать бета-обновления",
"settings_general_developer_mode": "Режим разработчика",
"settings_general_language": "Язык",
"settings_general_loading_animation": "Анимация загрузки",
"settings_about_version": "Версия",
"settings_about_author": "Автор",

View File

@ -39,7 +39,6 @@
"settings_general_join_beta_updates": "Cài đặt cập nhật thử nghiệm (beta)",
"settings_general_developer_mode": "Chế độ nhà phát triển",
"settings_general_language": "Ngôn ngữ",
"settings_general_loading_animation": "Hình ảnh tải",
"settings_about_version": "Phiên bản",
"settings_about_author": "Tác giả",

View File

@ -24,7 +24,6 @@
"settings_menu_settings": "设置",
"settings_menu_general": "通用",
"settings_menu_app_search_paths": "应用搜索路径",
"settings_menu_developer": "开发者",
"settings_menu_extensions": "插件",
"settings_menu_set_dev_ext": "设置开发插件",
@ -40,13 +39,6 @@
"settings_general_join_beta_updates": "加入 Beta 更新",
"settings_general_developer_mode": "开发者模式",
"settings_general_language": "语言",
"settings_general_loading_animation": "加载动画",
"settings_app_search_paths_title": "额外应用搜索路径",
"settings_app_search_paths_add_app_search_path": "添加应用搜索路径",
"settings_app_search_paths_table_col_search_path": "搜索路径",
"settings_app_search_paths_table_col_depth": "深度",
"settings_app_search_paths_table_col_actions": "操作",
"settings_about_version": "版本",
"settings_about_author": "作者",

View File

@ -1,6 +1,6 @@
{
"name": "@kksh/desktop",
"version": "0.1.37",
"version": "0.1.27",
"description": "",
"type": "module",
"scripts": {
@ -16,55 +16,47 @@
"license": "MIT",
"dependencies": {
"@formkit/auto-animate": "^0.8.2",
"@inlang/paraglide-sveltekit": "0.16.0",
"@kksh/drizzle": "workspace:*",
"@inlang/paraglide-sveltekit": "0.15.5",
"@kksh/extension": "workspace:*",
"@kksh/supabase": "workspace:*",
"@kksh/svelte5": "^0.1.15",
"@kksh/ui": "workspace:*",
"@kksh/utils": "workspace:*",
"@std/semver": "npm:@jsr/std__semver@^1.0.4",
"@supabase/supabase-js": "^2.49.1",
"@tanstack/table-core": "^8.21.2",
"@tauri-apps/api": "^2.3.0",
"@std/semver": "npm:@jsr/std__semver@^1.0.3",
"@supabase/supabase-js": "^2.48.0",
"@tanstack/table-core": "^8.20.5",
"@tauri-apps/api": "^2.1.1",
"@tauri-apps/plugin-autostart": "^2.2.0",
"@tauri-apps/plugin-shell": "^2.2.0",
"@tauri-apps/plugin-sql": "^2.2.0",
"@tauri-apps/plugin-stronghold": "^2.2.0",
"@tauri-store/svelte": "^2.1.1",
"dompurify": "^3.2.4",
"drizzle-orm": "^0.41.0",
"eslint": "^9.21.0",
"fuse.js": "^7.1.0",
"gsap": "^3.12.7",
"kkrpc": "^0.2.2",
"dompurify": "^3.2.3",
"gsap": "^3.12.5",
"kkrpc": "^0.1.1",
"lz-string": "^1.5.0",
"pretty-bytes": "^6.1.1",
"semver": "^7.7.1",
"svelte-inspect-value": "^0.5.0",
"semver": "^7.6.3",
"svelte-sonner": "^0.3.28",
"sveltekit-superforms": "^2.23.1",
"sveltekit-superforms": "^2.22.1",
"tauri-plugin-clipboard-api": "^2.1.11",
"tauri-plugin-shellx-api": "^2.0.16",
"tauri-plugin-svelte": "1.2.1",
"tauri-plugin-user-input-api": "workspace:*",
"uuid": "^11.1.0"
"uuid": "^11.0.3"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
"@eslint/js": "^9.19.0",
"@inlang/paraglide-js": "1.11.8",
"@kksh/types": "workspace:*",
"@sveltejs/adapter-static": "^3.0.8",
"@sveltejs/kit": "^2.17.3",
"@sveltejs/kit": "^2.17.1",
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"@tailwindcss/aspect-ratio": "^0.4.2",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/typography": "^0.5.16",
"@tauri-apps/cli": "^2.3.1",
"@tauri-apps/cli": "^2.2.7",
"@types/bun": "latest",
"@types/semver": "^7.5.8",
"@typescript-eslint/eslint-plugin": "^8.25.0",
"@typescript-eslint/parser": "^8.25.0",
"@typescript-eslint/eslint-plugin": "^8.23.0",
"@typescript-eslint/parser": "^8.23.0",
"autoprefixer": "^10.4.20",
"bits-ui": "1.0.0-next.86",
"clsx": "^2.1.1",
@ -72,15 +64,15 @@
"eslint-plugin-svelte": "^2.46.1",
"globals": "^15.14.0",
"lucide-svelte": "^0.474.0",
"prettier": "^3.5.2",
"prettier": "^3.4.2",
"svelte-radix": "^2.0.1",
"tailwind-merge": "^2.6.0",
"tailwind-variants": "^0.3.1",
"tailwind-merge": "^2.5.5",
"tailwind-variants": "^0.3.0",
"tailwindcss": "^3.4.17",
"tailwindcss-animate": "^1.0.7",
"tslib": "^2.8.1",
"typescript": "^5.6.3",
"typescript-eslint": "^8.25.0",
"vite": "^6.2.0"
"typescript-eslint": "^8.20.0",
"vite": "^6.0.3"
}
}

View File

@ -1,7 +1,7 @@
{
"$schema": "https://inlang.com/schema/project-settings",
"sourceLanguageTag": "en",
"languageTags": ["en", "zh", "ru", "pt", "vi", "de"],
"languageTags": ["en", "zh", "ru", "pt", "vi"],
"modules": [
"https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-empty-pattern@latest/dist/index.js",
"https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-missing-translation@latest/dist/index.js",

View File

@ -34,7 +34,7 @@ chrono = { workspace = true }
log = { workspace = true }
urlencoding = "2.1.3"
tauri-plugin-process = "2.2.0"
tauri-plugin-shellx = { workspace = true }
tauri-plugin-shellx = "2.0.12"
tauri-plugin-fs = { version = "2.2.0", features = ["watch"] }
tauri-plugin-dialog = "2.2.0"
tauri-plugin-notification = "2.2.1"
@ -58,7 +58,6 @@ uuid = "1.14.0"
obfstr = { workspace = true }
base64 = { workspace = true }
tauri-plugin-stronghold = "2.2.0"
tauri-plugin-sql = "2"
[target."cfg(target_os = \"macos\")".dependencies]
@ -66,10 +65,10 @@ cocoa = "0.24.1"
mac-security-rs = { workspace = true }
objc = "0.2.7"
[target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies]
tauri-plugin-autostart = "2"
tauri-plugin-cli = "2"
tauri-plugin-global-shortcut = "2.0.1"
tauri-plugin-single-instance = { version = "2", features = ["deep-link"] }
tauri-plugin-updater = "2.0.2"
tauri-plugin-svelte = "2.1.1"

View File

@ -24,7 +24,6 @@
"core:event:default",
"core:window:default",
"core:window:allow-set-size",
"core:window:allow-set-enabled",
"core:window:allow-start-dragging",
"core:window:allow-set-focus",
"core:window:allow-toggle-maximize",
@ -61,7 +60,6 @@
"shellx:allow-execute",
"shellx:allow-open",
"shellx:allow-kill",
"shellx:allow-kill-pid",
"shellx:allow-spawn",
"shellx:allow-stdin-write",
"shellx:allow-fix-path-env",

View File

@ -1,5 +0,0 @@
{
"identifier": "svelte",
"windows": ["*"],
"permissions": ["svelte:default", "core:event:default"]
}

View File

@ -27,7 +27,7 @@ use utils::server::tauri_file_server;
pub fn run() {
let context = tauri::generate_context!();
let mut builder = tauri::Builder::default();
// let app_data_path = tauri::path::PathResolver::app_data_dir().unwrap();
// let db_key = if cfg!(debug_assertions) {
// None
// } else {
@ -108,16 +108,10 @@ pub fn run() {
.build(),
)
.plugin(tauri_plugin_cli::init())
.plugin(
tauri_plugin_sql::Builder::default()
// .add_migrations("sqlite:mydatabase.db", migrations)
.build(),
)
.plugin(tauri_plugin_user_input::init())
.plugin(tauri_plugin_deep_link::init())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_svelte::init())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_autostart::init(
MacosLauncher::LaunchAgent,

View File

@ -5,7 +5,7 @@
"identifier": "sh.kunkun.desktop",
"build": {
"beforeDevCommand": "pnpm dev",
"devUrl": "http://localhost:1566",
"devUrl": "http://localhost:1420",
"beforeBuildCommand": "pnpm build",
"frontendDist": "../build"
},
@ -20,7 +20,6 @@
"url": "/app",
"title": "Kunkun",
"width": 800,
"label": "main",
"visible": false,
"height": 600,
"decorations": true,

View File

@ -11,7 +11,6 @@ import { WebviewWindow } from "@tauri-apps/api/webviewWindow"
import { exit } from "@tauri-apps/plugin-process"
import { dev } from "$app/environment"
import { goto } from "$app/navigation"
import Fuse from "fuse.js"
import { toast } from "svelte-sonner"
import { derived } from "svelte/store"
import * as clipboard from "tauri-plugin-clipboard-api"
@ -242,23 +241,6 @@ export const rawBuiltinCmds: BuiltinCmd[] = [
},
keywords: ["extension", "troubleshooter"]
},
{
name: "ORM Troubleshooter",
icon: {
type: IconEnum.Iconify,
value: "material-symbols:database"
},
description: "",
flags: {
developer: true,
dev: true
},
function: async () => {
appState.clearSearchTerm()
goto(i18n.resolveRoute("/app/troubleshooters/orm"))
},
keywords: ["extension", "troubleshooter", "database", "orm"]
},
{
name: "Create Quicklink",
icon: {
@ -428,7 +410,7 @@ export const rawBuiltinCmds: BuiltinCmd[] = [
visible: false
})
setTimeout(() => {
window.show().then(() => window.setFocus())
window.show()
}, 2_000)
}
},
@ -493,19 +475,11 @@ export const rawBuiltinCmds: BuiltinCmd[] = [
}
].map((cmd) => ({ ...cmd, id: uuidv4() }))
export const fuse = new Fuse<BuiltinCmd>(rawBuiltinCmds, {
includeScore: true,
threshold: 0.2,
keys: ["name"]
})
export const builtinCmds = derived([appConfig, appState], ([$appConfig, $appState]) => {
return $appState.searchTerm
? fuse
.search($appState.searchTerm)
.map((result) => result.item)
.filter(
(cmd) => (!cmd.flags?.developer || $appConfig.developerMode) && (!cmd.flags?.dev || dev)
)
: rawBuiltinCmds
return rawBuiltinCmds.filter((cmd) => {
const passDeveloper = cmd.flags?.developer ? $appConfig.developerMode : true
const passDev = cmd.flags?.dev ? dev : true
return passDeveloper && passDev
})
// .filter((cmd) => commandScore(cmd.name, $appState.searchTerm, cmd.keywords) > 0.5)
})

View File

@ -2,23 +2,17 @@ import { i18n } from "@/i18n"
import { appState } from "@/stores"
import { winExtMap } from "@/stores/winExtMap"
import { helperAPI } from "@/utils/helper"
import { paste } from "@/utils/hotkey"
import { decideKkrpcSerialization } from "@/utils/kkrpc"
import { sleep } from "@/utils/time"
import { trimSlash } from "@/utils/url"
import { constructExtensionSupportDir } from "@kksh/api"
import { spawnExtensionFileServer } from "@kksh/api/commands"
import { db, spawnExtensionFileServer } from "@kksh/api/commands"
import type { HeadlessCommand } from "@kksh/api/headless"
import { CustomUiCmd, ExtPackageJsonExtra, HeadlessCmd, TemplateUiCmd } from "@kksh/api/models"
import { constructJarvisServerAPIWithPermissions, type IApp } from "@kksh/api/ui"
import { db } from "@kksh/drizzle"
import { launchNewExtWindow, loadExtensionManifestFromDisk } from "@kksh/extension"
import type { IKunkunFullServerAPI } from "@kunkunapi/src/api/server"
import { convertFileSrc } from "@tauri-apps/api/core"
import * as path from "@tauri-apps/api/path"
import { getCurrentWindow } from "@tauri-apps/api/window"
import * as fs from "@tauri-apps/plugin-fs"
import { info } from "@tauri-apps/plugin-log"
import { platform } from "@tauri-apps/plugin-os"
import { goto } from "$app/navigation"
import { RPCChannel, WorkerParentIO } from "kkrpc/browser"
@ -26,12 +20,10 @@ import * as v from "valibot"
export const KunkunIframeExtParams = v.object({
url: v.string(),
cmdName: v.optional(v.string()),
extPath: v.string()
})
export type KunkunIframeExtParams = v.InferOutput<typeof KunkunIframeExtParams>
export const KunkunTemplateExtParams = v.object({
url: v.optional(v.string()),
extPath: v.string(),
cmdName: v.string()
})
@ -44,10 +36,10 @@ export async function createExtSupportDir(extPath: string) {
}
}
function setTemplateExtParams(extPath: string, cmdName: string, url?: string) {
function setTemplateExtParams(extPath: string, cmdName: string) {
localStorage.setItem(
"kunkun-template-ext-params",
JSON.stringify({ extPath, cmdName, url } satisfies KunkunTemplateExtParams)
JSON.stringify({ extPath, cmdName } satisfies KunkunTemplateExtParams)
)
}
@ -58,15 +50,13 @@ export async function onTemplateUiCmdSelect(
) {
await createExtSupportDir(ext.extPath)
const url = `/app/extension/ui-worker?extPath=${encodeURIComponent(ext.extPath)}&cmdName=${encodeURIComponent(cmd.name)}`
setTemplateExtParams(ext.extPath, cmd.name, url)
setTemplateExtParams(ext.extPath, cmd.name)
if (cmd.window) {
const winLabel = await winExtMap.registerExtensionWithWindow({ extPath: ext.extPath })
const paramsStr = JSON.stringify({
url,
extPath: ext.extPath,
cmdName: cmd.name
} satisfies KunkunIframeExtParams)
localStorage.setItem("kunkun-template-ext-params", paramsStr)
localStorage.setItem(
"kunkun-template-ext-params",
JSON.stringify({ url, extPath: ext.extPath } satisfies KunkunIframeExtParams)
)
const window = launchNewExtWindow(winLabel, url, cmd.window)
window.onCloseRequested(async (event) => {
await winExtMap.unregisterExtensionFromWindow(winLabel)
@ -88,7 +78,6 @@ export async function onHeadlessCmdSelect(
const loadedExt = await loadExtensionManifestFromDisk(
await path.join(ext.extPath, "package.json")
)
const scriptPath = await path.join(loadedExt.extPath, cmd.main)
const workerScript = await fs.readTextFile(scriptPath)
const blob = new Blob([workerScript], { type: "application/javascript" })
@ -100,21 +89,7 @@ export async function onHeadlessCmdSelect(
}
const serverAPI: IKunkunFullServerAPI = constructJarvisServerAPIWithPermissions(
loadedExt.kunkun.permissions,
loadedExt.extPath,
{
recordSpawnedProcess: async (pid: number) => {
console.log("recordSpawnedProcess pid", pid)
},
getSpawnedProcesses: async () => {
console.log("getSpawnedProcesses")
return []
},
paste: async () => {
await getCurrentWindow().hide()
await sleep(200)
return paste()
}
}
loadedExt.extPath
)
const serverAPI2 = {
...serverAPI,
@ -128,15 +103,8 @@ export async function onHeadlessCmdSelect(
} satisfies IApp
}
const io = new WorkerParentIO(worker)
const kkrpcSerialization = decideKkrpcSerialization(loadedExt)
info(
`Establishing kkrpc connection for ${loadedExt.kunkun.identifier} with serialization: ${kkrpcSerialization}`
)
const rpc = new RPCChannel<typeof serverAPI2, HeadlessCommand>(io, {
expose: serverAPI2,
serialization: {
version: kkrpcSerialization
}
expose: serverAPI2
})
const workerAPI = rpc.getAPI()
await workerAPI.load()

View File

@ -12,7 +12,7 @@ import { onQuickLinkSelect } from "./quick-links"
const onExtCmdSelect: OnExtCmdSelect = (
ext: ExtPackageJsonExtra,
cmd: CustomUiCmd | TemplateUiCmd | HeadlessCmd,
cmd: CustomUiCmd | TemplateUiCmd,
{ isDev, hmr }: { isDev: boolean; hmr: boolean }
) => {
switch (cmd.type) {

View File

@ -1,23 +1,10 @@
import { getSystemCommands } from "@kksh/api/commands"
import type { SysCommand } from "@kksh/api/models"
import { commandScore } from "@kksh/ui/utils"
import Fuse from "fuse.js"
import { derived, readable } from "svelte/store"
import { appState } from "../stores/appState"
export const systemCommands = getSystemCommands()
export const fuse = new Fuse<SysCommand>(systemCommands, {
includeScore: true,
threshold: 0.2,
keys: ["name"]
})
export const systemCommandsFiltered = derived(appState, ($appState) => {
return $appState.searchTerm
? fuse.search($appState.searchTerm).map((result) => result.item)
: systemCommands
})
export const systemCommands = readable(getSystemCommands())
// export const systemCommandsFiltered = derived(
// [systemCommands, appState],

View File

@ -1,37 +0,0 @@
<script lang="ts">
import { appConfig, appState } from "@/stores"
import { cn } from "@/utils"
import { Button } from "@kksh/svelte5"
import { BorderBeam, Constants, Layouts, TauriLink } from "@kksh/ui"
import { goto } from "$app/navigation"
import { ArrowLeftIcon, LoaderCircleIcon } from "lucide-svelte"
import Dance from "../dance/dance.svelte"
let { class: className }: { class?: string } = $props()
function goHome() {
appState.setFullScreenLoading(false)
goto("/app")
}
</script>
<Layouts.Center class={cn("flex h-screen flex-col items-center justify-center", className)}>
<Button
variant="outline"
size="icon"
onclick={goHome}
class={cn(Constants.CLASSNAMES.BACK_BUTTON, "absolute left-4 top-4")}
data-flip-id={Constants.CLASSNAMES.BACK_BUTTON}
>
<ArrowLeftIcon class="size-4" />
</Button>
{#if $appConfig.loadingAnimation === "kunkun-dancing"}
<!-- <DanceTransition delay={300} autoHide={false} show={!uiControl.iframeLoaded} /> -->
<Dance class="absolute z-50 h-screen opacity-20" />
{:else}
<!-- <LoadingAnimation delay={300} autoHide={false} show={!uiControl.iframeLoaded} /> -->
<LoaderCircleIcon class="h-24 w-24 animate-spin" />
<span class="font-mono">Loading</span>
{/if}
<BorderBeam size={150} duration={12} />
</Layouts.Center>

View File

@ -75,7 +75,7 @@
<div class={cn("flex items-center gap-2", className)}>
<Shiki class={cn("w-full overflow-x-scroll rounded-md p-1 px-2")} {code} {lang} />
<Button class="" size="sm" variant="secondary" onclick={copy}>Copy</Button>
<!-- <Button class="" size="sm" variant="secondary" onclick={autoInstall} disabled={!autoInstallable}>
<Button class="" size="sm" variant="secondary" onclick={autoInstall} disabled={!autoInstallable}>
Auto Install
</Button> -->
</Button>
</div>

View File

@ -1,8 +1,8 @@
<script lang="ts">
import { setAppConfigContext } from "@/context"
import { setAppStateContext } from "@/context/appState"
import type { AppConfigState, AppState } from "@kksh/types"
import { type Snippet } from "svelte"
import type { AppConfig, AppState } from "@kksh/types"
import type { Snippet } from "svelte"
import type { Writable } from "svelte/store"
const {
@ -10,7 +10,7 @@
appState,
children
}: {
appConfig: Writable<AppConfigState>
appConfig: Writable<AppConfig>
appState: Writable<AppState>
children: Snippet<[]>
} = $props()

View File

@ -1,11 +1,9 @@
<script lang="ts">
import { appState } from "@/stores"
import { IconEnum, type AppInfo } from "@kksh/api/models"
import { Command } from "@kksh/svelte5"
import { IconMultiplexer } from "@kksh/ui"
import { DraggableCommandGroup } from "@kksh/ui/custom"
import { convertFileSrc } from "@tauri-apps/api/core"
import { getCurrentWindow } from "@tauri-apps/api/window"
import * as os from "@tauri-apps/plugin-os"
import { toast } from "svelte-sonner"
import { executeBashScript, open } from "tauri-plugin-shellx-api"
@ -15,11 +13,10 @@
</script>
<DraggableCommandGroup heading="Apps">
{#each apps.filter((app) => app.name) as app, idx}
{@const iconPath = platform === "windows" ? (app.icon_path ?? app.app_path_exe) : app.icon_path}
{#each apps as app}
<Command.Item
class="flex justify-between"
onSelect={async () => {
onSelect={() => {
if (platform === "windows") {
if (app.app_path_exe) {
open(app.app_path_exe)
@ -37,17 +34,15 @@
} else {
toast.error("Unsupported platform")
}
await getCurrentWindow().hide()
appState.clearSearchTerm()
}}
value={`app:${idx}:${app.app_desktop_path}`}
value={app.name}
>
<span class="flex gap-2">
<IconMultiplexer
icon={iconPath
icon={app.icon_path
? {
type: IconEnum.RemoteUrl,
value: convertFileSrc(iconPath, "appicon")
value: convertFileSrc(app.icon_path, "appicon")
}
: {
type: IconEnum.Iconify,
@ -56,7 +51,6 @@
class="!h-5 !w-5 shrink-0"
/>
<span>{app.name}</span>
<!-- <span>{app.app_path_exe}</span> -->
</span>
</Command.Item>
{/each}

View File

@ -11,24 +11,20 @@
} from "@/paraglide/runtime"
import { appConfig } from "@/stores"
import { Select, Switch } from "@kksh/svelte5"
import type { LoadingAnimation } from "@kksh/types"
import * as autoStart from "@tauri-apps/plugin-autostart"
import { onMount } from "svelte"
import { toast } from "svelte-sonner"
const languages = availableLanguageTags.map((lang) => ({
value: lang,
label: LanguageMap[lang as keyof typeof LanguageMap] ?? lang
label: LanguageMap[lang] ?? lang
}))
let loadingAnimation = $state<LoadingAnimation>("spinning-circle")
const loadingAnimations = ["spinning-circle", "kunkun-dancing"] as const
let launchAtLogin = $state(false)
let language = $state(languageTag())
onMount(() => {
autoStart.isEnabled().then((enabled) => {
launchAtLogin = enabled
})
loadingAnimation = $appConfig.loadingAnimation
})
const triggerContent = $derived(languages.find((f) => f.value === language)?.label ?? "Language")
</script>
@ -105,31 +101,6 @@
</Select.Content>
</Select.Root>
</li>
<li>
<span>{m.settings_general_loading_animation()}</span>
<Select.Root type="single" name="loadingAnimation" bind:value={loadingAnimation}>
<Select.Trigger class="w-fit">
{loadingAnimation}
</Select.Trigger>
<Select.Content>
<Select.Group>
<Select.GroupHeading>Loading Animation</Select.GroupHeading>
{#each loadingAnimations as anim}
<Select.Item
onclick={() => {
appConfig.setLoadingAnimation(anim)
}}
value={anim}
label={anim}
>
{anim}
</Select.Item>
{/each}
</Select.Group>
</Select.Content>
</Select.Root>
</li>
</ul>
<style scoped>

View File

@ -51,18 +51,18 @@
runtime environment for executing extension code safely. It is optional but recommended.
</p>
<p class="font-mono text-sm">Choose any installation method below.</p>
<!-- <p class="font-mono text-sm">
<p class="font-mono text-sm">
If you are unsure, you can use <strong class="text-lg">Auto Install</strong>.
</p> -->
</p>
<p class="font-mono text-sm text-red-400">
After installation, ensure the `deno` command is accessible from your system's PATH.
</p>
<!-- {#if _platform === "macos" || _platform === "linux"}
{#if _platform === "macos" || _platform === "linux"}
<p class="font-mono text-sm text-red-400">
Installation with <span class="font-bold text-green-500">curl</span> command likely requires manual
configuration. So auto install is disabled. Please copy the command and run it in a terminal.
</p>
{/if} -->
{/if}
{#if denoPath}
<div class="flex items-center gap-2">
<span></span>

View File

@ -3,7 +3,7 @@
import DevExtPathForm from "@/components/standalone/settings/DevExtPathForm.svelte"
import { i18n } from "@/i18n"
import * as m from "@/paraglide/messages"
import { appConfig, appState, extensions } from "@/stores"
import { appConfig, extensions } from "@/stores"
import { goBackOnEscape } from "@/utils/key"
import { goBack } from "@/utils/route"
import { IconEnum } from "@kksh/api/models"
@ -65,12 +65,10 @@
}
async function pickExtFolders() {
appState.setLockHideOnBlur(true)
const selected = await openFileSelector({
directory: true,
multiple: true // allow install multiple extensions at once
})
appState.setLockHideOnBlur(false)
if (!selected) {
return toast.warning("No File Selected")
}
@ -93,7 +91,6 @@
toast.warning("Please set the dev extension path in the settings")
return goto(i18n.resolveRoute("/app/settings/set-dev-ext-path"))
}
appState.setLockHideOnBlur(true)
const selected = await openFileSelector({
directory: false,
multiple: true, // allow install multiple extensions at once
@ -104,7 +101,6 @@
}
]
})
appState.setLockHideOnBlur(false)
if (!selected) {
return toast.warning("No File Selected")
}

View File

@ -3,16 +3,16 @@
* It's designed to allow all components to access a shared state.
* With context, we can avoid prop drilling, and avoid using stores which makes components hard to encapsulate.
*/
import type { AppConfigState } from "@kksh/types"
import type { AppConfig } from "@/types/appConfig"
import { getContext, setContext } from "svelte"
import type { Writable } from "svelte/store"
export const APP_CONFIG_CONTEXT_KEY = Symbol("appConfig")
export function getAppConfigContext(): Writable<AppConfigState> {
export function getAppConfigContext(): Writable<AppConfig> {
return getContext(APP_CONFIG_CONTEXT_KEY)
}
export function setAppConfigContext(appConfig: Writable<AppConfigState>) {
export function setAppConfigContext(appConfig: Writable<AppConfig>) {
setContext(APP_CONFIG_CONTEXT_KEY, appConfig)
}

View File

@ -1,4 +1,4 @@
import type { AppState } from "@kksh/types"
import type { AppState } from "@/types/appState"
import { getContext, setContext } from "svelte"
import type { Writable } from "svelte/store"

View File

@ -1,60 +0,0 @@
// /* eslint-disable @typescript-eslint/no-explicit-any */
// import { db as dbCmd } from "@kksh/api/commands"
// import * as schema from "@kksh/drizzle/schema"
// import { error } from "@tauri-apps/plugin-log"
// import { drizzle } from "drizzle-orm/sqlite-proxy"
// /**
// * Loads the sqlite database via the Tauri Proxy.
// */
// // export const sqlite = await Database.load("sqlite:test.db");
// /**
// * The drizzle database instance.
// */
// export const db = drizzle<typeof schema>(
// async (sql, params, method) => {
// let rows: any = []
// let results = []
// console.log({
// sql,
// params,
// method
// })
// console.log(sql)
// // If the query is a SELECT, use the select method
// if (isSelectQuery(sql)) {
// rows = await dbCmd.select(sql, params).catch((e) => {
// error("SQL Error:", e)
// return []
// })
// } else {
// // Otherwise, use the execute method
// rows = await dbCmd.execute(sql, params).catch((e) => {
// error("SQL Error:", e)
// return []
// })
// return { rows: [] }
// }
// rows = rows.map((row: any) => {
// return Object.values(row)
// })
// // If the method is "all", return all rows
// results = method === "all" ? rows : rows[0]
// return { rows: results }
// },
// // Pass the schema to the drizzle instance
// { schema: schema, logger: true }
// )
// /**
// * Checks if the given SQL query is a SELECT query.
// * @param sql The SQL query to check.
// * @returns True if the query is a SELECT query, false otherwise.
// */
// function isSelectQuery(sql: string): boolean {
// const selectRegex = /^\s*SELECT\b/i
// return selectRegex.test(sql)
// }

View File

@ -1,16 +1,15 @@
import { getExtensionsFolder } from "@/constants"
import type { SearchPath } from "@kksh/api/models"
import { createTauriSyncStore, type WithSyncStore } from "@/utils/sync-store"
import { updateTheme, type ThemeConfig } from "@kksh/svelte5"
import { LoadingAnimation, PersistedAppConfig, type AppConfigState } from "@kksh/types"
import { debug, error, info } from "@tauri-apps/plugin-log"
import { PersistedAppConfig, type AppConfig } from "@kksh/types"
import { debug, error } from "@tauri-apps/plugin-log"
import * as os from "@tauri-apps/plugin-os"
import { load } from "@tauri-apps/plugin-store"
import { Store } from "@tauri-store/svelte"
import { toast } from "svelte-sonner"
import { get, writable } from "svelte/store"
import * as v from "valibot"
import { setLanguageTag } from "../paraglide/runtime"
export const defaultAppConfig: AppConfigState = {
export const defaultAppConfig: AppConfig = {
isInitialized: false,
platform: "macos",
language: "en",
@ -28,82 +27,75 @@ export const defaultAppConfig: AppConfigState = {
extensionAutoUpgrade: true,
joinBetaProgram: false,
onBoarded: false,
developerMode: false,
appSearchPaths: [],
loadingAnimation: "kunkun-dancing"
developerMode: false
}
export const appConfigLoaded = writable(false)
interface AppConfigAPI {
init: () => Promise<void>
get: () => AppConfigState
get: () => AppConfig
setTheme: (theme: ThemeConfig) => void
setDevExtensionPath: (devExtensionPath: string | null) => void
setTriggerHotkey: (triggerHotkey: string[]) => void
setOnBoarded: (onBoarded: boolean) => void
setLanguage: (language: string) => void
addAppSearchPath: (appSearchPath: SearchPath) => void
removeAppSearchPath: (appSearchPath: SearchPath) => void
}
class AppConfigStore extends Store<AppConfigState> implements AppConfigAPI {
constructor() {
super("app-config", defaultAppConfig, {
saveOnChange: true
})
this.start().catch((err) => {
error("Failed to start app config store", err)
toast.error("Failed to start app config store", { description: err.message })
})
}
async init() {
function createAppConfig(): WithSyncStore<AppConfig & { language: string }> & AppConfigAPI {
const store = createTauriSyncStore("app-config", defaultAppConfig)
async function init() {
debug("Initializing app config")
const extensionsInstallDir = await getExtensionsFolder()
this.update((config) => ({
...config,
isInitialized: true,
platform: os.platform(),
extensionsInstallDir
}))
const persistStore = await load("kk-config.json", { autoSave: true })
let loadedConfig = await persistStore.get("config")
if (typeof loadedConfig === "object") {
loadedConfig = { ...defaultAppConfig, ...loadedConfig }
}
const parseRes = v.safeParse(PersistedAppConfig, loadedConfig)
if (parseRes.success) {
console.log("Parse Persisted App Config Success", parseRes.output)
const extensionsInstallDir = await getExtensionsFolder()
store.update((config) => ({
...config,
...parseRes.output,
isInitialized: true,
extensionsInstallDir,
platform: os.platform()
}))
} else {
error("Failed to parse app config, going to remove it and reinitialize")
console.error(v.flatten<typeof PersistedAppConfig>(parseRes.issues))
await persistStore.clear()
await persistStore.set("config", v.parse(PersistedAppConfig, defaultAppConfig))
}
appConfigLoaded.set(true)
store.subscribe(async (config) => {
console.log("Saving app config", config)
await persistStore.set("config", config)
updateTheme(config.theme)
})
}
get() {
return get(this)
}
setTheme(theme: ThemeConfig) {
this.update((config) => ({ ...config, theme }))
}
setDevExtensionPath(devExtensionPath: string | null) {
info(`setDevExtensionPath ${devExtensionPath}`)
this.update((config) => ({ ...config, devExtensionPath }))
}
setTriggerHotkey(triggerHotkey: string[]) {
this.update((config) => ({ ...config, triggerHotkey }))
}
setOnBoarded(onBoarded: boolean) {
this.update((config) => ({ ...config, onBoarded }))
}
setLanguage(language: string) {
this.update((config) => ({ ...config, language }))
}
addAppSearchPath(appSearchPath: SearchPath) {
this.update((config) => ({
...config,
appSearchPaths: [...config.appSearchPaths, appSearchPath]
}))
}
removeAppSearchPath(appSearchPath: SearchPath) {
this.update((config) => ({
...config,
appSearchPaths: config.appSearchPaths.filter((path) => path.path !== appSearchPath.path)
}))
}
setLoadingAnimation(loadingAnimation: LoadingAnimation) {
this.update((config) => ({ ...config, loadingAnimation }))
return {
...store,
get: () => get(store),
setTheme: (theme: ThemeConfig) => store.update((config) => ({ ...config, theme })),
setDevExtensionPath: (devExtensionPath: string | null) => {
console.log("setDevExtensionPath", devExtensionPath)
store.update((config) => ({ ...config, devExtensionPath }))
},
setTriggerHotkey: (triggerHotkey: string[]) => {
store.update((config) => ({ ...config, triggerHotkey }))
},
setOnBoarded: (onBoarded: boolean) => {
store.update((config) => ({ ...config, onBoarded }))
},
setLanguage: (language: string) => {
store.update((config) => ({ ...config, language }))
},
init
}
}
// export const appConfig = createAppConfig()
export const appConfig = new AppConfigStore()
export const appConfig = createAppConfig()

View File

@ -7,9 +7,7 @@ export const defaultAppState: AppState = {
highlightedCmd: "",
loadingBar: false,
defaultAction: "",
actionPanel: undefined,
lockHideOnBlur: false, // when dialog is open, we don't hide the app, we lock the hide on blur and unlock when dialog is closed
fullScreenLoading: false
actionPanel: undefined
}
interface AppStateAPI {
@ -18,8 +16,6 @@ interface AppStateAPI {
setLoadingBar: (loadingBar: boolean) => void
setDefaultAction: (defaultAction: string | null) => void
setActionPanel: (actionPanel?: ActionSchema.ActionPanel) => void
setLockHideOnBlur: (lockHideOnBlur: boolean) => void
setFullScreenLoading: (fullScreenLoading: boolean) => void
}
function createAppState(): Writable<AppState> & AppStateAPI {
@ -39,12 +35,6 @@ function createAppState(): Writable<AppState> & AppStateAPI {
},
setActionPanel: (actionPanel?: ActionSchema.ActionPanel) => {
store.update((state) => ({ ...state, actionPanel }))
},
setLockHideOnBlur: (lockHideOnBlur: boolean) => {
store.update((state) => ({ ...state, lockHideOnBlur }))
},
setFullScreenLoading: (fullScreenLoading: boolean) => {
store.update((state) => ({ ...state, fullScreenLoading }))
}
}
}

View File

@ -3,16 +3,9 @@ import { AppInfo } from "@kksh/api/models"
import { commandScore } from "@kksh/ui/utils"
import * as fs from "@tauri-apps/plugin-fs"
import { platform } from "@tauri-apps/plugin-os"
import Fuse from "fuse.js"
import { derived, get, writable } from "svelte/store"
import { appState } from "./appState"
const fuse = new Fuse<AppInfo>([], {
includeScore: true,
threshold: 0.2,
keys: ["name"]
})
export function createAppsLoaderStore() {
const store = writable<AppInfo[]>([])
@ -35,14 +28,24 @@ export function createAppsLoaderStore() {
// console.log("filteredApps", apps)
// fs.writeTextFile("/Users/hk/Desktop/apps.json", JSON.stringify(apps))
store.set(apps)
fuse.setCollection(apps)
}
}
}
export const appsLoader = createAppsLoaderStore()
export const appsFiltered = derived([appsLoader, appState], ([$apps, $appState]) => {
return $appState.searchTerm.length > 0
? fuse.search($appState.searchTerm).map((result) => result.item)
: $apps.slice(0, 20)
})
// export const appsFiltered = derived([appsLoader, appState], ([$apps, $appState]) => {
// return []
// return $apps.filter((app) => {
// if ($appState.searchTerm.length === 0) {
// return false
// }
// return (
// commandScore(
// app.name,
// $appState.searchTerm
// // []
// ) > 0.8
// )
// })
// })

View File

@ -1,9 +1,11 @@
import type { CustomUiCmd, ExtPackageJsonExtra, HeadlessCmd, TemplateUiCmd } from "@kksh/api/models"
import { db } from "@kksh/drizzle"
import { getExtensionsFolder } from "@/constants"
import { db } from "@kksh/api/commands"
import type { ExtPackageJson, ExtPackageJsonExtra } from "@kksh/api/models"
import * as extAPI from "@kksh/extension"
import { commandScore } from "@kksh/ui/utils"
import * as path from "@tauri-apps/api/path"
import Fuse from "fuse.js"
import { derived, get, writable, type Writable } from "svelte/store"
import * as fs from "@tauri-apps/plugin-fs"
import { derived, get, writable, type Readable, type Writable } from "svelte/store"
import { appConfig } from "./appConfig"
import { appState } from "./appState"
@ -225,62 +227,44 @@ function createExtensionsStore(): Writable<ExtPackageJsonExtra[]> & {
}
export const extensions = createExtensionsStore()
export const installedStoreExts = derived(extensions, ($extensions) => {
const extContainerPath = get(appConfig).extensionsInstallDir
if (!extContainerPath) return []
return $extensions.filter((ext) => !extAPI.isExtPathInDev(extContainerPath, ext.extPath))
})
export const devStoreExts = derived(extensions, ($extensions) => {
const extContainerPath = get(appConfig).extensionsInstallDir
if (!extContainerPath) return []
return $extensions.filter((ext) => extAPI.isExtPathInDev(extContainerPath, ext.extPath))
})
export type StoreExtCmd = (CustomUiCmd | TemplateUiCmd | HeadlessCmd) & {
ext: ExtPackageJsonExtra
}
export const installedStoreExts: Readable<ExtPackageJsonExtra[]> = derived(
extensions,
($extensionsStore) => {
const extContainerPath = get(appConfig).extensionsInstallDir
if (!extContainerPath) return []
return $extensionsStore.filter((ext) => !extAPI.isExtPathInDev(extContainerPath, ext.extPath))
}
)
export const devStoreExts: Readable<ExtPackageJsonExtra[]> = derived(
extensions,
($extensionsStore) => {
const extContainerPath = get(appConfig).extensionsInstallDir
if (!extContainerPath) return []
return $extensionsStore.filter((ext) => extAPI.isExtPathInDev(extContainerPath, ext.extPath))
}
)
export const cmdsFuse = new Fuse<StoreExtCmd>([], {
includeScore: true,
threshold: 0.2,
keys: ["name"]
})
export const devCmdsFuse = new Fuse<StoreExtCmd>([], {
includeScore: true,
threshold: 0.2,
keys: ["name"]
})
// export const installedStoreExtsFiltered = derived(
// [installedStoreExts, appState],
// ([$installedStoreExts, $appState]) => {
// return $installedStoreExts.filter(
// (ext) => commandScore(ext.kunkun.name, $appState.searchTerm) > 0.5
// )
// }
// )
export const storeExtCmds = derived(installedStoreExts, ($exts) => {
const cmds = $exts.flatMap((ext) => {
return [
...(ext.kunkun.customUiCmds ?? []),
...(ext.kunkun.templateUiCmds ?? []),
...(ext.kunkun.headlessCmds ?? [])
].map((cmd) => ({ ...cmd, ext }))
})
cmdsFuse.setCollection(cmds)
return cmds
})
export const devStoreExtCmds = derived(devStoreExts, ($exts) => {
const cmds = $exts.flatMap((ext) => {
return [
...(ext.kunkun.customUiCmds ?? []),
...(ext.kunkun.templateUiCmds ?? []),
...(ext.kunkun.headlessCmds ?? [])
].map((cmd) => ({ ...cmd, ext }))
})
devCmdsFuse.setCollection(cmds)
return cmds
})
export const storeSearchExtCmds = derived([storeExtCmds, appState], ([$extCmds, $appState]) => {
return $appState.searchTerm
? cmdsFuse.search($appState.searchTerm).map((result) => result.item)
: $extCmds
})
export const devSearchExtCmds = derived([devStoreExtCmds, appState], ([$extCmds, $appState]) => {
return $appState.searchTerm
? devCmdsFuse.search($appState.searchTerm).map((result) => result.item)
: $extCmds
})
// export const devStoreExtsFiltered = derived(
// [devStoreExts, appState],
// ([$devStoreExts, $appState]) => {
// return $devStoreExts.filter((ext) => {
// console.log(
// "commandScore",
// ext.kunkun.name,
// $appState.searchTerm,
// commandScore(ext.kunkun.name, $appState.searchTerm)
// )
// return commandScore(ext.kunkun.name, $appState.searchTerm) > 0.1
// })
// }
// )

View File

@ -2,16 +2,9 @@ import type { Icon } from "@kksh/api/models"
import { createQuickLinkCommand, getAllQuickLinkCommands } from "@kksh/extension/db"
import type { QuickLink } from "@kksh/ui/types"
import { commandScore } from "@kksh/ui/utils"
import Fuse from "fuse.js"
import { derived, get, writable, type Writable } from "svelte/store"
import { appState } from "./appState"
const fuse = new Fuse<QuickLink>([], {
includeScore: true,
threshold: 0.2,
keys: ["name"]
})
export interface QuickLinkAPI {
get: () => QuickLink[]
init: () => Promise<void>
@ -28,9 +21,7 @@ function createQuickLinksStore(): Writable<QuickLink[]> & QuickLinkAPI {
async function refresh() {
const cmds = await getAllQuickLinkCommands()
const items = cmds.map((cmd) => ({ link: cmd.data.link, name: cmd.name, icon: cmd.data.icon }))
store.set(items)
fuse.setCollection(items)
store.set(cmds.map((cmd) => ({ link: cmd.data.link, name: cmd.name, icon: cmd.data.icon })))
}
async function createQuickLink(name: string, link: string, icon: Icon) {
@ -48,11 +39,7 @@ function createQuickLinksStore(): Writable<QuickLink[]> & QuickLinkAPI {
}
export const quickLinks = createQuickLinksStore()
export const quickLinksFiltered = derived([quickLinks, appState], ([$quickLinks, $appState]) => {
return $appState.searchTerm
? fuse.search($appState.searchTerm).map((result) => result.item)
: $quickLinks
})
// export const quickLinksFiltered = derived([quickLinks, appState], ([$quicklinks, $appState]) => {
// return $quicklinks.filter((lnk) => {
// if ($appState.searchTerm.length === 0) {

View File

@ -104,7 +104,6 @@ function createWinExtMapStore(): Writable<WinExtMap> & API {
}
},
registerProcess: async (windowLabel: string, pid: number) => {
console.log("registerProcess", windowLabel, pid)
const winExtMap = get(store)
await registerExtensionSpawnedProcess(windowLabel, pid)
if (!winExtMap[windowLabel]) {
@ -117,7 +116,6 @@ function createWinExtMapStore(): Writable<WinExtMap> & API {
const winExtMap = get(store)
const found = Object.entries(winExtMap).find(([windowLabel, ext]) => ext.pids.includes(pid))
if (!found) {
warn(`Process ${pid} does not have an extension registered, thus will not be killed`)
return
}
const [windowLabel, ext] = found

View File

@ -1,13 +1,19 @@
import { SupabaseAPI } from "@kksh/supabase/api"
import type { Database } from "@kksh/supabase/types"
import * as sb from "@supabase/supabase-js"
import { SUPABASE_ANON_KEY, SUPABASE_URL } from "./constants"
// export const supabase = createSB(SUPABASE_URL, SUPABASE_ANON_KEY)
export const supabase: sb.SupabaseClient = sb.createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
auth: {
flowType: "pkce"
export const supabase: sb.SupabaseClient<Database> = sb.createClient<Database>(
SUPABASE_URL,
SUPABASE_ANON_KEY,
{
auth: {
flowType: "pkce"
}
}
})
)
export const storage = supabase.storage
export const supabaseExtensionsStorage = supabase.storage.from("extensions")
// export const supabaseAPI = new SupabaseAPI(supabase)
export const supabaseAPI = new SupabaseAPI(supabase)

View File

@ -1,44 +0,0 @@
import { proxyDB, schema } from "@kksh/drizzle"
import { getExtClipboard } from "@kksh/drizzle/api"
import { error, info } from "@tauri-apps/plugin-log"
import * as orm from "drizzle-orm"
/**
* For now, simply delete all clipboard data older than 10 days
*/
export async function cleanClipboard() {
const nDays = 10
const clipboardExt = await getExtClipboard()
const nDaysAgo = new Date()
nDaysAgo.setDate(nDaysAgo.getDate() - nDays)
try {
// Select data older than 10 days to check what will be deleted
const oldClipboardData = await proxyDB
.select({ count: orm.count() })
.from(schema.extensionData)
.where(
orm.and(
orm.eq(schema.extensionData.extId, clipboardExt.extId),
orm.lt(schema.extensionData.createdAt, nDaysAgo.toISOString())
)
)
const nLinesToDelete = oldClipboardData.at(0)?.count ?? 0
console.info(`Found ${nLinesToDelete} clipboard entries older than ${nDays} days to clean up`)
info(`Found ${nLinesToDelete} clipboard entries older than ${nDays} days to clean up`)
// Now delete the old data
const deleted = await proxyDB
.delete(schema.extensionData)
.where(
orm.and(
orm.eq(schema.extensionData.extId, clipboardExt.extId),
orm.lt(schema.extensionData.createdAt, nDaysAgo.toISOString())
)
)
console.log("deleted", deleted)
} catch (e) {
error(`Error during clipboard cleanup: ${e}`)
}
}

View File

@ -1,13 +0,0 @@
import { proxyDB } from "@kksh/drizzle"
import { error, info } from "@tauri-apps/plugin-log"
import { sql } from "drizzle-orm"
export async function vacuumSqlite() {
const statement = sql`VACUUM`
try {
await proxyDB.run(statement)
info("Vacuumed sqlite")
} catch (error) {
console.error(`Failed to vacuum sqlite: ${error}`)
}
}

View File

@ -1,5 +1,4 @@
import { app } from "@tauri-apps/api"
import { getAllWindows, getCurrentWindow, type Window } from "@tauri-apps/api/window"
import { getAllWindows } from "@tauri-apps/api/window"
import { isRegistered, register, unregister } from "@tauri-apps/plugin-global-shortcut"
import { debug, info, warn } from "@tauri-apps/plugin-log"
import * as os from "@tauri-apps/plugin-os"
@ -48,7 +47,8 @@ export async function registerAppHotkey(hotkeyStr: string) {
mainWin.setFocus()
}
} else {
mainWin.show().then(() => mainWin.setFocus())
mainWin.show()
mainWin.setFocus()
}
}
})
@ -78,12 +78,12 @@ export async function applyKeyComb(keys: userInput.Key[]) {
// await Promise.all(keys.map((key) => userInput.key("KeyPress", key)))
for (const key of keys) {
await userInput.key("KeyPress", key)
await sleep(100)
await sleep(20)
}
await sleep(150)
await sleep(100)
for (const key of keys) {
await userInput.key("KeyRelease", key)
await sleep(100)
await sleep(20)
}
}
@ -101,12 +101,3 @@ export async function paste() {
console.error("Unsupported platform: " + _platform)
}
}
export async function hideAndPaste(win?: Window) {
return app
.hide()
.then(() => sleep(60))
.then(() => (win ?? getCurrentWindow()).hide())
.then(() => sleep(60))
.then(() => paste())
}

View File

@ -1,16 +1,14 @@
import { appConfig, extensions } from "@/stores"
import { getCurrentWindow } from "@tauri-apps/api/window"
import { error, info } from "@tauri-apps/plugin-log"
import { info } from "@tauri-apps/plugin-log"
import { dev } from "$app/environment"
import { cleanClipboard } from "./clipboard"
import { vacuumSqlite } from "./db"
import { mapKeyToTauriKey, registerAppHotkey } from "./hotkey"
import { listenToReloadOneExtension } from "./tauri-events"
/**
* Initialize the app
*/
export async function init() {
export function init() {
const window = getCurrentWindow()
if (window.label === "main") {
initMainWindow()
@ -19,14 +17,7 @@ export async function init() {
extensions.reloadExtension(extPath)
})
}
await cleanClipboard()
.then(() => {
info("Cleaned clipboard")
})
.catch((e) => {
error(`Failed to clean clipboard: ${e}`)
})
vacuumSqlite()
if (!dev) {
// document.addEventListener("contextmenu", function (event) {
// event.preventDefault()

View File

@ -97,7 +97,7 @@ export async function globalKeyDownHandler(e: KeyboardEvent) {
await appWin.hide()
location.reload()
setTimeout(() => {
appWin.show().then(() => appWin.setFocus())
appWin.show()
}, 1_000)
}
}

View File

@ -1,19 +0,0 @@
import { parseAPIVersion } from "@kksh/extension/load"
import type { ExtPackageJsonExtra } from "@kunkunapi/src/models/manifest"
import semver from "semver"
/**
* Decide the serialization method for kkrpc based on the api version
* apiVersion is populated in loadExtensionManifestFromDisk, but we parse it again
* @param apiVersion - The version of the @kksh/api
* @returns "superjson" or "json"
*/
export function decideKkrpcSerialization(ext: ExtPackageJsonExtra): "superjson" | "json" {
const apiVersion = parseAPIVersion(ext.dependencies || {})
if (apiVersion && semver.lte(apiVersion, "0.1.5")) {
// 0.1.6 is the first version that supports superjson and default to use superjson
return "json"
}
// fallback default to use superjson, some extensions might not install @kksh/api
return "superjson"
}

View File

@ -1,7 +1,8 @@
import { extensions } from "@/stores"
import { supabaseAPI } from "@/supabase"
import { isCompatible } from "@kksh/api"
import type { ExtPackageJsonExtra } from "@kksh/api/models"
import { getExtensionsLatestPublishByIdentifier } from "@kksh/sdk"
import { greaterThan } from "@std/semver"
import { relaunch } from "@tauri-apps/plugin-process"
import { check } from "@tauri-apps/plugin-updater"
import { gt } from "semver"
@ -31,22 +32,11 @@ export async function checkSingleExtensionUpdate(
installedExt: ExtPackageJsonExtra,
autoupgrade: boolean
) {
const {
data: sbExt,
error,
response
} = await getExtensionsLatestPublishByIdentifier({
path: {
identifier: "RAG"
}
})
// const { data: sbExt, error } = await supabaseAPI.getLatestExtPublish(
// installedExt.kunkun.identifier
// )
const { data: sbExt, error } = await supabaseAPI.getLatestExtPublish(
installedExt.kunkun.identifier
)
if (error) {
return toast.error(
`Failed to check update for ${installedExt.kunkun.identifier}: ${error} (${response.status})`
)
return toast.error(`Failed to check update for ${installedExt.kunkun.identifier}: ${error}`)
}
if (!sbExt) {
@ -59,7 +49,10 @@ export async function checkSingleExtensionUpdate(
) {
if (autoupgrade) {
await extensions
.upgradeStoreExtension(sbExt.identifier, sbExt.tarball_path)
.upgradeStoreExtension(
sbExt.identifier,
supabaseAPI.translateExtensionFilePathToUrl(sbExt.tarball_path)
)
.then(() => {
toast.success(`${sbExt.name} upgraded`, {
description: `From ${installedExt.version} to ${sbExt.version}`

View File

@ -12,7 +12,6 @@
<svelte:window on:keydown={handleKeyDown} />
<div class="fixed h-12 w-full" data-tauri-drag-region></div>
<Layouts.Center class="min-h-screen py-5">
<Error.RawErrorJSONPreset
title="Error"

View File

@ -2,8 +2,6 @@
import { ParaglideJS } from "@inlang/paraglide-sveltekit"
import { i18n } from "$lib/i18n"
import "../app.css"
import FullScreenLoading from "@/components/common/FullScreenLoading.svelte"
import { appState } from "@/stores/appState"
import { ModeWatcher, ThemeWrapper } from "@kksh/svelte5"
import { Toaster } from "svelte-sonner"
@ -14,9 +12,6 @@
<ModeWatcher />
<Toaster richColors closeButton />
<ThemeWrapper>
{#if $appState.fullScreenLoading}
<FullScreenLoading class="bg-background absolute inset-0 z-50" />
{/if}
{@render children()}
</ThemeWrapper>
</ParaglideJS>

View File

@ -1,15 +1,5 @@
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"
import { browser } from "$app/environment"
// Tauri doesn't have a Node.js server to do proper SSR
// so we will use adapter-static to prerender the app (SSG)
// See: https://v2.tauri.app/start/frontend/sveltekit/ for more info
export const prerender = true
export const ssr = false
export const load = () => {
if (browser) {
const win = getCurrentWebviewWindow()
return { win }
}
}

View File

@ -14,7 +14,7 @@
import { Constants, ViewTransition } from "@kksh/ui"
import type { UnlistenFn } from "@tauri-apps/api/event"
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"
import { attachConsole, debug, error, info } from "@tauri-apps/plugin-log"
import { attachConsole, error, info } from "@tauri-apps/plugin-log"
import { afterNavigate, beforeNavigate } from "$app/navigation"
import { gsap } from "gsap"
import { Flip } from "gsap/Flip"
@ -46,14 +46,13 @@
})
})
let { children, data } = $props()
let { children } = $props()
const unlisteners: UnlistenFn[] = []
onDestroy(() => {
unlisteners.forEach((unlistener) => unlistener())
})
onMount(async () => {
await attachConsole().then((unlistener) => unlisteners.push(unlistener))
attachConsole().then((unlistener) => unlisteners.push(unlistener))
initDeeplink().then((unlistener) => unlisteners.push(unlistener))
shellx
.fixPathEnv()
@ -80,7 +79,7 @@
// this extra is focused check may be needed because blur event got triggered somehow when window show()
// for edge case: when settings page is opened and focused, switch to main window, the blur event is triggered for main window
if (!isFocused) {
if ($appConfig.hideOnBlur && !$appState.lockHideOnBlur) {
if ($appConfig.hideOnBlur) {
win.hide()
}
}
@ -90,18 +89,18 @@
extensions.init()
unlisteners.push(
await listenToRecordExtensionProcessEvent(async (event) => {
debug(`record extension process event ${event.payload.pid}`)
console.log("record extension process event", event)
winExtMap.registerProcess(event.payload.windowLabel, event.payload.pid)
})
)
unlisteners.push(
await listenToKillProcessEvent((event) => {
debug(`kill process event ${event.payload.pid}`)
console.log("kill process event", event)
winExtMap.unregisterProcess(event.payload.pid)
})
)
}
data.win?.show().then(() => data.win?.setFocus())
getCurrentWebviewWindow().show()
})
</script>

View File

@ -1,11 +1,7 @@
import { getExtensionsFolder, IS_IN_TAURI } from "@/constants"
import * as path from "@tauri-apps/api/path"
import { error } from "@tauri-apps/plugin-log"
import { setStoreCollectionPath } from "@tauri-store/svelte"
import type { LayoutLoad } from "./$types"
export const load: LayoutLoad = async () => {
const appDataPath = await path.appDataDir()
await setStoreCollectionPath(await path.join(appDataPath, "kk-config"))
return { extsInstallDir: IS_IN_TAURI ? await getExtensionsFolder() : "", appDataPath }
return { extsInstallDir: IS_IN_TAURI ? await getExtensionsFolder() : "" }
}

View File

@ -2,20 +2,22 @@
<script lang="ts">
import { commandLaunchers } from "@/cmds"
import { builtinCmds } from "@/cmds/builtin"
import { systemCommands, systemCommandsFiltered } from "@/cmds/system"
import { systemCommands } from "@/cmds/system"
import AppsCmds from "@/components/main/AppsCmds.svelte"
import { i18n } from "@/i18n"
import * as m from "@/paraglide/messages"
import {
appConfig,
appConfigLoaded,
appsFiltered,
// appsFiltered,
appsLoader,
appState,
devSearchExtCmds,
devStoreExtCmds,
quickLinksFiltered,
storeExtCmds,
storeSearchExtCmds
devStoreExts,
// devStoreExtsFiltered,
// installedStoreExtsFiltered,
installedStoreExts,
quickLinks
// quickLinksFiltered
} from "@/stores"
import { cmdQueries } from "@/stores/cmdQuery"
import { isKeyboardEventFromInputElement } from "@/utils/dom"
@ -25,28 +27,19 @@
import {
BuiltinCmds,
CustomCommandInput,
ExtCmds,
ExtCmdsGroup,
GlobalCommandPaletteFooter,
QuickLinks,
SystemCmds
} from "@kksh/ui/main"
import { cn } from "@kksh/ui/utils"
import { Ext } from "@kunkunapi/src/models/extension"
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"
import { getCurrentWindow, Window } from "@tauri-apps/api/window"
import { platform } from "@tauri-apps/plugin-os"
import { exit } from "@tauri-apps/plugin-process"
import { goto } from "$app/navigation"
import {
ArrowBigUpIcon,
CircleXIcon,
EllipsisVerticalIcon,
RefreshCcwIcon,
SettingsIcon
} from "lucide-svelte"
import { ArrowBigUpIcon, CircleXIcon, EllipsisVerticalIcon, RefreshCcwIcon } from "lucide-svelte"
import { onMount } from "svelte"
import { Inspect } from "svelte-inspect-value"
import * as v from "valibot"
const win = getCurrentWindow()
let inputEle: HTMLInputElement | null = $state(null)
@ -66,16 +59,12 @@
if (splashscreenWin) {
splashscreenWin.close()
}
win.show().then(() => win.setFocus())
win.show()
})
win.onFocusChanged(({ payload: focused }) => {
if (focused) {
win
.show()
.then(() => win.setFocus())
.finally(() => {
inputEle?.focus()
})
win.show()
inputEle?.focus()
}
})
inputEle?.focus()
@ -83,7 +72,6 @@
// wait for appConfig store to be loaded, it's async and saved to disk when changed, so we use another store appConfigLoaded
// to keep track of the loading status
if (loaded) {
console.log("appConfig.get().onBoarded", appConfig.get().onBoarded)
if (!appConfig.get().onBoarded) {
setTimeout(() => {
goto(i18n.resolveRoute("/app/help/onboarding"))
@ -106,21 +94,10 @@
}
}}
/>
<!--
<Inspect name="devStoreExts" value={$devStoreExts} />
<Inspect name="extensions" value={$extensions} />
<Inspect name="installedStoreExts" value={$installedStoreExts} />
<Inspect name="storeSearchExtCmds" value={$storeSearchExtCmds} />
<Inspect name="devSearchExtCmds" value={$devSearchExtCmds} />
<Inspect name="storeExtCmds" value={$storeExtCmds} />
<Inspect name="devStoreExtCmds" value={$devStoreExtCmds} />
<Inspect name="$appState.searchTerm" value={$appState.searchTerm} />
-->
<Command.Root
class={cn("h-screen rounded-lg shadow-md")}
bind:value={$appState.highlightedCmd}
shouldFilter={false}
shouldFilter={true}
loop
>
<CustomCommandInput
@ -192,8 +169,8 @@
<span class="flex items-center">⌃+<ArrowBigUpIcon class="h-4 w-4" />+R </span>
</DropdownMenu.Shortcut>
</DropdownMenu.Item>
<DropdownMenu.Item onclick={() => goto(i18n.resolveRoute("/app/settings"))}>
<SettingsIcon class="mr-2 h-4 w-4 text-green-500" />
<DropdownMenu.Item onclick={() => location.reload()}>
<RefreshCcwIcon class="mr-2 h-4 w-4 text-green-500" />
{m.home_command_input_dropdown_open_preference()}
<DropdownMenu.Shortcut>
{#if platform() === "macos"}
@ -221,37 +198,45 @@
</CustomCommandInput>
<Command.List class="max-h-screen grow">
<Command.Empty data-tauri-drag-region>No results found.</Command.Empty>
{#if $devSearchExtCmds.length > 0}
<ExtCmds
{#if $appConfig.extensionsInstallDir && $devStoreExts.length > 0}
<ExtCmdsGroup
extensions={$devStoreExts}
heading={m.command_group_heading_dev_ext()}
extCmds={$devSearchExtCmds}
hmr={$appConfig.hmr}
isDev={true}
onExtCmdSelect={commandLaunchers.onExtCmdSelect}
hmr={$appConfig.hmr}
/>
{/if}
{#if $appConfig.extensionsInstallDir && $installedStoreExts.length > 0}
<ExtCmdsGroup
extensions={$installedStoreExts}
heading={m.command_group_heading_ext()}
isDev={false}
hmr={false}
onExtCmdSelect={commandLaunchers.onExtCmdSelect}
/>
{/if}
{#if $storeSearchExtCmds.length > 0}
<ExtCmds
heading={m.command_group_heading_ext()}
extCmds={$storeSearchExtCmds}
hmr={false}
isDev={false}
onExtCmdSelect={commandLaunchers.onExtCmdSelect}
/>
<QuickLinks quickLinks={$quickLinks} />
<BuiltinCmds builtinCmds={$builtinCmds} />
<SystemCmds systemCommands={$systemCommands} />
<AppsCmds apps={$appsLoader} />
<!-- <AppsCmds apps={$appsFiltered} /> -->
<!-- {#if $quickLinksFiltered.length > 0}
<QuickLinks quickLinks={$quickLinksFiltered} />
{/if}
{#if $appsFiltered.length > 0}
<AppsCmds apps={$appsFiltered} />
{/if}
{#if $builtinCmds.length > 0}
<BuiltinCmds builtinCmds={$builtinCmds} />
{/if}
{#if $systemCommandsFiltered.length > 0}
<SystemCmds systemCommands={$systemCommandsFiltered} />
{/if}
{#if $appsFiltered.length > 0}
<AppsCmds apps={$appsFiltered} />
{/if}
{#if $quickLinksFiltered.length > 0}
<QuickLinks quickLinks={$quickLinksFiltered} />
{/if}
{/if} -->
<!-- <AppsCmds apps={$appsLoader} /> -->
<!-- <AppsCmds apps={$appsFiltered} /> -->
</Command.List>
<GlobalCommandPaletteFooter />
</Command.Root>

View File

@ -1,14 +1,15 @@
<script lang="ts">
import { hideAndPaste } from "@/utils/hotkey"
import { goHome } from "@/utils/route"
import { listenToNewClipboardItem, listenToWindowFocus } from "@/utils/tauri-events"
import { paste } from "@/utils/hotkey"
import { goBack, goHome } from "@/utils/route"
import { listenToNewClipboardItem } from "@/utils/tauri-events"
import { sleep } from "@/utils/time"
import Icon from "@iconify/svelte"
import { ClipboardContentType } from "@kksh/api/commands"
import { ClipboardContentType, db } from "@kksh/api/commands"
import { SearchModeEnum, SQLSortOrderEnum, type ExtData } from "@kksh/api/models"
import { db } from "@kksh/drizzle"
import { Button, Command, Resizable } from "@kksh/svelte5"
import { Constants } from "@kksh/ui"
import { CustomCommandInput, GlobalCommandPaletteFooter } from "@kksh/ui/main"
import { app } from "@tauri-apps/api"
import type { UnlistenFn } from "@tauri-apps/api/event"
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"
import { platform } from "@tauri-apps/plugin-os"
@ -19,14 +20,12 @@
import ContentPreview from "./content-preview.svelte"
const _platform = platform()
let inputEle = $state<HTMLInputElement | null>(null)
const curWin = getCurrentWebviewWindow()
let searchTerm = $state("")
let clipboardHistoryList = $state<ExtData[]>([])
let highlightedItemValue = $state<string>("")
let highlighted = $state<ExtData | null>(null)
let unlistenClipboard = $state<UnlistenFn | null>(null)
let unlistenFocusEvt = $state<UnlistenFn | null>(null)
let isScrolling = $state(false)
let page = $state(1)
@ -75,19 +74,10 @@
}).then((unlisten) => {
unlistenClipboard = unlisten
})
listenToWindowFocus(async () => {
if (inputEle) {
inputEle.focus()
}
}).then((unlisten) => {
unlistenFocusEvt = unlisten
})
})
onDestroy(() => {
unlistenClipboard?.()
unlistenFocusEvt?.()
})
$effect(() => {
@ -201,7 +191,11 @@
return Promise.reject(new Error("No data found"))
}
return writeToClipboard(data).then(async () => {
return hideAndPaste(curWin)
return app
.hide()
.then(() => sleep(100))
.then(() => curWin.hide())
.then(() => paste())
})
})
.then(() => toast.success("Copied to clipboard"))
@ -248,7 +242,6 @@
autofocus
placeholder="Type a command or search..."
leftSlot={leftSlot as Snippet}
bind:ref={inputEle}
bind:value={searchTerm}
/>
<Resizable.PaneGroup direction="horizontal" class="w-full rounded-lg">

View File

@ -1,7 +1,7 @@
<script lang="ts">
import { cn } from "@/utils"
import { db } from "@kksh/api/commands"
import type { ExtData } from "@kksh/api/models"
import { db } from "@kksh/drizzle"
import { Resizable, Separator } from "@kksh/svelte5"
import { convertFileSrc } from "@tauri-apps/api/core"
import DOMPurify from "dompurify"

View File

@ -8,8 +8,6 @@
import * as userInput from "tauri-plugin-user-input-api"
import { type InputEvent } from "tauri-plugin-user-input-api"
let { data } = $props()
const SymbolMap = {
Alt: "⎇",
AltGr: "⌥",
@ -99,7 +97,10 @@
}
$effect(() => {
data.win?.show().then(() => data.win?.setFocus())
const win = getCurrentWebviewWindow()
if (win) {
win.show()
}
userInput.setEventTypes([userInput.EventTypeEnum.KeyPress, userInput.EventTypeEnum.KeyRelease])
userInput.startListening((evt: InputEvent) => {

View File

@ -8,24 +8,23 @@
import * as clipboard from "tauri-plugin-clipboard-api"
let image = $state<string | null>(null)
let { data } = $props()
const appWin = getCurrentWebviewWindow()
let originalSize = $state<{ width: number; height: number } | null>(null)
let originalScaleFactor = $state<number | null>(null)
let scale = $state<number>(1)
let currentSize = $derived(
originalSize ? { width: originalSize.width * scale, height: originalSize.height * scale } : null
)
const win = getCurrentWebviewWindow()
$effect(() => {
if (currentSize) {
win.setSize(new LogicalSize(currentSize.width, currentSize.height))
appWin.setSize(new LogicalSize(currentSize.width, currentSize.height))
}
})
async function getWindowSize() {
const size = await win.outerSize()
const scaleFactor = originalScaleFactor ?? (await win.scaleFactor())
const size = await appWin.outerSize()
const scaleFactor = originalScaleFactor ?? (await appWin.scaleFactor())
const logicalSize = size.toLogical(scaleFactor)
return { logicalSize, scaleFactor }
}
@ -37,7 +36,7 @@
image = b64
})
.finally(() => {
data.win?.show().then(() => data.win?.setFocus())
appWin.show()
})
const { logicalSize, scaleFactor } = await getWindowSize()
originalSize = { width: logicalSize.width, height: logicalSize.height }
@ -68,13 +67,13 @@
<svelte:window
on:keydown={(e) => {
if (e.key === "Escape") {
win.close()
appWin.close()
}
}}
/>
<Button size="icon" variant="ghost" class="fixed left-2 top-2" onclick={() => win.close()}>
<CircleX />
</Button>
<Button size="icon" variant="ghost" class="fixed left-2 top-2" onclick={() => appWin.close()}
><CircleX /></Button
>
<main class="z-50 h-screen w-screen overflow-hidden" data-tauri-drag-region>
{#if image}
<img

View File

@ -2,13 +2,14 @@
import { getExtensionsFolder } from "@/constants"
import { appState, extensions } from "@/stores"
import { keys } from "@/stores/keys"
import { supabaseAPI } from "@/supabase"
import { goBackOnEscapeClearSearchTerm, goHomeOnEscapeClearSearchTerm } from "@/utils/key"
import { goBack, goHome } from "@/utils/route"
import { Action as ActionSchema, ExtensionStoreListItem, ExtPublish } from "@kksh/api/models"
import { Action as ActionSchema } from "@kksh/api/models"
import { Action } from "@kksh/api/ui"
import {
getExtensionsLatestPublishByIdentifier,
postExtensionsIncrementDownloads
} from "@kksh/sdk"
import { SBExt } from "@kksh/supabase/models"
import type { ExtPublishMetadata } from "@kksh/supabase/models"
import { type Tables } from "@kksh/supabase/types"
import { Button, Command } from "@kksh/svelte5"
import { Constants } from "@kksh/ui"
import { ExtListItem } from "@kksh/ui/extension"
@ -16,9 +17,8 @@
import { platform } from "@tauri-apps/plugin-os"
import { goto } from "$app/navigation"
import { ArrowLeft } from "lucide-svelte"
import { onMount, type Snippet } from "svelte"
import type { Snippet } from "svelte"
import { toast } from "svelte-sonner"
import type { Action as SvelteAction } from "svelte/action"
import { getInstallExtras } from "./[identifier]/helper.js"
let { data } = $props()
@ -38,89 +38,51 @@
// )
// }
onMount(() => {
// setTimeout(() => {
// console.log("focus", listviewInputRef)
// listviewInputRef?.focus()
// }, 3_000)
})
const inputAction: SvelteAction = (node) => {
// the node has been mounted in the DOM
$effect(() => {
// setup goes here
console.log("inputAction", node)
listviewInputRef?.focus()
return () => {
// teardown goes here
}
})
}
$effect(() => {
function docKeyDown(e: KeyboardEvent) {
if (e.key === "/") {
listviewInputRef?.focus()
}
}
document.addEventListener("keydown", docKeyDown)
return () => {
document.removeEventListener("keydown", docKeyDown)
}
})
function onExtItemSelected(ext: ExtensionStoreListItem) {
function onExtItemSelected(ext: SBExt) {
goto(`./store/${ext.identifier}`)
}
async function onExtItemUpgrade(ext: ExtensionStoreListItem) {
const { data, error, response } = await getExtensionsLatestPublishByIdentifier({
path: {
identifier: ext.identifier
}
})
if (error)
async function onExtItemUpgrade(ext: SBExt) {
const res = await supabaseAPI.getLatestExtPublish(ext.identifier)
if (res.error)
return toast.error("Fail to get latest extension", {
description: error.error
description: res.error.message
})
const installExtras = await getInstallExtras(data?.metadata)
const tarballUrl = res.data.tarball_path.startsWith("http")
? res.data.tarball_path
: supabaseAPI.translateExtensionFilePathToUrl(res.data.tarball_path)
const installExtras = await getInstallExtras(
res.data as Tables<"ext_publish"> & { metadata: ExtPublishMetadata }
)
return extensions
.upgradeStoreExtension(ext.identifier, data.tarball_path, installExtras)
.upgradeStoreExtension(ext.identifier, tarballUrl, installExtras)
.then((newExt) => {
toast.success(`${ext.name} Upgraded to ${newExt.version}`)
})
}
async function onExtItemInstall(ext: ExtensionStoreListItem) {
const { data, error, response } = await getExtensionsLatestPublishByIdentifier({
path: {
identifier: ext.identifier
}
})
if (error)
async function onExtItemInstall(ext: SBExt) {
const res = await supabaseAPI.getLatestExtPublish(ext.identifier)
if (res.error)
return toast.error("Fail to get latest extension", {
description: error.error
description: res.error.message
})
const installExtras = await getInstallExtras(data?.metadata)
const tarballUrl = res.data.tarball_path.startsWith("http")
? res.data.tarball_path
: supabaseAPI.translateExtensionFilePathToUrl(res.data.tarball_path)
const installExtras = await getInstallExtras(
res.data as Tables<"ext_publish"> & { metadata: ExtPublishMetadata }
)
const installDir = await getExtensionsFolder()
return extensions
.installFromTarballUrl(data.tarball_path, installDir, installExtras)
.installFromTarballUrl(tarballUrl, installDir, installExtras)
.then(() => toast.success(`Plugin ${ext.name} Installed`))
.then(() =>
postExtensionsIncrementDownloads({
body: {
identifier: ext.identifier,
version: ext.version
}
supabaseAPI.incrementDownloads({
identifier: ext.identifier,
version: ext.version
})
.then(({ error }) => {
if (error) {
console.error(error)
}
})
.catch(console.error)
)
}
@ -164,7 +126,7 @@
})
</script>
<svelte:window {onkeydown} />
<svelte:window on:keydown={onkeydown} />
{#snippet leftSlot()}
<Button
variant="outline"
@ -180,7 +142,7 @@
<CustomCommandInput
bind:ref={listviewInputRef}
autofocus
placeholder="Type / to focus"
placeholder="Type a command or search..."
leftSlot={leftSlot as Snippet}
bind:value={$appState.searchTerm}
onkeydown={(e) => {
@ -205,9 +167,7 @@
{ext}
installedVersion={$installedExtsMap[ext.identifier]}
isUpgradable={!!$upgradableExpsMap[ext.identifier]}
onSelect={() => {
onExtItemSelected(ext)
}}
onSelect={() => {}}
onUpgrade={() => onExtItemUpgrade(ext)}
onInstall={() => onExtItemInstall(ext)}
/>

View File

@ -1,47 +1,43 @@
import { appConfig, appState, extensions, installedStoreExts } from "@/stores"
import { goHome } from "@/utils/route"
// import { supabaseAPI } from "@/supabase"
import type { ExtensionStoreListItem, ExtPackageJsonExtra } from "@kksh/api/models"
import { appConfig, extensions, installedStoreExts } from "@/stores"
import { supabaseAPI } from "@/supabase"
import type { ExtPackageJsonExtra } from "@kksh/api/models"
import { isExtPathInDev, isUpgradable } from "@kksh/extension"
import { getExtensionsStoreList } from "@kksh/sdk"
import { toast } from "svelte-sonner"
import { SBExt } from "@kksh/supabase/models"
import { error } from "@sveltejs/kit"
import { derived, get, type Readable } from "svelte/store"
import type { PageLoad } from "./$types"
export const load: PageLoad = (): Promise<{
storeExtList: ExtensionStoreListItem[]
export const load: PageLoad = async (): Promise<{
storeExtList: SBExt[]
installedStoreExts: Readable<ExtPackageJsonExtra[]>
installedExtsMap: Readable<Record<string, string>>
upgradableExpsMap: Readable<Record<string, boolean>>
}> => {
appState.setFullScreenLoading(true)
return getExtensionsStoreList()
.then(({ data: storeExtList, error, response }) => {
storeExtList = storeExtList ?? []
if (error) {
toast.error(`Failed to load extension store: ${error} (${response.status})`)
goHome()
}
const storeExtsMap = Object.fromEntries(storeExtList.map((ext) => [ext.identifier, ext]))
const installedExtsMap = derived(installedStoreExts, ($exts) =>
Object.fromEntries($exts.map((ext) => [ext.kunkun.identifier, ext.version]))
)
const upgradableExpsMap = derived(installedStoreExts, ($exts) =>
Object.fromEntries(
$exts.map((ext) => {
const dbExt: ExtensionStoreListItem | undefined = storeExtsMap[ext.kunkun.identifier]
return [ext.kunkun.identifier, dbExt ? isUpgradable(dbExt, ext.version) : false]
})
)
)
return {
storeExtList,
installedStoreExts,
installedExtsMap,
upgradableExpsMap
}
})
.finally(() => {
appState.setFullScreenLoading(false)
})
const storeExtList = await supabaseAPI.getExtList()
// map identifier to extItem
const storeExtsMap = Object.fromEntries(storeExtList.map((ext) => [ext.identifier, ext]))
const _appConfig = get(appConfig)
// const installedStoreExts = derived(extensions, ($extensions) => {
// if (!_appConfig.extensionPath) return []
// return $extensions.filter((ext) => !isExtPathInDev(_appConfig.extensionPath!, ext.extPath))
// })
// map installed extension identifier to version
const installedExtsMap = derived(installedStoreExts, ($exts) =>
Object.fromEntries($exts.map((ext) => [ext.kunkun.identifier, ext.version]))
)
const upgradableExpsMap = derived(installedStoreExts, ($exts) =>
Object.fromEntries(
$exts.map((ext) => {
const dbExt: SBExt | undefined = storeExtsMap[ext.kunkun.identifier]
return [ext.kunkun.identifier, dbExt ? isUpgradable(dbExt, ext.version) : false]
})
)
)
return {
storeExtList,
installedStoreExts,
installedExtsMap,
upgradableExpsMap
}
}

View File

@ -2,8 +2,11 @@
import { getExtensionsFolder } from "@/constants.js"
import { i18n } from "@/i18n.js"
import { extensions, installedStoreExts } from "@/stores/extensions.js"
import { DBExtension, ExtensionStoreListItem, ExtPackageJson, ExtPublish } from "@kksh/api/models"
import { postExtensionsIncrementDownloads } from "@kksh/sdk"
import { supabaseAPI } from "@/supabase"
import { goBack } from "@/utils/route.js"
import { ExtPackageJson } from "@kksh/api/models"
import { ExtPublishMetadata } from "@kksh/supabase/models"
import type { Tables } from "@kksh/supabase/types"
import { Button } from "@kksh/svelte5"
import { cn } from "@kksh/svelte5/utils"
import { Constants } from "@kksh/ui"
@ -19,8 +22,10 @@
import { getInstallExtras } from "./helper"
const { data } = $props()
const extPublish = $derived(data.extPublish)
const ext = $derived(data.ext)
const extPublish: Tables<"ext_publish"> & { metadata: ExtPublishMetadata } = $derived(
data.extPublish
)
const ext: Tables<"extensions"> = $derived(data.ext)
const manifest = $derived(data.manifest)
const installedExt = storeDerived(installedStoreExts, ($e) => {
return $e.find((e) => e.kunkun.identifier === extPublish.identifier)
@ -72,29 +77,26 @@
}, 500)
})
const demoImages = $derived(extPublish.demo_images)
const demoImages = $derived(
extPublish.demo_images.map((src) => supabaseAPI.translateExtensionFilePathToUrl(src))
)
async function onInstallSelected() {
loading.install = true
const installExtras = await getInstallExtras(extPublish.metadata)
const tarballUrl = extPublish.tarball_path.startsWith("http")
? extPublish.tarball_path
: supabaseAPI.translateExtensionFilePathToUrl(extPublish.tarball_path)
const installExtras = await getInstallExtras(extPublish)
const installDir = await getExtensionsFolder()
return extensions
.installFromTarballUrl(extPublish.tarball_path, installDir, installExtras)
.installFromTarballUrl(tarballUrl, installDir, installExtras)
.then(() => toast.success(`Plugin ${extPublish.name} Installed`))
.then((loadedExt) => {
info(`Successfully installed ${extPublish.name}`)
postExtensionsIncrementDownloads({
body: {
identifier: extPublish.identifier,
version: extPublish.version
}
supabaseAPI.incrementDownloads({
identifier: extPublish.identifier,
version: extPublish.version
})
.then(({ error }) => {
if (error) {
console.error(error)
}
})
.catch(console.error)
showBtn.install = false
showBtn.uninstall = true
})
@ -109,8 +111,9 @@
function onUpgradeSelected() {
loading.upgrade = true
const tarballUrl = supabaseAPI.translateExtensionFilePathToUrl(extPublish.tarball_path)
return extensions
.upgradeStoreExtension(extPublish.identifier, extPublish.tarball_path)
.upgradeStoreExtension(extPublish.identifier, tarballUrl)
.then((newExt) => {
toast.success(
`${extPublish.name} Upgraded from ${$installedExt?.version} to ${newExt.version}`

View File

@ -1,57 +1,53 @@
import { appState } from "@/stores"
import { DBExtension, ExtPublish, KunkunExtManifest } from "@kksh/api/models"
import { getExtensionsByIdentifier, getExtensionsLatestPublishByIdentifier } from "@kksh/sdk"
import { extensions } from "@/stores"
import { supabaseAPI } from "@/supabase"
import { KunkunExtManifest, type ExtPackageJsonExtra } from "@kksh/api/models"
import { ExtPublishMetadata } from "@kksh/supabase/models"
import type { Tables } from "@kksh/supabase/types"
import { error } from "@sveltejs/kit"
import { toast } from "svelte-sonner"
import * as v from "valibot"
import type { PageLoad } from "./$types"
export const load: PageLoad = ({
export const load: PageLoad = async ({
params
}): Promise<{
extPublish: ExtPublish
ext: DBExtension
extPublish: Tables<"ext_publish"> & { metadata: ExtPublishMetadata }
ext: Tables<"extensions">
manifest: KunkunExtManifest
params: {
identifier: string
}
}> => {
appState.setFullScreenLoading(true)
return getExtensionsLatestPublishByIdentifier({
path: {
identifier: params.identifier
}
})
.then(async ({ data: extPublish, error: err, response }) => {
if (err || !extPublish) {
return error(response.status, {
message: "Failed to get extension publish"
})
}
const {
data: ext,
error: extError,
response: extRes
} = await getExtensionsByIdentifier({
path: {
identifier: params.identifier
}
})
if (extError || !ext) {
console.error(extError)
return error(extRes.status, {
message: extError.error || "Failed to get extension"
})
}
return {
extPublish: v.parse(ExtPublish, extPublish),
ext,
manifest: v.parse(KunkunExtManifest, extPublish.manifest),
params
}
const { error: dbError, data: extPublish } = await supabaseAPI.getLatestExtPublish(
params.identifier
)
const metadataParse = v.safeParse(ExtPublishMetadata, extPublish?.metadata ?? {})
if (dbError) {
return error(400, {
message: dbError.message
})
.finally(() => {
appState.setFullScreenLoading(false)
}
const metadata = metadataParse.success ? metadataParse.output : {}
const parseManifest = v.safeParse(KunkunExtManifest, extPublish.manifest)
if (!parseManifest.success) {
const errMsg = "Invalid extension manifest, you may need to upgrade your app."
toast.error(errMsg)
throw error(400, errMsg)
}
const { data: ext, error: extError } = await supabaseAPI.getExtension(params.identifier)
if (extError) {
return error(400, {
message: extError.message
})
}
return {
extPublish: { ...extPublish, metadata },
ext,
params,
manifest: parseManifest.output
}
}
export const csr = true

View File

@ -1,15 +1,15 @@
import type { ExtPublishMetadata } from "@kunkunapi/src/models"
import type { ExtPublishMetadata } from "@kksh/supabase/models"
import type { Tables } from "@kksh/supabase/types"
export async function getInstallExtras(extMetadata?: {
sourceType?: string
source?: string
}): Promise<{ overwritePackageJson?: string }> {
export async function getInstallExtras(
ext: Tables<"ext_publish"> & { metadata?: ExtPublishMetadata }
): Promise<{ overwritePackageJson?: string }> {
const extras: { overwritePackageJson?: string } = {}
if (extMetadata?.sourceType) {
if (extMetadata?.sourceType === "jsr") {
if (extMetadata?.source) {
if (ext.metadata?.sourceType) {
if (ext.metadata?.sourceType === "jsr") {
if (ext.metadata?.source) {
try {
const res = await fetch(`${extMetadata.source}/package.json`)
const res = await fetch(`${ext.metadata.source}/package.json`)
const pkgJsonContent = await res.text()
extras.overwritePackageJson = pkgJsonContent
} catch (error) {

View File

@ -1,15 +1,13 @@
<script lang="ts">
import DanceTransition from "@/components/dance/dance-transition.svelte"
import { i18n } from "@/i18n"
import { appConfig, appState, winExtMap } from "@/stores"
import { appConfig, winExtMap } from "@/stores"
import { helperAPI } from "@/utils/helper"
import { paste } from "@/utils/hotkey"
import { goBackOnEscape } from "@/utils/key"
import { decideKkrpcSerialization } from "@/utils/kkrpc"
import { goHome } from "@/utils/route"
import { positionToCssStyleString, positionToTailwindClasses } from "@/utils/style"
import { sleep } from "@/utils/time"
import { isInMainWindow } from "@/utils/window"
import { db } from "@kksh/api/commands"
import { CustomPosition, ThemeColor, type Position } from "@kksh/api/models"
import {
constructJarvisServerAPIWithPermissions,
@ -18,17 +16,10 @@
type IUiCustom
} from "@kksh/api/ui"
import { toast, type IUiCustomServer1, type IUiCustomServer2 } from "@kksh/api/ui/custom"
import { db } from "@kksh/drizzle"
import { Button } from "@kksh/svelte5"
import { cn } from "@kksh/ui/utils"
import type { IKunkunFullServerAPI } from "@kunkunapi/src/api/server"
import {
RECORD_EXTENSION_PROCESS_EVENT,
type IRecordExtensionProcessEvent
} from "@kunkunapi/src/events"
import { emitTo } from "@tauri-apps/api/event"
import { getCurrentWindow } from "@tauri-apps/api/window"
import { info } from "@tauri-apps/plugin-log"
import { goto } from "$app/navigation"
import { IframeParentIO, RPCChannel } from "kkrpc/browser"
import { ArrowLeftIcon, MoveIcon, RefreshCcwIcon, XIcon } from "lucide-svelte"
@ -37,7 +28,7 @@
let { data }: { data: PageData } = $props()
const { loadedExt, url, extPath, extInfoInDB } = data
let extSpawnedProcesses = $state<number[]>([])
const appWin = getCurrentWindow()
let iframeRef: HTMLIFrameElement
let uiControl = $state<{
iframeLoaded: boolean
@ -64,7 +55,7 @@
if (isInMainWindow()) {
goto(i18n.resolveRoute("/app/"))
} else {
data.win?.close()
appWin.close()
}
},
hideBackButton: async () => {
@ -116,25 +107,7 @@
const serverAPI: IKunkunFullServerAPI = constructJarvisServerAPIWithPermissions(
loadedExt.kunkun.permissions,
loadedExt.extPath,
{
recordSpawnedProcess: async (pid: number) => {
extSpawnedProcesses = [...extSpawnedProcesses, pid]
// winExtMap.registerProcess(appWin.label, pid)
const curWin = await getCurrentWindow()
await emitTo("main", RECORD_EXTENSION_PROCESS_EVENT, {
windowLabel: curWin.label,
pid
} satisfies IRecordExtensionProcessEvent)
// TODO: record process in a store
},
getSpawnedProcesses: () => Promise.resolve(extSpawnedProcesses),
paste: async () => {
await data.win?.hide()
await sleep(200)
return paste()
}
}
loadedExt.extPath
)
const serverAPI2 = {
...serverAPI,
@ -154,7 +127,7 @@
if (isInMainWindow()) {
goHome()
} else {
data.win?.close()
appWin.close()
}
}
@ -162,27 +135,16 @@
setTimeout(() => {
iframeRef.focus()
uiControl.iframeLoaded = true
appState.setFullScreenLoading(false)
}, 300)
}
onMount(() => {
appState.setFullScreenLoading(true)
setTimeout(() => {
data.win?.setFocus()
appWin.show()
}, 200)
if (iframeRef?.contentWindow) {
const io = new IframeParentIO(iframeRef.contentWindow)
const kkrpcSerialization = decideKkrpcSerialization(loadedExt)
info(
`Establishing kkrpc connection for ${loadedExt.kunkun.identifier} with serialization: ${kkrpcSerialization}`
)
const rpc = new RPCChannel(io, {
expose: serverAPI2,
serialization: {
version: kkrpcSerialization
}
})
const rpc = new RPCChannel(io, { expose: serverAPI2 })
} else {
toast.warning("iframeRef.contentWindow not available")
}
@ -195,7 +157,7 @@
})
onDestroy(() => {
winExtMap.unregisterExtensionFromWindow(data.win?.label ?? "")
winExtMap.unregisterExtensionFromWindow(appWin.label)
})
</script>
@ -208,7 +170,7 @@
onclick={onBackBtnClicked}
style={`${positionToCssStyleString(uiControl.backBtnPosition)}`}
>
{#if data.win?.label === "main"}
{#if appWin.label === "main"}
<ArrowLeftIcon class="w-4" />
{:else}
<XIcon class="w-4" />
@ -239,6 +201,7 @@
{/if}
<main class="h-screen">
<DanceTransition delay={300} autoHide={false} show={!uiControl.iframeLoaded} />
<iframe
bind:this={iframeRef}
class={cn("h-full", {

View File

@ -1,7 +1,7 @@
import { KunkunIframeExtParams } from "@/cmds/ext"
import { i18n } from "@/i18n"
import { db, unregisterExtensionWindow } from "@kksh/api/commands"
import type { Ext as ExtInfoInDB, ExtPackageJsonExtra } from "@kksh/api/models"
import { db } from "@kksh/drizzle"
import { loadExtensionManifestFromDisk } from "@kksh/extension"
import { error as svError } from "@sveltejs/kit"
import { join } from "@tauri-apps/api/path"

View File

@ -3,16 +3,15 @@
import { appState } from "@/stores/appState.js"
import { keys } from "@/stores/keys"
import { winExtMap } from "@/stores/winExtMap.js"
import { WatchEvent } from "@/types/fs.js"
import { helperAPI } from "@/utils/helper.js"
import { paste } from "@/utils/hotkey"
import { decideKkrpcSerialization } from "@/utils/kkrpc.js"
import {
emitReloadOneExtension,
listenToFileDrop,
listenToRefreshDevExt
} from "@/utils/tauri-events.js"
import { sleep } from "@/utils/time.js"
import { isInMainWindow } from "@/utils/window.js"
import { db } from "@kksh/api/commands"
import {
constructJarvisServerAPIWithPermissions,
type IApp,
@ -28,29 +27,19 @@
type IComponent,
type TemplateUiCommand
} from "@kksh/api/ui/template"
import { db } from "@kksh/drizzle"
import { Button, Form } from "@kksh/svelte5"
import { LoadingBar } from "@kksh/ui"
import { Templates } from "@kksh/ui/extension"
import { GlobalCommandPaletteFooter } from "@kksh/ui/main"
import type { IKunkunFullServerAPI } from "@kunkunapi/src/api/server"
import {
RECORD_EXTENSION_PROCESS_EVENT,
type IRecordExtensionProcessEvent
} from "@kunkunapi/src/events.js"
import { Channel, invoke } from "@tauri-apps/api/core"
import { emitTo, type UnlistenFn } from "@tauri-apps/api/event"
import type { UnlistenFn } from "@tauri-apps/api/event"
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"
import { getCurrentWindow } from "@tauri-apps/api/window"
import * as fs from "@tauri-apps/plugin-fs"
import { readTextFile } from "@tauri-apps/plugin-fs"
import { debug, info } from "@tauri-apps/plugin-log"
import { debug } from "@tauri-apps/plugin-log"
import { platform } from "@tauri-apps/plugin-os"
import { goto } from "$app/navigation"
import { RPCChannel, WorkerParentIO } from "kkrpc/browser"
import { onDestroy, onMount, tick } from "svelte"
import Inspect from "svelte-inspect-value"
import { type CommandEvent } from "tauri-plugin-shellx-api"
import * as v from "valibot"
const { data } = $props()
@ -61,22 +50,21 @@
let unlistenRefreshWorkerExt: UnlistenFn | undefined
let unlistenFileDrop: UnlistenFn | undefined
let worker: Worker | undefined
let listViewContent = $state<ListSchema.List | null>(null)
let formViewContent = $state<FormSchema.Form | null>(null)
let markdownViewContent = $state<MarkdownSchema | null>(null)
let listViewContent = $state<ListSchema.List>()
let formViewContent = $state<FormSchema.Form>()
let markdownViewContent = $state<MarkdownSchema>()
let extensionLoadingBar = $state(false) // whether extension called showLoadingBar
let pbar = $state<number | null>(null)
let loading = $state(false)
let searchTerm = $state("")
let searchBarPlaceholder = $state("")
let extSpawnedProcesses = $state<number[]>([])
const appWin = getCurrentWebviewWindow()
const loadingBar = $derived($appState.loadingBar || extensionLoadingBar)
let loaded = $state(false)
let listview: Templates.ListView | undefined = $state(undefined)
const _platform = platform()
let unlistenPkgJsonWatch: UnlistenFn | undefined
let curViewNodeName = $state<NodeNameEnum | FormNodeNameEnum | null>(null)
async function goBack() {
if (isInMainWindow()) {
goto(i18n.resolveRoute("/app/"))
@ -85,28 +73,23 @@
}
}
async function clearViewContent(keep?: "list" | "form" | "markdown") {
function clearViewContent(keep?: "list" | "form" | "markdown") {
if (keep !== "list") {
listViewContent = null
listViewContent = undefined
}
if (keep !== "form") {
formViewContent = null
formViewContent = undefined
}
if (keep !== "markdown") {
markdownViewContent = null
markdownViewContent = undefined
}
await tick()
// await sleep(3000)
}
const extUiAPI: IUiTemplate = {
async render(_view: IComponent<ListSchema.List | FormSchema.Form | MarkdownSchema>) {
// console.log("render nodeName", _view.nodeName)
// console.log("render", _view)
curViewNodeName = _view.nodeName
if (_view.nodeName === NodeNameEnum.List) {
await clearViewContent("list")
const parsedListViewRes = v.safeParse(ListSchema.List, _view)
async render(view: IComponent<ListSchema.List | FormSchema.Form | MarkdownSchema>) {
if (view.nodeName === NodeNameEnum.List) {
clearViewContent("list")
const parsedListViewRes = v.safeParse(ListSchema.List, view)
if (!parsedListViewRes.success) {
toast.error("Invalid List View", {
description: "See console for details"
@ -186,22 +169,20 @@
// } else {
// listViewContent = parsedListView
// }
} else if (_view.nodeName === FormNodeNameEnum.Form) {
listViewContent = null
// await clearViewContent("form")
// await tick()
const parsedForm = v.parse(FormSchema.Form, _view)
} else if (view.nodeName === FormNodeNameEnum.Form) {
listViewContent = undefined
clearViewContent("form")
const parsedForm = v.parse(FormSchema.Form, view)
formViewContent = parsedForm
// TODO: convert form to zod schema
// const zodSchema = convertFormToZod(parsedForm)
// formViewZodSchema = zodSchema
// formFieldConfig = buildFieldConfig(parsedForm)
} else if (_view.nodeName === NodeNameEnum.Markdown) {
await clearViewContent("markdown")
await tick()
markdownViewContent = v.parse(MarkdownSchema, _view)
} else if (view.nodeName === NodeNameEnum.Markdown) {
clearViewContent("markdown")
markdownViewContent = v.parse(MarkdownSchema, view)
} else {
toast.error(`Unsupported view type: ${_view.nodeName}`)
toast.error(`Unsupported view type: ${view.nodeName}`)
}
},
async showLoadingBar(loading: boolean) {
@ -218,6 +199,7 @@
searchTerm = term
},
async setSearchBarPlaceholder(placeholder: string) {
console.log("setSearchBarPlaceholder", placeholder)
searchBarPlaceholder = placeholder
},
async goBack() {
@ -239,25 +221,7 @@
worker = new Worker(blobURL)
const serverAPI: IKunkunFullServerAPI = constructJarvisServerAPIWithPermissions(
loadedExt.kunkun.permissions,
loadedExt.extPath,
{
recordSpawnedProcess: async (pid: number) => {
extSpawnedProcesses = [...extSpawnedProcesses, pid]
// winExtMap.registerProcess(appWin.label, pid)
const curWin = await getCurrentWindow()
await emitTo("main", RECORD_EXTENSION_PROCESS_EVENT, {
windowLabel: curWin.label,
pid
} satisfies IRecordExtensionProcessEvent)
// TODO: record process in a store
},
getSpawnedProcesses: () => Promise.resolve(extSpawnedProcesses),
paste: async () => {
await appWin.hide()
await sleep(200)
return paste()
}
}
loadedExt.extPath
)
const serverAPI2 = {
...serverAPI,
@ -270,16 +234,10 @@
language: () => Promise.resolve("en")
} satisfies IApp
}
const io = new WorkerParentIO(worker)
const kkrpcSerialization = decideKkrpcSerialization(loadedExt)
info(
`Establishing kkrpc connection for ${loadedExt.kunkun.identifier} with serialization: ${kkrpcSerialization}`
)
const rpc = new RPCChannel<typeof serverAPI2, TemplateUiCommand>(io, {
expose: serverAPI2,
serialization: {
version: kkrpcSerialization
}
expose: serverAPI2
})
workerAPI = rpc.getAPI()
await workerAPI.load()
@ -292,25 +250,25 @@
}
})
// function onPkgJsonChange(evt: fs.WatchEvent) {
// const parsed = v.safeParse(WatchEvent, evt)
// if (parsed.success) {
// if (
// parsed.output.type.modify.kind === "data" &&
// parsed.output.type.modify.mode === "content" &&
// parsed.output.paths.includes(data.pkgJsonPath)
// ) {
// console.log("pkgJson changed", parsed.output.paths)
// // emit event to reload extension commands
// emitReloadOneExtension(loadedExt.extPath)
// }
// }
// }
function onPkgJsonChange(evt: fs.WatchEvent) {
const parsed = v.safeParse(WatchEvent, evt)
if (parsed.success) {
if (
parsed.output.type.modify.kind === "data" &&
parsed.output.type.modify.mode === "content" &&
parsed.output.paths.includes(data.pkgJsonPath)
) {
console.log("pkgJson changed", parsed.output.paths)
// emit event to reload extension commands
emitReloadOneExtension(loadedExt.extPath)
}
}
}
onMount(async () => {
setTimeout(() => {
appState.setLoadingBar(true)
appWin.show().then(() => appWin.setFocus())
appWin.show()
}, 100)
unlistenRefreshWorkerExt = await listenToRefreshDevExt(() => {
debug("Refreshing Worker Extension")
@ -326,9 +284,10 @@
appState.setLoadingBar(false)
loaded = true
}, 500)
// fs.watch(data.pkgJsonPath, onPkgJsonChange).then((unlisten) => {
// unlistenPkgJsonWatch = unlisten
// })
console.log("watching", data.pkgJsonPath)
fs.watch(data.pkgJsonPath, onPkgJsonChange).then((unlisten) => {
unlistenPkgJsonWatch = unlisten
})
})
onDestroy(() => {
@ -338,8 +297,6 @@
winExtMap.unregisterExtensionFromWindow(appWin.label)
extensionLoadingBar = false
appState.setActionPanel(undefined)
appState.setDefaultAction(null)
appState.setActionPanel(undefined)
})
$effect(() => {
@ -379,8 +336,7 @@
{#if loadingBar}
<LoadingBar class="fixed left-0 top-0 w-full" color="white" />
{/if}
{#if curViewNodeName === NodeNameEnum.List && listViewContent}
{#if loaded && listViewContent !== undefined}
<Templates.ListView
bind:inputRef={listviewInputRef}
bind:searchTerm
@ -402,18 +358,26 @@
onSearchTermChange={(searchTerm: string) => {
workerAPI?.onSearchTermChange(searchTerm)
}}
onHighlightedItemChanged={(item: ListSchema.Item) => {
if (item.defaultAction) {
appState.setDefaultAction(item.defaultAction)
} else if (listViewContent?.defaultAction) {
appState.setDefaultAction(listViewContent.defaultAction)
onHighlightedItemChanged={(value: string) => {
// workerAPI?.onHighlightedListItemChanged(value)
// if (listViewContent?.defaultAction) {
// appState.setDefaultAction(listViewContent.defaultAction)
// }
// if (listViewContent?.actions) {
// appState.setActionPanel(listViewContent.actions)
// }
try {
const parsedItem = v.parse(ListSchema.Item, JSON.parse(value))
if (parsedItem.defaultAction) {
appState.setDefaultAction(parsedItem.defaultAction)
}
if (parsedItem.actions) {
appState.setActionPanel(parsedItem.actions)
}
workerAPI?.onHighlightedListItemChanged(parsedItem.value)
} catch (error) {
console.error(error)
}
if (item.actions) {
appState.setActionPanel(item.actions)
} else if (listViewContent?.actions) {
appState.setActionPanel(listViewContent.actions)
}
workerAPI?.onHighlightedListItemChanged(item.value)
}}
>
{#snippet footer()}
@ -431,8 +395,7 @@
/>
{/snippet}
</Templates.ListView>
{/if}
{#if curViewNodeName === FormNodeNameEnum.Form && formViewContent}
{:else if loaded && formViewContent !== undefined}
<Templates.FormView
{formViewContent}
{pbar}
@ -442,7 +405,6 @@
workerAPI?.onFormSubmit(formData)
}}
/>
{/if}
{#if curViewNodeName === NodeNameEnum.Markdown && markdownViewContent}
{:else if loaded && markdownViewContent !== undefined}
<Templates.MarkdownView {markdownViewContent} onGoBack={goBack} />
{/if}

View File

@ -1,11 +1,10 @@
import { KunkunTemplateExtParams } from "@/cmds/ext"
import { i18n } from "@/i18n"
import type { ExtPackageJsonExtra } from "@kksh/api/models"
import { db } from "@kksh/drizzle"
import { db, unregisterExtensionWindow } from "@kksh/api/commands"
import type { Ext as ExtInfoInDB, ExtPackageJsonExtra } from "@kksh/api/models"
import { loadExtensionManifestFromDisk } from "@kksh/extension"
import { error as sbError, error as svError } from "@sveltejs/kit"
import { join } from "@tauri-apps/api/path"
import { getCurrentWindow } from "@tauri-apps/api/window"
import { exists, readTextFile } from "@tauri-apps/plugin-fs"
import { error } from "@tauri-apps/plugin-log"
import { goto } from "$app/navigation"
@ -15,16 +14,15 @@ import type { PageLoad } from "./$types"
export const load: PageLoad = async ({ url }) => {
// both query parameter must exist
const rawKunkunTemplateExtParams = localStorage.getItem("kunkun-template-ext-params")
if (!rawKunkunTemplateExtParams) {
toast.error("Invalid extension path or url")
return svError(404, "Invalid extension path or url")
}
const json = JSON.parse(rawKunkunTemplateExtParams)
const parsed = v.safeParse(KunkunTemplateExtParams, json)
const parsed = v.safeParse(KunkunTemplateExtParams, JSON.parse(rawKunkunTemplateExtParams))
if (!parsed.success) {
getCurrentWindow().show()
console.error(v.flatten<typeof KunkunTemplateExtParams>(parsed.issues))
toast.error("Fail to parse extension params from local storage", {
description: `${v.flatten<typeof KunkunTemplateExtParams>(parsed.issues)}`
})

View File

@ -8,6 +8,7 @@
import { goto } from "$app/navigation"
import { ArrowRightIcon } from "lucide-svelte"
import { onMount } from "svelte"
import { fade } from "svelte/transition"
import { whereIsCommand } from "tauri-plugin-shellx-api"
import { Step } from "./steps"
@ -25,22 +26,18 @@
}
$effect(() => {
if (step > Step.GeneralSettings) {
if (step === Step.DenoInstall) {
if (denoPath) {
step++
}
} else if (step === Step.FFmpegInstall) {
if (ffmpegPath) {
step++
}
} else if (step > Step.FFmpegInstall) {
appConfig.setOnBoarded(true)
goto(i18n.resolveRoute("/app"))
}
// if (step === Step.DenoInstall) {
// if (denoPath) {
// step++
// }
// } else if (step === Step.FFmpegInstall) {
// if (ffmpegPath) {
// step++
// }
// } else if (step > Step.FFmpegInstall) {
// appConfig.setOnBoarded(true)
// goto(i18n.resolveRoute("/app"))
// }
})
</script>

View File

@ -1,137 +0,0 @@
<script lang="ts">
import * as m from "@/paraglide/messages"
import { appsLoader } from "@/stores"
import { SearchPath } from "@kksh/api/models"
import { Button, Input, Table } from "@kksh/svelte5"
import { Form } from "@kksh/ui"
import * as dialog from "@tauri-apps/plugin-dialog"
import * as fs from "@tauri-apps/plugin-fs"
import { appConfig } from "$lib/stores/appConfig"
import { Inspect } from "svelte-inspect-value"
import { toast } from "svelte-sonner"
import SuperDebug, { defaults, superForm } from "sveltekit-superforms"
import { valibot, valibotClient } from "sveltekit-superforms/adapters"
import { open } from "tauri-plugin-shellx-api"
import * as v from "valibot"
export const SearchPathFormSchema = v.object({
path: v.pipe(v.string(), v.minLength(1)),
depth: v.optional(v.number(), 1)
})
const form = superForm(defaults(valibot(SearchPathFormSchema)), {
validators: valibotClient(SearchPathFormSchema),
SPA: true,
async onUpdate({ form, cancel }) {
if (!form.valid) {
return
}
const { path, depth } = form.data
if (!(await fs.exists(path))) {
return toast.error("Path does not exist")
}
appConfig.addAppSearchPath({ path, depth })
toast.success("Search Path Added")
appsLoader.init()
cancel()
}
})
const { form: formData, enhance, errors } = form
async function pickSearchPath() {
const result = await dialog.open({
directory: true
})
if (result) {
$formData.path = result
}
}
</script>
<main class="container flex flex-col space-y-2">
<h1 class="text-2xl font-bold">{m.settings_app_search_paths_title()}</h1>
{#if $appConfig.developerMode}
<Inspect name="Extra App Search Paths" value={$appConfig.appSearchPaths} />
{/if}
<form method="POST" use:enhance>
<Form.Field {form} name="path">
<Form.Control>
{#snippet children({ props })}
<Form.Label>{m.settings_app_search_paths_table_col_search_path()}</Form.Label>
<div class="flex items-center gap-1">
<Input
{...props}
disabled
bind:value={$formData.path}
placeholder={m.settings_app_search_paths_table_col_search_path()}
/>
<Form.Button class="my-1" onclick={pickSearchPath}>Pick</Form.Button>
</div>
{/snippet}
</Form.Control>
<Form.FieldErrors />
</Form.Field>
<Form.Field {form} name="depth">
<Form.Control>
{#snippet children({ props })}
<Form.Label>{m.settings_app_search_paths_table_col_depth()}</Form.Label>
<Input
{...props}
type="number"
bind:value={$formData.depth}
placeholder={m.settings_app_search_paths_table_col_depth()}
/>
{/snippet}
</Form.Control>
<Form.FieldErrors />
</Form.Field>
<Button class="w-full" type="submit">
{m.settings_app_search_paths_add_app_search_path()}
</Button>
</form>
{#if $appConfig.developerMode}
<SuperDebug data={$formData} />
{/if}
<Table.Root>
<Table.Header>
<Table.Row>
<Table.Head>{m.settings_app_search_paths_table_col_search_path()}</Table.Head>
<Table.Head class="text-center">
{m.settings_app_search_paths_table_col_depth()}
</Table.Head>
<Table.Head class="text-center">
{m.settings_app_search_paths_table_col_actions()}
</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#each $appConfig.appSearchPaths as appSearchPath, i (i)}
<Table.Row>
<Table.Cell
class="cursor-pointer font-medium"
onclick={() => {
open(appSearchPath.path)
}}
>
<code>{appSearchPath.path}</code>
</Table.Cell>
<Table.Cell class="text-center">{appSearchPath.depth}</Table.Cell>
<Table.Cell class="text-center">
<Button
variant="destructive"
onclick={() => {
appConfig.removeAppSearchPath(appSearchPath)
toast.error("Search Path Removed")
appsLoader.init()
}}
>
Remove
</Button>
</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
</main>

View File

@ -4,7 +4,7 @@
import { goHome } from "@/utils/route"
import { Button, Sidebar } from "@kksh/svelte5"
import { Constants } from "@kksh/ui"
import { ArrowLeftIcon, FolderSearch } from "lucide-svelte"
import { ArrowLeftIcon } from "lucide-svelte"
import Blocks from "lucide-svelte/icons/blocks"
import Cog from "lucide-svelte/icons/cog"
import FileCode2 from "lucide-svelte/icons/file-code-2"
@ -19,15 +19,10 @@
icon: Cog
},
{
title: m.settings_menu_app_search_paths(),
url: i18n.resolveRoute("/app/settings/app-search-paths"),
icon: FolderSearch
title: m.settings_menu_developer(),
url: i18n.resolveRoute("/app/settings/developer"),
icon: SquareTerminal
},
// {
// title: m.settings_menu_developer(),
// url: i18n.resolveRoute("/app/settings/developer"),
// icon: SquareTerminal
// },
{
title: m.settings_menu_extensions(),
url: i18n.resolveRoute("/app/settings/extensions"),

View File

@ -1,6 +1,6 @@
<script lang="ts">
import * as m from "@/paraglide/messages"
import { db } from "@kksh/drizzle"
import { db } from "@kksh/api/commands"
import { loadExtensionManifestFromDisk } from "@kksh/extension"
import { Button, Dialog, Table } from "@kksh/svelte5"
import { join } from "@tauri-apps/api/path"

View File

@ -1,124 +0,0 @@
<script lang="ts">
import {
getAllCmds,
getAllExtensions,
getExtensionDataById,
getUniqueExtensionByIdentifier,
getUniqueExtensionByPath,
searchExtensionData,
updateCmdByID
} from "@kksh/drizzle/api"
import * as schema from "@kksh/drizzle/schema"
import { Button, Input } from "@kksh/svelte5"
import { CmdTypeEnum, Ext } from "@kunkunapi/src/models/extension"
import { SearchModeEnum, SQLSortOrderEnum } from "@kunkunapi/src/models/sql"
// import * as orm from "drizzle-orm"
import { Inspect } from "svelte-inspect-value"
import { toast } from "svelte-sonner"
import * as v from "valibot"
let searchText = $state("")
/* eslint-disable */
let data: any = $state(null)
let inspectTitle = $state("")
</script>
<main class="container space-y-2">
<Button
onclick={async () => {
getAllCmds()
.then((cmds) => {
console.log(cmds)
data = cmds
inspectTitle = "All Commands"
})
.catch((e) => {
console.error(e)
toast.error("Failed to get all commands", {
description: "See console for more details"
})
})
}}
>
Get All Commands
</Button>
<Button
onclick={() => {
getAllExtensions()
.then((exts) => {
data = exts
inspectTitle = "All Extensions"
})
.catch((e) => {
console.error(e)
toast.error("Failed to get all extensions", {
description: "See console for more details"
})
})
}}
>
Get All Extensions
</Button>
<Button
onclick={async () => {
// get all extensions with path not null
const exts = await getAllExtensions()
for (const ext of exts) {
if (ext.path === null) continue
const _ext = await getUniqueExtensionByIdentifier(ext.identifier)
console.log(_ext)
if (ext.path) {
const __ext = await getUniqueExtensionByPath(ext.path)
console.log(__ext)
}
}
// data = exts
}}
>
Get Unique Extension By Identifier and Path
</Button>
<!-- <Button
onclick={async () => {
updateCmdByID({
cmdId: 1,
name: "google",
cmdType: CmdTypeEnum.QuickLink,
data: `{"link":"https://google.com/search?query={argument}","icon":{"type":"remote-url","value":"https://google.com/favicon.ico","invert":false}}`,
enabled: true
})
}}
>
Update Command By ID
</Button> -->
<Button
onclick={async () => {
const _data = await getExtensionDataById(1, ["search_text", "data"])
data = _data
inspectTitle = "Extension Data"
}}
>
Get Extension Data By ID
</Button>
<form
class="flex gap-1"
onsubmit={async (e) => {
e.preventDefault()
const _data = await searchExtensionData({
extId: 1,
searchMode: SearchModeEnum.FTS,
searchText: searchText,
orderByCreatedAt: SQLSortOrderEnum.Desc,
limit: 10,
fields: ["search_text", "data"]
})
console.log(_data)
data = _data
inspectTitle = "Search Results"
}}
>
<Input class="" bind:value={searchText} placeholder="Search Text" />
<Button class="" type="submit">Search Extension Data</Button>
</form>
<Inspect name={inspectTitle} value={data} expandLevel={2} />
</main>

View File

@ -6,7 +6,6 @@
import { Constants } from "@kksh/ui"
import { ArrowLeftIcon } from "lucide-svelte"
import AppWindow from "lucide-svelte/icons/app-window"
import DB from "lucide-svelte/icons/database"
import Loader from "lucide-svelte/icons/loader"
import Network from "lucide-svelte/icons/network"
@ -26,11 +25,6 @@
title: m.troubleshooters_sidebar_mdns_debugger_title(),
url: i18n.resolveRoute("/app/troubleshooters/mdns-debugger"),
icon: Network
},
{
title: "ORM",
url: i18n.resolveRoute("/app/troubleshooters/orm"),
icon: DB
}
]
let currentItem = $state(items.find((item) => window.location.pathname === item.url))

View File

@ -1,11 +1,11 @@
<script lang="ts">
import { Layouts } from "@kksh/ui"
import { getCurrentWindow } from "@tauri-apps/api/window"
import { onMount } from "svelte"
let { data } = $props()
onMount(() => {
data.win?.show().then(() => data.win?.setFocus())
onMount(async () => {
const mainWin = await getCurrentWindow()
mainWin.show()
})
</script>

View File

@ -86,18 +86,12 @@ const config: Config = {
"caret-blink": {
"0%,70%,100%": { opacity: "1" },
"20%,50%": { opacity: "0" }
},
"border-beam": {
"100%": {
"offset-distance": "100%"
}
}
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
"caret-blink": "caret-blink 1.25s ease-out infinite",
"border-beam": "border-beam calc(var(--duration)*1s) infinite linear"
"caret-blink": "caret-blink 1.25s ease-out infinite"
}
}
},

View File

@ -9,8 +9,7 @@
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true
"moduleResolution": "bundler"
},
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
// except $lib which is handled by https://kit.svelte.dev/docs/configuration#files

View File

@ -14,7 +14,7 @@ export default defineConfig(async () => ({
clearScreen: false,
// 2. tauri expects a fixed port, fail if that port is not available
server: {
port: 1566,
port: 1420,
strictPort: true,
host: host || false,
hmr: host

2476
deno.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -11,47 +11,46 @@
"format": "prettier --write \"**/*.{ts,tsx,md,svelte}\""
},
"devDependencies": {
"@ianvs/prettier-plugin-sort-imports": "^4.4.1",
"@ianvs/prettier-plugin-sort-imports": "^4.4.0",
"@kksh/api": "workspace:*",
"prettier": "^3.5.2",
"prettier-plugin-svelte": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"svelte": "^5.20.5",
"svelte-check": "^4.1.4",
"turbo": "^2.4.4",
"typescript": "^5.0.0",
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.2",
"prettier-plugin-tailwindcss": "^0.6.9",
"svelte": "^5.16.6",
"svelte-check": "^4.1.1",
"turbo": "^2.3.4",
"typescript": "5.7.2",
"verify-package-export": "^0.0.3"
},
"packageManager": "pnpm@10.7.0",
"packageManager": "pnpm@9.15.4",
"engines": {
"node": ">=22"
},
"dependencies": {
"@changesets/cli": "^2.28.1",
"@hey-api/client-fetch": "^0.8.3",
"@changesets/cli": "^2.27.11",
"@iconify/svelte": "^4.2.0",
"@kksh/sdk": "^0.0.3",
"@supabase/supabase-js": "^2.49.1",
"@tauri-apps/api": "^2.3.0",
"@tauri-apps/cli": "^2.3.1",
"@supabase/supabase-js": "^2.48.0",
"@tauri-apps/api": "^2.2.0",
"@tauri-apps/cli": "^2.2.2",
"@tauri-apps/plugin-deep-link": "^2.2.0",
"@tauri-apps/plugin-dialog": "^2.2.0",
"@tauri-apps/plugin-fs": "^2.2.0",
"@tauri-apps/plugin-global-shortcut": "^2.2.0",
"@tauri-apps/plugin-http": "^2.3.0",
"@tauri-apps/plugin-log": "^2.2.3",
"@tauri-apps/plugin-notification": "^2.2.1",
"@tauri-apps/plugin-http": "^2.2.0",
"@tauri-apps/plugin-log": "^2.2.0",
"@tauri-apps/plugin-notification": "^2.2.0",
"@tauri-apps/plugin-os": "^2.2.0",
"@tauri-apps/plugin-process": "2.2.0",
"@tauri-apps/plugin-shell": "^2.2.0",
"@tauri-apps/plugin-store": "^2.2.0",
"@tauri-apps/plugin-updater": "^2.5.1",
"supabase": "^2.15.8",
"@tauri-apps/plugin-updater": "^2.3.1",
"supabase": "^2.2.1",
"tauri-plugin-keyring-api": "workspace:*",
"tauri-plugin-network-api": "workspace:*",
"tauri-plugin-shellx-api": "^2.0.14",
"tauri-plugin-system-info-api": "workspace:*",
"valibot": "^1.0.0",
"zod": "^3.24.2"
"valibot": "^1.0.0-beta.11",
"zod": "^3.24.1"
},
"workspaces": [
"apps/*",

View File

@ -1,17 +1,5 @@
# @kksh/api
## 0.1.5
### Patch Changes
- Add clipboard.paste() API
## 0.1.4
### Patch Changes
- Add killPid extension API
## 0.1.2
### Patch Changes
@ -53,16 +41,3 @@
### Patch Changes
- More Icon Options
## 0.1.6
### Patch Changes
- Upgrade kkrpc to 0.2.1, which uses superjson for serialization
## 0.1.7
### Patch Changes
- Upgrade kkrpc to 0.2.2, supports both json and superjson serialization, for backward compatibility
- The previous version breaks extension compatibility.

View File

@ -1,6 +1,6 @@
{
"name": "@kksh/api",
"version": "0.1.7",
"version": "0.1.3",
"type": "module",
"repository": {
"type": "git",
@ -65,17 +65,17 @@
"@tauri-apps/plugin-store": "^2.2.0",
"@tauri-apps/plugin-updater": "^2.3.0",
"@tauri-apps/plugin-upload": "^2.2.1",
"kkrpc": "^0.2.2",
"kkrpc": "^0.1.1",
"lodash": "^4.17.21",
"minimatch": "^10.0.1",
"node-fetch": "^3.3.2",
"semver": "^7.6.3",
"svelte-sonner": "^0.3.28",
"tauri-api-adapter": "^0.3.27",
"tauri-api-adapter": "^0.3.20",
"tauri-plugin-network-api": "2.0.5",
"tauri-plugin-shellx-api": "^2.0.16",
"tauri-plugin-shellx-api": "^2.0.14",
"tauri-plugin-system-info-api": "2.0.8",
"valibot": "^1.0.0"
"valibot": "^1.0.0-beta.10"
},
"files": [
"src",

View File

@ -14,23 +14,26 @@ import type {
writeFile,
writeTextFile
} from "@tauri-apps/plugin-fs"
import type { IShell as IShell1, IPath as ITauriPath } from "tauri-api-adapter"
import type {
IClipboard as _IClipboard,
IShell as IShell1,
IPath as ITauriPath
} from "tauri-api-adapter"
Child,
ChildProcess,
CommandEvents,
hasCommand,
InternalSpawnOptions,
IOPayload,
likelyOnWindows,
OutputEvents,
SpawnOptions
} from "tauri-plugin-shellx-api"
import { EventEmitter, open as shellxOpen } from "tauri-plugin-shellx-api"
import * as v from "valibot"
import { KV, type JarvisExtDB } from "../commands/db"
import type { fileSearch } from "../commands/fileSearch"
import { type AppInfo } from "../models/apps"
import { type ExtData } from "../models/extension"
import { ExtDataField, SearchMode, SQLSortOrder } from "../models/sql"
import type { LightMode, Position, Radius, ThemeColor } from "../models/styles"
import type { DenoSysOptions } from "../permissions/schema"
export type IClipboard = _IClipboard & {
paste: (options?: {}) => Promise<void>
}
type PromiseWrap<T extends (...args: any[]) => any> = (
...args: Parameters<T>
) => Promise<ReturnType<T>>
@ -155,34 +158,23 @@ export interface IUiCustom {
}
export interface IDb {
add: (data: { data: string; dataType?: string; searchText?: string }) => Promise<void>
delete: (dataId: number) => Promise<void>
search: (searchParams: {
dataId?: number
searchMode?: SearchMode
dataType?: string
searchText?: string
afterCreatedAt?: Date
beforeCreatedAt?: Date
limit?: number
orderByCreatedAt?: SQLSortOrder
orderByUpdatedAt?: SQLSortOrder
fields?: ExtDataField[]
}) => Promise<ExtData[]>
retrieveAll: (options: { fields?: ExtDataField[] }) => Promise<ExtData[]>
retrieveAllByType: (dataType: string) => Promise<ExtData[]>
deleteAll: () => Promise<void>
update: (data: { dataId: number; data: string; searchText?: string }) => Promise<void>
add: typeof JarvisExtDB.prototype.add
delete: typeof JarvisExtDB.prototype.delete
search: typeof JarvisExtDB.prototype.search
retrieveAll: typeof JarvisExtDB.prototype.retrieveAll
retrieveAllByType: typeof JarvisExtDB.prototype.retrieveAllByType
deleteAll: typeof JarvisExtDB.prototype.deleteAll
update: typeof JarvisExtDB.prototype.update
}
/**
* A key-value store built on top of the Database API (based on sqlite)
*/
export interface IKV {
get: <T = string>(key: string) => Promise<T | null | undefined>
set: (key: string, value: string) => Promise<void>
exists: (key: string) => Promise<boolean>
delete: (key: string) => Promise<void>
get: typeof KV.prototype.get
set: typeof KV.prototype.set
exists: typeof KV.prototype.exists
delete: typeof KV.prototype.delete
}
export interface IFs {

View File

@ -1,18 +0,0 @@
import { constructClipboardApi as _constructClipboardApi } from "tauri-api-adapter"
import { type ClipboardPermission as _ClipboardPermission } from "tauri-api-adapter/permissions"
import type { ClipboardPermission } from "../../permissions/schema"
import { checkPermission } from "../../utils/permission-check"
import type { IClipboard } from "../client"
export function constructClipboardApi(
permissions: ClipboardPermission[],
paste: (options?: {}) => Promise<void>
): IClipboard {
return {
..._constructClipboardApi(permissions.filter((p) => p !== "clipboard:paste")), // this constructor has no paste API
paste: (options?: {}) => {
checkPermission(permissions, ["clipboard:paste"])
return paste(options)
}
}
}

View File

@ -1,4 +1,5 @@
import {
constructClipboardApi,
constructDialogApi,
constructFetchApi,
// constructFsApi, // a local constructFsApi is defined
@ -10,7 +11,7 @@ import {
// constructShellApi, // a local custom constructShellApi is defined
constructSystemInfoApi,
constructUpdownloadApi,
// type IClipboard,
type IClipboard,
type IDialog,
type IFetchInternal,
type ILogger,
@ -36,6 +37,7 @@ import {
type SystemInfoPermission,
type UpdownloadPermission
} from "tauri-api-adapter/permissions"
import type { IEvent, IFs, IOpen, ISecurity, ISystem, IToast, IUtils } from "../../api/client"
import type { IUiCustomServer1 } from "../../api/server-types"
import {
AllKunkunPermission,
@ -48,8 +50,6 @@ import {
type ShellPermissionScoped,
type SystemPermission
} from "../../permissions"
import type { IClipboard, IEvent, IFs, IOpen, ISecurity, ISystem, IToast, IUtils } from "../client"
import { constructClipboardApi } from "./clipboard"
// import type { IDbServer } from "./db"
import { constructEventApi } from "./event"
import { constructFsApi } from "./fs"
@ -126,19 +126,13 @@ export type IKunkunFullServerAPI = {
*/
export function constructJarvisServerAPIWithPermissions(
permissions: AllPermissions[],
extPath: string,
customFunctions: {
recordSpawnedProcess: (pid: number) => Promise<void>
getSpawnedProcesses: () => Promise<number[]>
paste: (options?: {}) => Promise<void>
}
extPath: string
): IKunkunFullServerAPI {
return {
clipboard: constructClipboardApi(
getStringPermissions(permissions).filter((p) =>
p.startsWith("clipboard:")
) as ClipboardPermission[],
customFunctions.paste
) as ClipboardPermission[]
),
fetch: constructFetchApi(
getStringPermissions(permissions).filter((p) => p.startsWith("fetch:")) as FetchPermission[]
@ -199,9 +193,7 @@ export function constructJarvisServerAPIWithPermissions(
p.permission.startsWith("shell:")
)
],
extPath,
customFunctions.recordSpawnedProcess,
customFunctions.getSpawnedProcesses
extPath
),
iframeUi: constructIframeUiApi(),
utils: constructUtilsApi(),

View File

@ -2,7 +2,6 @@ import { emitKillProcessEvent } from "@kksh/api/events"
import { Channel, invoke } from "@tauri-apps/api/core"
import { emitTo } from "@tauri-apps/api/event"
import { getCurrentWindow } from "@tauri-apps/api/window"
import { toast } from "svelte-sonner"
import {
hasCommand,
whereIsCommand,
@ -11,7 +10,6 @@ import {
type InternalSpawnOptions,
type IOPayload
} from "tauri-plugin-shellx-api"
import * as shell from "tauri-plugin-shellx-api"
import type { DenoRunConfig } from "../../api/client"
import type { IShellServer } from "../../api/server-types"
import { RECORD_EXTENSION_PROCESS_EVENT, type IRecordExtensionProcessEvent } from "../../events"
@ -75,14 +73,13 @@ async function verifyShellCmdPermission(
*/
export function constructShellApi(
permissions: (ShellPermissionScoped | ShellPermission)[],
extPath: string,
recordSpawnedProcess: (pid: number) => Promise<void>,
getSpawnedProcesses: () => Promise<number[]>
extPath: string
): IShellServer {
const stringPermissiongs = permissions.filter((p) => typeof p === "string") as ShellPermission[]
const objectPermissions = permissions.filter(
(p) => typeof p !== "string"
) as ShellPermissionScoped[]
async function execute(
program: string,
args: string[],
@ -102,19 +99,14 @@ export function constructShellApi(
options: options
})
}
async function kill(pid: number) {
if (!stringPermissiongs.some((p) => ShellPermissionMap.kill.includes(p))) {
function kill(pid: number) {
if (!stringPermissiongs.some((p) => ShellPermissionMap.kill.includes(p)))
return Promise.reject(
new Error(`Permission denied. Requires one of ${ShellPermissionMap.kill}`)
)
}
const pids = await getSpawnedProcesses()
if (!pids.includes(pid)) {
return Promise.reject(new Error(`Process ${pid} not spawned by this extension`))
}
return invoke<void>("plugin:shellx|kill", {
cmd: "killChild",
pid
pid: pid
}).then(() => {
emitKillProcessEvent(pid)
})
@ -154,24 +146,13 @@ export function constructShellApi(
options: InternalSpawnOptions,
cb: (evt: CommandEvent<O>) => void
) {
await verifyShellCmdPermission(
ShellPermissionMap.rawSpawn,
objectPermissions,
program,
args
).catch((err) => {
toast.error("Permission denied", {
description: err.message
})
console.error("rawSpawn permission denied", err)
throw err
})
await verifyShellCmdPermission(ShellPermissionMap.rawSpawn, objectPermissions, program, args)
const onEvent = new Channel<CommandEvent<O>>()
onEvent.onmessage = cb
return invoke<number>("plugin:shellx|spawn", {
program,
args,
options,
program: program,
args: args,
options: options,
onEvent
})
}
@ -229,14 +210,6 @@ export function constructShellApi(
return likelyOnWindows()
}
function killPid(pid: number) {
if (!stringPermissiongs.some((p) => ShellPermissionMap.killPid.includes(p)))
return Promise.reject(
new Error(`Permission denied. Requires one of ${ShellPermissionMap.killPid}`)
)
return shell.killPid(pid)
}
return {
whereIsCommand(command: string): Promise<string | null> {
const cleanedCommand = command.trim().split(" ")[0]
@ -245,7 +218,17 @@ export function constructShellApi(
}
return whereIsCommand(cleanedCommand).then((res) => (res === "" ? null : res))
},
recordSpawnedProcess,
async recordSpawnedProcess(pid: number): Promise<void> {
// get window label
const curWin = await getCurrentWindow()
console.log("recordSpawnedProcess", pid, curWin.label)
await emitTo("main", RECORD_EXTENSION_PROCESS_EVENT, {
windowLabel: curWin.label,
pid
} satisfies IRecordExtensionProcessEvent)
// TODO: record process in a store
return Promise.resolve()
},
async denoExecute(
scriptPath: string,
config: DenoRunConfig,
@ -264,6 +247,7 @@ export function constructShellApi(
args1,
extPath
)
console.log("denoExecute", program, args, options)
return invoke<ChildProcess<IOPayload>>("plugin:shellx|execute", {
program,
args,
@ -302,7 +286,6 @@ export function constructShellApi(
},
execute,
kill,
killPid,
stdinWrite,
open,
rawSpawn,

View File

@ -59,8 +59,8 @@ export class Child {
*
* @since 2.0.0
*/
kill(): Promise<void> {
return this.api.kill(this.pid)
async kill(): Promise<void> {
this.api.kill(this.pid)
// await invoke("plugin:shellx|kill", {
// cmd: "killChild",
// pid: this.pid
@ -184,7 +184,6 @@ export class DenoCommand<O extends IOPayload> extends BaseShellCommand<O> {
}
})
.then(async (pid) => {
console.log("spawned deno process", pid)
await this.api.recordSpawnedProcess(pid)
return new Child(pid, this.api)
})
@ -233,7 +232,6 @@ export type IShell = {
}>
RPCChannel: typeof RPCChannel
whereIsCommand: (command: string) => Promise<string | null>
killPid: (pid: number) => Promise<void>
}
export class TauriShellStdio implements IoInterface {
@ -349,7 +347,6 @@ export function constructShellAPI(api: IShellServer): IShell {
return {
open: api.open,
killPid: api.killPid,
makeBashScript,
makePowershellScript,
makeAppleScript,

View File

@ -1,5 +1,5 @@
import { invoke } from "@tauri-apps/api/core"
import { AppInfo, SearchPath } from "../models"
import { AppInfo } from "../models"
import { generateJarvisPluginCommand } from "./common"
export function getAllApps(): Promise<AppInfo[]> {
@ -14,10 +14,6 @@ export function refreshApplicationsListInBg(): Promise<void> {
return invoke(generateJarvisPluginCommand("refresh_applications_list_in_bg"))
}
export function setExtraAppSearchPaths(paths: SearchPath[]): Promise<void> {
return invoke(generateJarvisPluginCommand("set_extra_app_search_paths"), { paths })
}
// export function convertAppToTListItem(app: AppInfo): TListItem {
// return {
// title: app.name,

View File

@ -0,0 +1,437 @@
import { invoke } from "@tauri-apps/api/core"
import { array, literal, optional, parse, safeParse, union, type InferOutput } from "valibot"
import { KUNKUN_EXT_IDENTIFIER } from "../constants"
import { CmdType, Ext, ExtCmd, ExtData } from "../models/extension"
import { convertDateToSqliteString, SearchMode, SearchModeEnum, SQLSortOrder } from "../models/sql"
import { generateJarvisPluginCommand } from "./common"
/* -------------------------------------------------------------------------- */
/* Extension CRUD */
/* -------------------------------------------------------------------------- */
export function createExtension(ext: {
identifier: string
version: string
enabled?: boolean
path?: string
data?: any
}) {
return invoke<void>(generateJarvisPluginCommand("create_extension"), ext)
}
export function getAllExtensions() {
return invoke<Ext[]>(generateJarvisPluginCommand("get_all_extensions"))
}
export function getUniqueExtensionByIdentifier(identifier: string) {
return invoke<Ext | undefined>(
generateJarvisPluginCommand("get_unique_extension_by_identifier"),
{
identifier
}
)
}
export function getUniqueExtensionByPath(path: string) {
return invoke<Ext | undefined>(generateJarvisPluginCommand("get_unique_extension_by_path"), {
path
})
}
export function getAllExtensionsByIdentifier(identifier: string) {
return invoke<Ext[]>(generateJarvisPluginCommand("get_all_extensions_by_identifier"), {
identifier
})
}
/**
* Use this function when you expect the extension to exist. Such as builtin extensions.
* @param identifier
* @returns
*/
export function getExtensionByIdentifierExpectExists(identifier: string): Promise<Ext> {
return getUniqueExtensionByIdentifier(identifier).then((ext) => {
if (!ext) {
throw new Error(`Unexpexted Error: Extension ${identifier} not found`)
}
return ext
})
}
// TODO: clean this up
// export function deleteExtensionByIdentifier(identifier: string) {
// return invoke<void>(generateJarvisPluginCommand("delete_extension_by_identifier"), { identifier })
// }
export function deleteExtensionByPath(path: string) {
return invoke<void>(generateJarvisPluginCommand("delete_extension_by_path"), {
path
})
}
export function deleteExtensionByExtId(extId: string) {
return invoke<void>(generateJarvisPluginCommand("delete_extension_by_ext_id"), { extId })
}
/* -------------------------------------------------------------------------- */
/* Extension Command CRUD */
/* -------------------------------------------------------------------------- */
export function createCommand(data: {
extId: number
name: string
cmdType: CmdType
data: string
alias?: string
hotkey?: string
enabled?: boolean
}) {
return invoke<void>(generateJarvisPluginCommand("create_command"), {
...data,
enabled: data.enabled ?? false
})
}
export function getCommandById(cmdId: number) {
return invoke<ExtCmd | undefined>(generateJarvisPluginCommand("get_command_by_id"), { cmdId })
}
export function getCommandsByExtId(extId: number) {
return invoke<ExtCmd[]>(generateJarvisPluginCommand("get_commands_by_ext_id"), { extId })
}
export function deleteCommandById(cmdId: number) {
return invoke<void>(generateJarvisPluginCommand("delete_command_by_id"), {
cmdId
})
}
export function updateCommandById(data: {
cmdId: number
name: string
cmdType: CmdType
data: string
alias?: string
hotkey?: string
enabled: boolean
}) {
return invoke<void>(generateJarvisPluginCommand("update_command_by_id"), data)
}
/* -------------------------------------------------------------------------- */
/* Extension Data CRUD */
/* -------------------------------------------------------------------------- */
export const ExtDataField = union([literal("data"), literal("search_text")])
export type ExtDataField = InferOutput<typeof ExtDataField>
function convertRawExtDataToExtData(rawData?: {
createdAt: string
updatedAt: string
data: null | string
searchText: null | string
}): ExtData | undefined {
if (!rawData) {
return rawData
}
const parsedRes = safeParse(ExtData, {
...rawData,
createdAt: new Date(rawData.createdAt),
updatedAt: new Date(rawData.updatedAt),
data: rawData.data ?? undefined,
searchText: rawData.searchText ?? undefined
})
if (parsedRes.success) {
return parsedRes.output
} else {
console.error("Extension Data Parse Failure", parsedRes.issues)
throw new Error("Fail to parse extension data")
}
}
export function createExtensionData(data: {
extId: number
dataType: string
data: string
searchText?: string
}) {
return invoke<void>(generateJarvisPluginCommand("create_extension_data"), data)
}
export function getExtensionDataById(dataId: number, fields?: ExtDataField[]) {
return invoke<
| (ExtData & {
createdAt: string
updatedAt: string
data: null | string
searchText: null | string
})
| undefined
>(generateJarvisPluginCommand("get_extension_data_by_id"), {
dataId,
fields
}).then(convertRawExtDataToExtData)
}
/**
* Fields option can be used to select optional fields. By default, if left empty, data and searchText are not returned.
* This is because data and searchText can be large and we don't want to return them by default.
* If you just want to get data ids in order to delete them, retrieving all data is not necessary.
* @param searchParams
*/
export async function searchExtensionData(searchParams: {
extId: number
searchMode: SearchMode
dataId?: number
dataType?: string
searchText?: string
afterCreatedAt?: string
beforeCreatedAt?: string
limit?: number
offset?: number
orderByCreatedAt?: SQLSortOrder
orderByUpdatedAt?: SQLSortOrder
fields?: ExtDataField[]
}): Promise<ExtData[]> {
const fields = parse(optional(array(ExtDataField), []), searchParams.fields)
let items = await invoke<
(ExtData & {
createdAt: string
updatedAt: string
data: null | string
searchText: null | string
})[]
>(generateJarvisPluginCommand("search_extension_data"), {
searchQuery: {
...searchParams,
fields
}
})
return items.map(convertRawExtDataToExtData).filter((item) => item) as ExtData[]
}
export function deleteExtensionDataById(dataId: number) {
return invoke<void>(generateJarvisPluginCommand("delete_extension_data_by_id"), { dataId })
}
export function updateExtensionDataById(data: {
dataId: number
data: string
searchText?: string
}) {
return invoke<void>(generateJarvisPluginCommand("update_extension_data_by_id"), data)
}
/* -------------------------------------------------------------------------- */
/* Built-in Extensions */
/* -------------------------------------------------------------------------- */
export function getExtClipboard() {
return getExtensionByIdentifierExpectExists(KUNKUN_EXT_IDENTIFIER.KUNKUN_CLIPBOARD_EXT_IDENTIFIER)
}
export function getExtQuickLinks() {
return getExtensionByIdentifierExpectExists(
KUNKUN_EXT_IDENTIFIER.KUNKUN_QUICK_LINKS_EXT_IDENTIFIER
)
}
export function getExtRemote() {
return getExtensionByIdentifierExpectExists(KUNKUN_EXT_IDENTIFIER.KUNKUN_REMOTE_EXT_IDENTIFIER)
}
export function getExtScriptCmd() {
return getExtensionByIdentifierExpectExists(
KUNKUN_EXT_IDENTIFIER.KUNKUN_SCRIPT_CMD_EXT_IDENTIFIER
)
}
export function getExtDev() {
return getExtensionByIdentifierExpectExists(KUNKUN_EXT_IDENTIFIER.KUNKUN_DEV_EXT_IDENTIFIER)
}
/**
* Database API for extensions.
* Extensions shouldn't have full access to the database, they can only access their own data.
* When an extension is loaded, the main thread will create an instance of this class and
* expose it to the extension.
*/
export class JarvisExtDB {
extId: number
constructor(extId: number) {
this.extId = extId
}
async add(data: { data: string; dataType?: string; searchText?: string }) {
return createExtensionData({
data: data.data,
dataType: data.dataType ?? "default",
searchText: data.searchText,
extId: this.extId
})
}
async delete(dataId: number): Promise<void> {
// Verify if this data belongs to this extension
const d = await getExtensionDataById(dataId)
if (!d || d.extId !== this.extId) {
throw new Error("Extension Data not found")
}
return await deleteExtensionDataById(dataId)
}
async search(searchParams: {
dataId?: number
searchMode?: SearchMode
dataType?: string
searchText?: string
afterCreatedAt?: Date
beforeCreatedAt?: Date
limit?: number
orderByCreatedAt?: SQLSortOrder
orderByUpdatedAt?: SQLSortOrder
fields?: ExtDataField[]
}): Promise<ExtData[]> {
const beforeCreatedAt = searchParams.beforeCreatedAt
? convertDateToSqliteString(searchParams.beforeCreatedAt)
: undefined
const afterCreatedAt = searchParams.afterCreatedAt
? convertDateToSqliteString(searchParams.afterCreatedAt)
: undefined
return searchExtensionData({
...searchParams,
searchMode: searchParams.searchMode ?? SearchModeEnum.FTS,
extId: this.extId,
beforeCreatedAt,
afterCreatedAt
})
}
/**
* Retrieve all data of this extension.
* Use `search()` method for more advanced search.
* @param options optional fields to retrieve. By default, data and searchText are not returned.
* @returns
*/
retrieveAll(options: { fields?: ExtDataField[] }): Promise<ExtData[]> {
return this.search({ fields: options.fields })
}
/**
* Retrieve all data of this extension by type.
* Use `search()` method for more advanced search.
* @param dataType
* @returns
*/
retrieveAllByType(dataType: string): Promise<ExtData[]> {
return this.search({ dataType })
}
/**
* Delete all data of this extension.
*/
deleteAll(): Promise<void> {
return this.search({})
.then((items) => {
return Promise.all(items.map((item) => this.delete(item.dataId)))
})
.then(() => {})
}
/**
* Update data and searchText of this extension.
* @param dataId unique id of the data
* @param data
* @param searchText
* @returns
*/
async update(data: { dataId: number; data: string; searchText?: string }): Promise<void> {
const d = await getExtensionDataById(data.dataId)
if (!d || d.extId !== this.extId) {
throw new Error("Extension Data not found")
}
return updateExtensionDataById(data)
}
}
export class KV {
extId: number
db: JarvisExtDB
private DataType: string = "kunkun_kv"
constructor(extId: number) {
this.extId = extId
this.db = new JarvisExtDB(extId)
}
get<T = string>(key: string): Promise<T | null | undefined> {
return this.db
.search({
dataType: this.DataType,
searchText: key,
searchMode: SearchModeEnum.ExactMatch,
fields: ["search_text", "data"]
})
.then((items) => {
if (items.length === 0) {
return null
} else if (items.length > 1) {
throw new Error("Multiple KVs with the same key")
}
return items[0].data ? (JSON.parse(items[0].data).value as T) : null
})
.catch((err) => {
console.warn(err)
return null
})
}
set(key: string, value: string): Promise<void> {
return this.db
.search({
dataType: this.DataType,
searchText: key,
searchMode: SearchModeEnum.ExactMatch
})
.then((items) => {
if (items.length === 0) {
return this.db.add({
data: JSON.stringify({ value: value }),
dataType: this.DataType,
searchText: key
})
} else if (items.length === 1) {
return this.db.update({
dataId: items[0].dataId,
data: JSON.stringify({ value: value }),
searchText: key
})
} else {
return Promise.all(items.map((item) => this.db.delete(item.dataId))).then(() =>
Promise.resolve()
)
}
})
}
delete(key: string): Promise<void> {
return this.db
.search({
dataType: this.DataType,
searchText: key,
searchMode: SearchModeEnum.ExactMatch
})
.then((items) => {
return Promise.all(items.map((item) => this.db.delete(item.dataId))).then(() =>
Promise.resolve()
)
})
}
exists(key: string): Promise<boolean> {
return this.db
.search({
dataType: this.DataType,
searchText: key,
searchMode: SearchModeEnum.ExactMatch,
fields: []
})
.then((items) => {
return items.length > 0
})
}
}

View File

@ -5,7 +5,8 @@ export * from "./tools"
export * from "./extension"
export * from "./system"
export * from "./store"
export * as sql from "./sql"
export * as db from "./db"
export { JarvisExtDB } from "./db"
export * from "./clipboard"
export * from "./fileSearch"
export * from "./utils"

View File

@ -1,30 +0,0 @@
import { invoke } from "@tauri-apps/api/core"
import { generateJarvisPluginCommand } from "./common"
export interface QueryResult {
/** The number of rows affected by the query. */
rowsAffected: number
/**
* The last inserted `id`.
*
* This value is not set for Postgres databases. If the
* last inserted id is required on Postgres, the `select` function
* must be used, with a `RETURNING` clause
* (`INSERT INTO todos (title) VALUES ($1) RETURNING id`).
*/
lastInsertId?: number
}
export function select(query: string, values: any[]) {
return invoke<any[]>(generateJarvisPluginCommand("select"), {
query,
values
})
}
export function execute(query: string, values: any[]) {
return invoke<QueryResult>(generateJarvisPluginCommand("execute"), {
query,
values
})
}

View File

@ -46,7 +46,7 @@ export async function refreshTemplateWorkerCommandViaServer() {
const ports = await findLocalhostKunkunPorts()
console.log("Kunkun ports", ports)
if (ports.length === 0) {
console.warn("\x1b[33mFailed to find localhost kunkun ports\x1b[0m")
console.error("Failed to find localhost kunkun ports")
return
} else if (ports.length > 1) {
console.warn("Found multiple localhost kunkun ports", ports)

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