diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..423830b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +pnpm-lock.yaml linguist-generated=true +packages/tauri-plugins/jarvis/permissions/autogenerated linguist-generated=true diff --git a/apps/desktop/app.d.ts b/apps/desktop/app.d.ts new file mode 100644 index 0000000..9959563 --- /dev/null +++ b/apps/desktop/app.d.ts @@ -0,0 +1,8 @@ +import type { AttributifyAttributes } from "@unocss/preset-attributify" + +declare module "svelte/elements" { + // eslint-disable-next-line @typescript-eslint/no-empty-object-type, @typescript-eslint/no-unused-vars + interface HTMLAttributes extends AttributifyAttributes {} +} + +export {} diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 717ed17..b909e8b 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@kksh/desktop", - "version": "0.1.9-beta.8", + "version": "0.1.10", "description": "", "type": "module", "scripts": { @@ -15,6 +15,7 @@ "license": "MIT", "dependencies": { "@formkit/auto-animate": "^0.8.2", + "@huakunshen/comlink": "^4.4.1", "@kksh/extension": "workspace:*", "@kksh/supabase": "workspace:*", "@kksh/ui": "workspace:*", @@ -27,9 +28,11 @@ "lucide-svelte": "^0.454.0", "lz-string": "^1.5.0", "mode-watcher": "^0.4.1", + "semver": "^7.6.3", "svelte-radix": "^2.0.1", "svelte-sonner": "^0.3.28", - "sveltekit-superforms": "^2.20.0" + "sveltekit-superforms": "^2.20.0", + "uuid": "^11.0.2" }, "devDependencies": { "@kksh/types": "workspace:*", @@ -42,6 +45,8 @@ "@tailwindcss/typography": "^0.5.15", "@tauri-apps/cli": "^2.0.4", "@types/bun": "latest", + "@types/semver": "^7.5.8", + "@unocss/preset-attributify": "^0.64.0", "autoprefixer": "^10.4.20", "clsx": "^2.1.1", "embla-carousel-svelte": "^8.3.1", @@ -52,6 +57,7 @@ "tailwindcss-animate": "^1.0.7", "tslib": "^2.8.1", "typescript": "^5.6.3", + "unocss": "^0.64.0", "vaul-svelte": "^0.3.2", "vite": "^5.4.10" } diff --git a/apps/desktop/src-tauri/src/utils/server.rs b/apps/desktop/src-tauri/src/utils/server.rs index 53b161c..c6377f0 100644 --- a/apps/desktop/src-tauri/src/utils/server.rs +++ b/apps/desktop/src-tauri/src/utils/server.rs @@ -8,47 +8,14 @@ pub fn tauri_file_server( extension_folder_path: PathBuf, dist: Option, ) -> tauri::http::Response> { - // let host = request.uri().host().unwrap(); - // let host_parts: Vec<&str> = host.split(".").collect(); - // if host_parts.len() != 3 { - // return tauri::http::Response::builder() - // .status(tauri::http::StatusCode::NOT_FOUND) - // .header("Access-Control-Allow-Origin", "*") - // .body("Invalid Host".as_bytes().to_vec()) - // .unwrap(); - // } - // expect 3 parts, ext_identifier, dist and ext_type - // let ext_identifier = host_parts[0]; - // let dist = host_parts[1]; - // let ext_type = host_parts[2]; // ext or dev-ext - // let app_state = app.state::(); - // let app_state: tauri:State = app.state(); - // let extension_folder_path: Option = match ext_type { - // "ext" => Some(app_state.extension_path.lock().unwrap().clone()), - // "dev-ext" => app_state.dev_extension_path.lock().unwrap().clone(), - // _ => None, - // }; - // let extension_folder_path = match extension_folder_path { - // Some(path) => path, - // None => { - // return tauri::http::Response::builder() - // .status(tauri::http::StatusCode::NOT_FOUND) - // .header("Access-Control-Allow-Origin", "*") - // .body("Extension Folder Not Found".as_bytes().to_vec()) - // .unwrap() - // } - // }; - println!("dist: {:?}", dist); let path = &request.uri().path()[1..]; // skip the first / let path = urlencoding::decode(path).unwrap().to_string(); let mut url_file_path = extension_folder_path; - // .join(ext_identifier) match dist { Some(dist) => url_file_path = url_file_path.join(dist), None => {} } url_file_path = url_file_path.join(path); - println!("url_file_path: {:?}", url_file_path); // check if it's file or directory, if file and exist, return file, if directory, return index.html, if neither, check .html if url_file_path.is_file() { // println!("1st case url_file_path: {:?}", url_file_path); diff --git a/apps/desktop/src/lib/cmds/builtin.ts b/apps/desktop/src/lib/cmds/builtin.ts index e6ee94e..f6b3a01 100644 --- a/apps/desktop/src/lib/cmds/builtin.ts +++ b/apps/desktop/src/lib/cmds/builtin.ts @@ -1,7 +1,12 @@ -import { appState } from "@/stores" +import { appConfig, appState, auth } from "@/stores" +import { checkUpdateAndInstall } from "@/utils/updater" import type { BuiltinCmd } from "@kksh/ui/types" -import { dev } from "$app/environment" +import { getVersion } from "@tauri-apps/api/app" +import { WebviewWindow } from "@tauri-apps/api/webviewWindow" +import { exit } from "@tauri-apps/plugin-process" import { goto } from "$app/navigation" +import { toast } from "svelte-sonner" +import { v4 as uuidv4 } from "uuid" export const builtinCmds: BuiltinCmd[] = [ { @@ -13,58 +18,59 @@ export const builtinCmds: BuiltinCmd[] = [ goto("/extension/store") } }, - // { - // name: "Sign In", - // iconifyIcon: "mdi:login-variant", - // description: "", - // function: async () => { - // goto("/auth") - // } - // }, - // { - // name: "Sign Out", - // iconifyIcon: "mdi:logout-variant", - // description: "", - // function: async () => { - // const supabase = useSupabaseClient() - // supabase.auth.signOut() - // } - // }, - // { - // name: "Show Draggable Area", - // iconifyIcon: "mingcute:move-fill", - // description: "", - // function: async () => { - // // select all html elements with attribute data-tauri-drag-region - // const elements = document.querySelectorAll("[data-tauri-drag-region]") - // elements.forEach((el) => { - // el.classList.add("bg-red-500/30") - // }) - // setTimeout(() => { - // elements.forEach((el) => { - // el.classList.remove("bg-red-500/30") - // }) - // }, 2_000) - // } - // }, - // { - // name: "Add Dev Extension", - // iconifyIcon: "lineicons:dev", - // description: "", - // function: async () => { - // const appStateStore = useAppStateStore() - // appStateStore.setSearchTermSync("") - // goto("/add-dev-ext") - // } - // }, - // { - // name: "Kunkun Version", - // iconifyIcon: "stash:version-solid", - // description: "", - // function: async () => { - // toast.success(`Kunkun Version: ${await getVersion()}`) - // } - // }, + { + name: "Sign In", + iconifyIcon: "mdi:login-variant", + description: "", + function: async () => { + goto("/auth") + } + }, + { + name: "Sign Out", + iconifyIcon: "mdi:logout-variant", + description: "", + function: async () => { + auth + .signOut() + .then(() => toast.success("Signed out")) + .catch((err) => toast.error("Failed to sign out: ", { description: err.message })) + } + }, + { + name: "Show Draggable Area", + iconifyIcon: "mingcute:move-fill", + description: "", + function: async () => { + // select all html elements with attribute data-tauri-drag-region + const elements = document.querySelectorAll("[data-tauri-drag-region]") + elements.forEach((el) => { + el.classList.add("bg-red-500/30") + }) + setTimeout(() => { + elements.forEach((el) => { + el.classList.remove("bg-red-500/30") + }) + }, 2_000) + } + }, + { + name: "Add Dev Extension", + iconifyIcon: "lineicons:dev", + description: "", + function: async () => { + appState.clearSearchTerm() + goto("/settings/add-dev-extension") + } + }, + { + name: "Kunkun Version", + iconifyIcon: "stash:version-solid", + description: "", + function: async () => { + toast.success(`Kunkun Version: ${await getVersion()}`) + } + }, { name: "Set Dev Extension Path", iconifyIcon: "lineicons:dev", @@ -75,52 +81,48 @@ export const builtinCmds: BuiltinCmd[] = [ goto("/settings/set-dev-ext-path") } }, - // { - // name: "Extension Window Troubleshooter", - // iconifyIcon: "material-symbols:window-outline", - // description: "", - // function: async () => { - // const appStateStore = useAppStateStore() - // appStateStore.setSearchTermSync("") - // // goto("/window-troubleshooter") - // const winLabel = `main:window-troubleshooter-${uuidv4()}` - // console.log(winLabel) - // new WebviewWindow(winLabel, { - // url: "/window-troubleshooter", - // title: "Window Troubleshooter" - // }) - // } - // }, - // { - // name: "Extension Permission Inspector", - // iconifyIcon: "hugeicons:inspect-code", - // description: "", - // function: async () => { - // const appStateStore = useAppStateStore() - // appStateStore.setSearchTermSync("") - // goto("/ext-permission-inspector") - // } - // }, - // { - // name: "Extension Loading Troubleshooter", - // iconifyIcon: "material-symbols:troubleshoot", - // description: "", - // function: async () => { - // const appStateStore = useAppStateStore() - // appStateStore.setSearchTermSync("") - // goto("/extension-load-troubleshooter") - // } - // }, - // { - // name: "Create Quicklink", - // iconifyIcon: "material-symbols:link", - // description: "Create a Quicklink", - // function: async () => { - // const appStateStore = useAppStateStore() - // appStateStore.setSearchTermSync("") - // goto("/create-quicklink") - // } - // }, + { + name: "Extension Window Troubleshooter", + iconifyIcon: "material-symbols:window-outline", + description: "", + function: async () => { + appState.clearSearchTerm() + // goto("/window-troubleshooter") + const winLabel = `main:extension-window-troubleshooter-${uuidv4()}` + console.log(winLabel) + new WebviewWindow(winLabel, { + url: "/troubleshooters/extension-window", + title: "Extension Window Troubleshooter" + }) + } + }, + { + name: "Extension Permission Inspector", + iconifyIcon: "hugeicons:inspect-code", + description: "", + function: async () => { + appState.clearSearchTerm() + goto("/extension/permission-inspector") + } + }, + { + name: "Extension Loading Troubleshooter", + iconifyIcon: "material-symbols:troubleshoot", + description: "", + function: async () => { + appState.clearSearchTerm() + goto("/troubleshooters/extension-loading") + } + }, + { + name: "Create Quicklink", + iconifyIcon: "material-symbols:link", + description: "Create a Quicklink", + function: async () => { + appState.clearSearchTerm() + goto("/extension/create-quick-link") + } + }, // { // name: "Settings", // iconifyIcon: "solar:settings-linear", @@ -143,30 +145,32 @@ export const builtinCmds: BuiltinCmd[] = [ // appStateStore.setSearchTermSync("") // } // }, - // { - // name: "Check Update", - // iconifyIcon: "material-symbols:update", - // description: "Check for updates", - // function: async () => { - // checkUpdateAndInstall() - // } - // }, - // { - // name: "Check Beta Update", - // iconifyIcon: "material-symbols:update", - // description: "Check for Beta updates", - // function: async () => { - // checkUpdateAndInstall(true) - // } - // }, - // { - // name: "Reload", - // iconifyIcon: "tabler:reload", - // description: "Reload this page", - // function: async () => { - // location.reload() - // } - // }, + { + name: "Check Update", + iconifyIcon: "material-symbols:update", + description: "Check for updates", + function: async () => { + checkUpdateAndInstall() + appState.clearSearchTerm() + } + }, + { + name: "Check Beta Update", + iconifyIcon: "material-symbols:update", + description: "Check for Beta updates", + function: async () => { + checkUpdateAndInstall({ beta: true }) + appState.clearSearchTerm() + } + }, + { + name: "Reload", + iconifyIcon: "tabler:reload", + description: "Reload this page", + function: async () => { + location.reload() + } + }, { name: "Dance", iconifyIcon: "mdi:dance-pole", @@ -174,33 +178,43 @@ export const builtinCmds: BuiltinCmd[] = [ function: async () => { goto("/dance") } + }, + { + name: "Quit Kunkun", + iconifyIcon: "emojione:cross-mark-button", + description: "Quit Kunkun", + function: async () => { + exit(0) + } + }, + { + name: "Toggle Dev Extension HMR", + iconifyIcon: "ri:toggle-line", + description: "Load dev extensions from their dev server URLs", + function: async () => { + appConfig.update((config) => { + toast.success(`Dev Extension HMR toggled to: ${!config.hmr}`) + return { + ...config, + hmr: !config.hmr + } + }) + appState.clearSearchTerm() + } + }, + { + name: "Toggle Hide On Blur", + iconifyIcon: "ri:toggle-line", + description: "Toggle Hide On Blur", + function: async () => { + appConfig.update((config) => { + toast.success(`"Hide on Blur" toggled to: ${!config.hideOnBlur}`) + return { + ...config, + hideOnBlur: !config.hideOnBlur + } + }) + appState.clearSearchTerm() + } } - // { - // name: "Quit Kunkun", - // iconifyIcon: "emojione:cross-mark-button", - // description: "Quit Kunkun", - // function: async () => { - // exit(0) - // } - // }, - // { - // name: "Toggle Dev Extension Live Load Mode", - // iconifyIcon: "ri:toggle-line", - // description: "Load dev extensions from their dev server URLs", - // function: async () => { - // toggleDevExtensionLiveLoadMode() - // } - // }, - // { - // name: "Toggle Hide On Blur", - // iconifyIcon: "ri:toggle-line", - // description: "Toggle Hide On Blur", - // function: async () => { - // const appConfig = useAppConfigStore() - // appConfig.setHideOnBlur(!appConfig.hideOnBlur) - // const appStateStore = useAppStateStore() - // appStateStore.setSearchTermSync("") - // toast.success(`"Hide on Blur" toggled to: ${appConfig.hideOnBlur}`) - // } - // } ] diff --git a/apps/desktop/src/lib/cmds/ext.ts b/apps/desktop/src/lib/cmds/ext.ts index 68fb4b6..1aede5c 100644 --- a/apps/desktop/src/lib/cmds/ext.ts +++ b/apps/desktop/src/lib/cmds/ext.ts @@ -1,14 +1,12 @@ +import { appState } from "@/stores" import { winExtMap } from "@/stores/winExtMap" import { trimSlash } from "@/utils/url" import { constructExtensionSupportDir } from "@kksh/api" -import { CmdTypeEnum, CustomUiCmd, ExtPackageJsonExtra, TemplateUiCmd } from "@kksh/api/models" +import { CustomUiCmd, ExtPackageJsonExtra, TemplateUiCmd } from "@kksh/api/models" import { launchNewExtWindow } from "@kksh/extension" import { convertFileSrc } from "@tauri-apps/api/core" -import { WebviewWindow } from "@tauri-apps/api/webviewWindow" import * as fs from "@tauri-apps/plugin-fs" -import { debug } from "@tauri-apps/plugin-log" import { goto } from "$app/navigation" -import * as v from "valibot" export async function createExtSupportDir(extPath: string) { const extSupportDir = await constructExtensionSupportDir(extPath) @@ -23,7 +21,19 @@ export async function onTemplateUiCmdSelect( { isDev, hmr }: { isDev: boolean; hmr: boolean } ) { await createExtSupportDir(ext.extPath) - console.log("onTemplateUiCmdSelect", ext, cmd, isDev, hmr) + // console.log("onTemplateUiCmdSelect", ext, cmd, isDev, hmr) + const url = `/extension/ui-worker?extPath=${encodeURIComponent(ext.extPath)}&cmdName=${encodeURIComponent(cmd.name)}` + if (cmd.window) { + const winLabel = await winExtMap.registerExtensionWithWindow({ extPath: ext.extPath }) + const window = launchNewExtWindow(winLabel, url, cmd.window) + window.onCloseRequested(async (event) => { + await winExtMap.unregisterExtensionFromWindow(winLabel) + }) + } else { + return winExtMap + .registerExtensionWithWindow({ windowLabel: "main", extPath: ext.extPath }) + .then(() => goto(url)) + } } export async function onCustomUiCmdSelect( @@ -31,7 +41,7 @@ export async function onCustomUiCmdSelect( cmd: CustomUiCmd, { isDev, hmr }: { isDev: boolean; hmr: boolean } ) { - console.log("onCustomUiCmdSelect", ext, cmd, isDev, hmr) + // console.log("onCustomUiCmdSelect", ext, cmd, isDev, hmr) await createExtSupportDir(ext.extPath) let url = cmd.main @@ -48,14 +58,14 @@ export async function onCustomUiCmdSelect( }) console.log("Launch new window, ", winLabel) const window = launchNewExtWindow(winLabel, url2, cmd.window) + window.onCloseRequested(async (event) => { + await winExtMap.unregisterExtensionFromWindow(winLabel) + }) } else { console.log("Launch main window") return winExtMap - .registerExtensionWithWindow({ - windowLabel: "main", - extPath: ext.extPath, - dist: cmd.dist - }) + .registerExtensionWithWindow({ windowLabel: "main", extPath: ext.extPath, dist: cmd.dist }) .then(() => goto(url2)) } + appState.clearSearchTerm() } diff --git a/apps/desktop/src/lib/cmds/index.ts b/apps/desktop/src/lib/cmds/index.ts index 3efc68d..a845ef1 100644 --- a/apps/desktop/src/lib/cmds/index.ts +++ b/apps/desktop/src/lib/cmds/index.ts @@ -2,6 +2,7 @@ import { CmdTypeEnum, CustomUiCmd, ExtPackageJsonExtra, TemplateUiCmd } from "@k import type { CommandLaunchers, OnExtCmdSelect } from "@kksh/ui/types" import * as v from "valibot" import { onCustomUiCmdSelect, onTemplateUiCmdSelect } from "./ext" +import { onQuickLinkSelect } from "./quick-links" const onExtCmdSelect: OnExtCmdSelect = ( ext: ExtPackageJsonExtra, @@ -20,4 +21,4 @@ const onExtCmdSelect: OnExtCmdSelect = ( } } -export const commandLaunchers = { onExtCmdSelect } satisfies CommandLaunchers +export const commandLaunchers = { onExtCmdSelect, onQuickLinkSelect } satisfies CommandLaunchers diff --git a/apps/desktop/src/lib/cmds/quick-links.ts b/apps/desktop/src/lib/cmds/quick-links.ts new file mode 100644 index 0000000..43662a4 --- /dev/null +++ b/apps/desktop/src/lib/cmds/quick-links.ts @@ -0,0 +1,25 @@ +import { appState } from "@/stores" +import type { CmdQuery, CmdValue } from "@kksh/ui/types" +import { open } from "tauri-plugin-shellx-api" + +/** + * Given some link like https://google.com/search?q={argument}&query={query} + * Find {argument} and {query} + */ +export function findAllArgsInLink(link: string): string[] { + const regex = /\{([^}]+)\}/g + const matches = [...link.matchAll(regex)] + return matches.map((match) => match[1]) +} + +export function onQuickLinkSelect(quickLink: CmdValue, queries: CmdQuery[]) { + console.log(quickLink, queries) + let qlink = quickLink.data + for (const arg of queries) { + console.log(`replace all {${arg.name}} with ${arg.value}`) + qlink = qlink.replaceAll(`{${arg.name}}`, arg.value) + } + appState.clearSearchTerm() + console.log(qlink) + open(qlink) +} diff --git a/apps/desktop/src/lib/cmds/system.ts b/apps/desktop/src/lib/cmds/system.ts new file mode 100644 index 0000000..cd7221f --- /dev/null +++ b/apps/desktop/src/lib/cmds/system.ts @@ -0,0 +1,4 @@ +import { getSystemCommands } from "@kksh/api/commands" +import type { SysCommand } from "@kksh/api/models" + +export const systemCommands: SysCommand[] = getSystemCommands() diff --git a/apps/desktop/src/lib/components/common/DragNDrop.svelte b/apps/desktop/src/lib/components/common/DragNDrop.svelte new file mode 100644 index 0000000..c74dbce --- /dev/null +++ b/apps/desktop/src/lib/components/common/DragNDrop.svelte @@ -0,0 +1,38 @@ + + + + {@render children()} + diff --git a/apps/desktop/src/lib/components/main/CommandPalette.svelte b/apps/desktop/src/lib/components/main/CommandPalette.svelte deleted file mode 100644 index 8e117e4..0000000 --- a/apps/desktop/src/lib/components/main/CommandPalette.svelte +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - No results found. - - {#if $appConfig.extensionPath && $devStoreExts.length > 0} - - {/if} - {#if $appConfig.extensionPath && $installedStoreExts.length > 0} - - {/if} - - - - diff --git a/apps/desktop/src/lib/components/standalone/settings/AddDevExtForm.svelte b/apps/desktop/src/lib/components/standalone/settings/AddDevExtForm.svelte new file mode 100644 index 0000000..53f66d3 --- /dev/null +++ b/apps/desktop/src/lib/components/standalone/settings/AddDevExtForm.svelte @@ -0,0 +1,149 @@ + + +
+ + +
+ + +

Drag and Drop

+
+ + + { + handleDragNDropInstall(e.payload.paths) + }} + onEnter={() => (dragging = true)} + onCancelled={() => (dragging = false)} + > + +
+
+ + Drag and Drop + Extension Folder or Tarball +
+
+
+
+
+ +

Install Tarball From URL

+
+ + diff --git a/apps/desktop/src/lib/components/standalone/settings/install-npm-package-name-form.svelte b/apps/desktop/src/lib/components/standalone/settings/install-npm-package-name-form.svelte new file mode 100644 index 0000000..4cb78b5 --- /dev/null +++ b/apps/desktop/src/lib/components/standalone/settings/install-npm-package-name-form.svelte @@ -0,0 +1,61 @@ + + +
+ + + {#snippet children({ props })} + + + Install + + {/snippet} + + + +
diff --git a/apps/desktop/src/lib/components/standalone/settings/install-tarball-url-form.svelte b/apps/desktop/src/lib/components/standalone/settings/install-tarball-url-form.svelte new file mode 100644 index 0000000..a950a64 --- /dev/null +++ b/apps/desktop/src/lib/components/standalone/settings/install-tarball-url-form.svelte @@ -0,0 +1,62 @@ + + +
+ + + {#snippet children({ props })} + + + Install + + {/snippet} + + + +
diff --git a/apps/desktop/src/lib/stores/appConfig.ts b/apps/desktop/src/lib/stores/appConfig.ts index aa50558..78ebcd0 100644 --- a/apps/desktop/src/lib/stores/appConfig.ts +++ b/apps/desktop/src/lib/stores/appConfig.ts @@ -1,12 +1,10 @@ import { getExtensionsFolder } from "@/constants" -import { themeConfigStore, updateTheme, type ThemeConfig } from "@kksh/svelte5" +import { createTauriSyncStore, type WithSyncStore } from "@/utils/sync-store" +import { updateTheme, type ThemeConfig } from "@kksh/svelte5" import { PersistedAppConfig, type AppConfig } from "@kksh/types" -import * as path from "@tauri-apps/api/path" -import { remove } from "@tauri-apps/plugin-fs" import { debug, error } from "@tauri-apps/plugin-log" import * as os from "@tauri-apps/plugin-os" import { load } from "@tauri-apps/plugin-store" -import { get, writable, type Writable } from "svelte/store" import * as v from "valibot" export const defaultAppConfig: AppConfig = { @@ -21,7 +19,7 @@ export const defaultAppConfig: AppConfig = { launchAtLogin: true, showInTray: true, devExtensionPath: null, - extensionPath: undefined, + extensionsInstallDir: undefined, hmr: false, hideOnBlur: true, extensionAutoUpgrade: true, @@ -35,25 +33,22 @@ interface AppConfigAPI { setDevExtensionPath: (devExtensionPath: string | null) => void } -function createAppConfig(): Writable & AppConfigAPI { - const { subscribe, update, set } = writable(defaultAppConfig) +function createAppConfig(): WithSyncStore & AppConfigAPI { + const store = createTauriSyncStore("app-config", defaultAppConfig) async function init() { debug("Initializing app config") - const appDataDir = await path.appDataDir() - // const appConfigPath = await path.join(appDataDir, "appConfig.json") - // debug(`appConfigPath: ${appConfigPath}`) const persistStore = await load("kk-config.json", { autoSave: true }) const loadedConfig = await persistStore.get("config") const parseRes = v.safeParse(PersistedAppConfig, loadedConfig) if (parseRes.success) { console.log("Parse Persisted App Config Success", parseRes.output) - const extensionPath = await path.join(appDataDir, "extensions") - update((config) => ({ + const extensionsInstallDir = await getExtensionsFolder() + store.update((config) => ({ ...config, ...parseRes.output, isInitialized: true, - extensionPath, + extensionsInstallDir, platform: os.platform() })) } else { @@ -63,7 +58,7 @@ function createAppConfig(): Writable & AppConfigAPI { await persistStore.set("config", v.parse(PersistedAppConfig, defaultAppConfig)) } - subscribe(async (config) => { + store.subscribe(async (config) => { console.log("Saving app config", config) await persistStore.set("config", config) updateTheme(config.theme) @@ -71,15 +66,13 @@ function createAppConfig(): Writable & AppConfigAPI { } return { - setTheme: (theme: ThemeConfig) => update((config) => ({ ...config, theme })), + ...store, + setTheme: (theme: ThemeConfig) => store.update((config) => ({ ...config, theme })), setDevExtensionPath: (devExtensionPath: string | null) => { console.log("setDevExtensionPath", devExtensionPath) - update((config) => ({ ...config, devExtensionPath })) + store.update((config) => ({ ...config, devExtensionPath })) }, - init, - subscribe, - update, - set + init } } diff --git a/apps/desktop/src/lib/stores/appState.ts b/apps/desktop/src/lib/stores/appState.ts index aaaf45d..d45c87a 100644 --- a/apps/desktop/src/lib/stores/appState.ts +++ b/apps/desktop/src/lib/stores/appState.ts @@ -1,26 +1,42 @@ -import type { AppState } from "@/types" -import { get, writable, type Writable } from "svelte/store" +import { findAllArgsInLink } from "@/cmds/quick-links" +import { Action as ActionSchema, CmdTypeEnum } from "@kksh/api/models" +import type { AppState } from "@kksh/types" +import type { CmdValue } from "@kksh/ui/types" +import { derived, get, writable, type Writable } from "svelte/store" export const defaultAppState: AppState = { searchTerm: "", - highlightedCmd: "" + highlightedCmd: "", + loadingBar: false, + defaultAction: "", + actionPanel: undefined } interface AppStateAPI { clearSearchTerm: () => void get: () => AppState + setLoadingBar: (loadingBar: boolean) => void + setDefaultAction: (defaultAction: string) => void + setActionPanel: (actionPanel?: ActionSchema.ActionPanel) => void } function createAppState(): Writable & AppStateAPI { const store = writable(defaultAppState) return { - subscribe: store.subscribe, - update: store.update, - set: store.set, + ...store, get: () => get(store), clearSearchTerm: () => { store.update((state) => ({ ...state, searchTerm: "" })) + }, + setLoadingBar: (loadingBar: boolean) => { + store.update((state) => ({ ...state, loadingBar })) + }, + setDefaultAction: (defaultAction: string) => { + store.update((state) => ({ ...state, defaultAction })) + }, + setActionPanel: (actionPanel?: ActionSchema.ActionPanel) => { + store.update((state) => ({ ...state, actionPanel })) } } } diff --git a/apps/desktop/src/lib/stores/auth.ts b/apps/desktop/src/lib/stores/auth.ts new file mode 100644 index 0000000..c88e3bd --- /dev/null +++ b/apps/desktop/src/lib/stores/auth.ts @@ -0,0 +1,47 @@ +import { supabase } from "@/supabase" +import type { AuthError, Session, User } from "@supabase/supabase-js" +import { get, writable, type Writable } from "svelte/store" + +type State = { session: Session | null; user: User | null } + +interface AuthAPI { + get: () => State + refresh: () => Promise + signOut: () => Promise<{ error: AuthError | null }> + signInExchange: (code: string) => Promise<{ error: AuthError | null }> +} + +function createAuth(): Writable & AuthAPI { + const store = writable({ session: null, user: null }) + async function refresh() { + const { + data: { session }, + error + } = await supabase.auth.getSession() + const { + data: { user } + } = await supabase.auth.getUser() + store.update((state) => ({ ...state, session, user })) + } + async function signOut() { + return supabase.auth.signOut().then((res) => { + refresh() + return res + }) + } + async function signInExchange(code: string) { + return supabase.auth.exchangeCodeForSession(code).then((res) => { + refresh() + return res + }) + } + return { + ...store, + get: () => get(store), + refresh, + signOut, + signInExchange + } +} + +export const auth = createAuth() diff --git a/apps/desktop/src/lib/stores/cmdQuery.ts b/apps/desktop/src/lib/stores/cmdQuery.ts new file mode 100644 index 0000000..18bcb01 --- /dev/null +++ b/apps/desktop/src/lib/stores/cmdQuery.ts @@ -0,0 +1,23 @@ +import { findAllArgsInLink } from "@/cmds/quick-links" +import { CmdTypeEnum } from "@kksh/api/models" +import type { CmdQuery, CmdValue } from "@kksh/ui/main" +import { derived, get, writable, type Writable } from "svelte/store" +import { appState } from "./appState" + +function createCmdQueryStore(): Writable { + const store = writable([]) + appState.subscribe(($appState) => { + if ($appState.highlightedCmd.startsWith("{")) { + const parsedCmd = JSON.parse($appState.highlightedCmd) as CmdValue + if (parsedCmd.cmdType === CmdTypeEnum.QuickLink && parsedCmd.data) { + return store.set(findAllArgsInLink(parsedCmd.data).map((arg) => ({ name: arg, value: "" }))) + } + } + store.set([]) + }) + return { + ...store + } +} + +export const cmdQueries = createCmdQueryStore() diff --git a/apps/desktop/src/lib/stores/extensions.ts b/apps/desktop/src/lib/stores/extensions.ts index 0bcb1a4..770ed2a 100644 --- a/apps/desktop/src/lib/stores/extensions.ts +++ b/apps/desktop/src/lib/stores/extensions.ts @@ -10,22 +10,25 @@ import { appConfig } from "./appConfig" function createExtensionsStore(): Writable & { init: () => Promise getExtensionsFromStore: () => ExtPackageJsonExtra[] + installTarball: (tarballPath: string, extsDir: string) => Promise + installDevExtensionDir: (dirPath: string) => Promise installFromTarballUrl: (tarballUrl: string, installDir: string) => Promise + installFromNpmPackageName: (name: string, installDir: string) => Promise findStoreExtensionByIdentifier: (identifier: string) => ExtPackageJsonExtra | undefined registerNewExtensionByPath: (extPath: string) => Promise uninstallStoreExtensionByIdentifier: (identifier: string) => Promise upgradeStoreExtension: (identifier: string, tarballUrl: string) => Promise } { - const { subscribe, update, set } = writable([]) + const store = writable([]) function init() { return extAPI.loadAllExtensionsFromDb().then((exts) => { - set(exts) + store.set(exts) }) } function getExtensionsFromStore() { - const extContainerPath = get(appConfig).extensionPath + const extContainerPath = get(appConfig).extensionsInstallDir if (!extContainerPath) return [] return get(extensions).filter((ext) => !extAPI.isExtPathInDev(extContainerPath, ext.extPath)) } @@ -43,7 +46,7 @@ function createExtensionsStore(): Writable & { return extAPI .loadExtensionManifestFromDisk(await path.join(extPath, "package.json")) .then((ext) => { - update((exts) => { + store.update((exts) => { const existingExt = exts.find((e) => e.extPath === ext.extPath) if (existingExt) return exts return [...exts, ext] @@ -56,12 +59,36 @@ function createExtensionsStore(): Writable & { }) } + /** + * Install extension from tarball file + * @param tarballPath absolute path to the tarball file + * @param extsDir absolute path to the extensions directory + * @returns loaded extension + */ + async function installTarball(tarballPath: string, extsDir: string) { + return extAPI.installTarballUrl(tarballPath, extsDir).then((extInstallPath) => { + return registerNewExtensionByPath(extInstallPath) + }) + } + + async function installDevExtensionDir(dirPath: string) { + return extAPI.installDevExtensionDir(dirPath).then((ext) => { + return registerNewExtensionByPath(ext.extPath) + }) + } + async function installFromTarballUrl(tarballUrl: string, extsDir: string) { return extAPI.installTarballUrl(tarballUrl, extsDir).then((extInstallPath) => { return registerNewExtensionByPath(extInstallPath) }) } + async function installFromNpmPackageName(name: string, extsDir: string) { + return extAPI.installFromNpmPackageName(name, extsDir).then((extInstallPath) => { + return registerNewExtensionByPath(extInstallPath) + }) + } + async function uninstallExtensionByPath(targetPath: string) { const targetExt = get(extensions).find((ext) => ext.extPath === targetPath) if (!targetExt) throw new Error(`Extension ${targetPath} not registered in DB`) @@ -69,7 +96,7 @@ function createExtensionsStore(): Writable & { return extAPI .uninstallExtensionByPath(targetPath) - .then(() => update((exts) => exts.filter((ext) => ext.extPath !== targetExt.extPath))) + .then(() => store.update((exts) => exts.filter((ext) => ext.extPath !== targetExt.extPath))) .then(() => targetExt) } @@ -83,7 +110,7 @@ function createExtensionsStore(): Writable & { identifier: string, tarballUrl: string ): Promise { - const extsDir = get(appConfig).extensionPath + const extsDir = get(appConfig).extensionsInstallDir if (!extsDir) throw new Error("Extension path not set") return uninstallStoreExtensionByIdentifier(identifier).then(() => installFromTarballUrl(tarballUrl, extsDir) @@ -91,16 +118,17 @@ function createExtensionsStore(): Writable & { } return { + ...store, init, getExtensionsFromStore, findStoreExtensionByIdentifier, registerNewExtensionByPath, + installTarball, + installDevExtensionDir, installFromTarballUrl, + installFromNpmPackageName, uninstallStoreExtensionByIdentifier, - upgradeStoreExtension, - subscribe, - update, - set + upgradeStoreExtension } } @@ -109,7 +137,7 @@ export const extensions = createExtensionsStore() export const installedStoreExts: Readable = derived( extensions, ($extensionsStore) => { - const extContainerPath = get(appConfig).extensionPath + const extContainerPath = get(appConfig).extensionsInstallDir if (!extContainerPath) return [] return $extensionsStore.filter((ext) => !extAPI.isExtPathInDev(extContainerPath, ext.extPath)) } @@ -117,7 +145,7 @@ export const installedStoreExts: Readable = derived( export const devStoreExts: Readable = derived( extensions, ($extensionsStore) => { - const extContainerPath = get(appConfig).extensionPath + const extContainerPath = get(appConfig).extensionsInstallDir if (!extContainerPath) return [] return $extensionsStore.filter((ext) => extAPI.isExtPathInDev(extContainerPath, ext.extPath)) } diff --git a/apps/desktop/src/lib/stores/index.ts b/apps/desktop/src/lib/stores/index.ts index daaa348..141acbc 100644 --- a/apps/desktop/src/lib/stores/index.ts +++ b/apps/desktop/src/lib/stores/index.ts @@ -2,3 +2,5 @@ export * from "./appConfig" export * from "./appState" export * from "./winExtMap" export * from "./extensions" +export * from "./auth" +export * from "./quick-links" diff --git a/apps/desktop/src/lib/stores/quick-links.ts b/apps/desktop/src/lib/stores/quick-links.ts new file mode 100644 index 0000000..e265ca4 --- /dev/null +++ b/apps/desktop/src/lib/stores/quick-links.ts @@ -0,0 +1,39 @@ +import type { Icon } from "@kksh/api/models" +import { createQuickLinkCommand, getAllQuickLinkCommands } from "@kksh/extension/db" +import type { CmdQuery, QuickLink } from "@kksh/ui/types" +import { get, writable, type Writable } from "svelte/store" + +export interface QuickLinkAPI { + get: () => QuickLink[] + init: () => Promise + refresh: () => Promise + createQuickLink: (name: string, link: string, icon: Icon) => Promise +} + +function createQuickLinksStore(): Writable & QuickLinkAPI { + const store = writable([]) + + async function init() { + refresh() + } + + async function refresh() { + const cmds = await getAllQuickLinkCommands() + 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) { + await createQuickLinkCommand(name, link, icon) + await refresh() + } + + return { + ...store, + get: () => get(store), + init, + refresh, + createQuickLink + } +} + +export const quickLinks = createQuickLinksStore() diff --git a/apps/desktop/src/lib/stores/winExtMap.ts b/apps/desktop/src/lib/stores/winExtMap.ts index 7b5cd5f..2bea26a 100644 --- a/apps/desktop/src/lib/stores/winExtMap.ts +++ b/apps/desktop/src/lib/stores/winExtMap.ts @@ -40,6 +40,7 @@ function createWinExtMapStore(): Writable & API { async function init() {} return { + ...store, init, registerExtensionWithWindow: async ({ extPath, @@ -58,11 +59,11 @@ function createWinExtMapStore(): Writable & API { await killProcesses(winExtMap[windowLabel].pids) delete winExtMap[windowLabel] } else { - winExtMap[windowLabel] = { - windowLabel, - extPath, - pids: [] - } + // winExtMap[windowLabel] = { + // windowLabel, + // extPath, + // pids: [] + // } } } const returnedWinLabel = await registerExtensionWindow({ @@ -70,6 +71,11 @@ function createWinExtMapStore(): Writable & API { windowLabel, dist }) + winExtMap[returnedWinLabel] = { + windowLabel: returnedWinLabel, + extPath, + pids: [] + } store.set(winExtMap) return returnedWinLabel }, @@ -109,10 +115,7 @@ function createWinExtMapStore(): Writable & API { return unregisterExtensionSpawnedProcess(windowLabel, pid).then(() => { ext.pids = ext.pids.filter((p) => p !== pid) }) - }, - subscribe: store.subscribe, - update: store.update, - set: store.set + } } } diff --git a/apps/desktop/src/lib/utils/deeplink.ts b/apps/desktop/src/lib/utils/deeplink.ts new file mode 100644 index 0000000..11bcc7f --- /dev/null +++ b/apps/desktop/src/lib/utils/deeplink.ts @@ -0,0 +1,115 @@ +import { emitRefreshDevExt } from "@/utils/tauri-events" +import { + DEEP_LINK_PATH_AUTH_CONFIRM, + DEEP_LINK_PATH_OPEN, + DEEP_LINK_PATH_REFRESH_DEV_EXTENSION, + DEEP_LINK_PATH_STORE +} from "@kksh/api" +import type { UnlistenFn } from "@tauri-apps/api/event" +import { extname } from "@tauri-apps/api/path" +import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow" +import * as deepLink from "@tauri-apps/plugin-deep-link" +import { error } from "@tauri-apps/plugin-log" +import { goto } from "$app/navigation" +import { toast } from "svelte-sonner" +import * as v from "valibot" +import { isInMainWindow } from "./window" + +const StorePathSearchParams = v.object({ + identifier: v.optional(v.string()) +}) + +export function initDeeplink(): Promise { + console.log("init deeplink") + if (!isInMainWindow()) { + return Promise.resolve(() => {}) + } + // deepLink.getCurrent() + return deepLink.onOpenUrl((urls) => { + console.log("deep link:", urls) + urls.forEach(handleDeepLink) + }) +} + +/** + * Show and focus on the main window + */ +function openMainWindow() { + const appWindow = getCurrentWebviewWindow() + return appWindow + .show() + .then(() => appWindow.setFocus()) + .catch((err) => { + console.error(err) + error(`Failed to show window upon deep link: ${err.message}`) + toast.error("Failed to show window upon deep link", { + description: err.message + }) + }) +} + +export async function handleKunkunProtocol(parsedUrl: URL) { + const params = Object.fromEntries(parsedUrl.searchParams) + const { host, pathname, href } = parsedUrl + if (href.startsWith(DEEP_LINK_PATH_OPEN)) { + openMainWindow() + } else if (href.startsWith(DEEP_LINK_PATH_STORE)) { + const parsed = v.parse(StorePathSearchParams, params) + openMainWindow() + if (parsed.identifier) { + goto(`/extension/store/${parsed.identifier}`) + } else { + goto("/extension/store") + } + } else if (href.startsWith(DEEP_LINK_PATH_REFRESH_DEV_EXTENSION)) { + emitRefreshDevExt() + } else if (href.startsWith(DEEP_LINK_PATH_AUTH_CONFIRM)) { + openMainWindow() + goto(`/auth/confirm?${parsedUrl.searchParams.toString()}`) + } else { + console.error("Invalid path:", pathname) + toast.error("Invalid path", { + description: parsedUrl.href + }) + } +} + +export async function handleFileProtocol(parsedUrl: URL) { + console.log("File protocol:", parsedUrl) + const filePath = parsedUrl.pathname // Remove the leading '//' kunkun://open?identifier=qrcode gives "open" + console.log("File path:", filePath) + // from file absolute path, get file extension + const fileExt = await extname(filePath) + console.log("File extension:", fileExt) + switch (fileExt) { + case "kunkun": + // TODO: Handle file protocol, install extension from file (essentially a .tgz file) + break + default: + console.error("Unknown file extension:", fileExt) + toast.error("Unknown file extension", { + description: fileExt + }) + break + } +} + +/** + * + * @param url Deep Link URl, e.g. kunkun://open + */ +export async function handleDeepLink(url: string) { + const parsedUrl = new URL(url) + switch (parsedUrl.protocol) { + case "kunkun:": + return handleKunkunProtocol(parsedUrl) + case "file:": + return handleFileProtocol(parsedUrl) + default: + console.error("Invalid Protocol:", parsedUrl.protocol) + toast.error("Invalid Protocol", { + description: parsedUrl.protocol + }) + break + } +} diff --git a/apps/desktop/src/lib/utils/dom.ts b/apps/desktop/src/lib/utils/dom.ts new file mode 100644 index 0000000..36bcfa6 --- /dev/null +++ b/apps/desktop/src/lib/utils/dom.ts @@ -0,0 +1,3 @@ +export function getActiveElementNodeName(): string | undefined { + return document.activeElement?.nodeName +} diff --git a/apps/desktop/src/lib/utils/key.ts b/apps/desktop/src/lib/utils/key.ts index 62f16f1..61e9527 100644 --- a/apps/desktop/src/lib/utils/key.ts +++ b/apps/desktop/src/lib/utils/key.ts @@ -23,3 +23,13 @@ export function goBackOnEscapeClearSearchTerm(e: KeyboardEvent) { } } } + +export function goHomeOnEscapeClearSearchTerm(e: KeyboardEvent) { + if (e.key === "Escape") { + if (appState.get().searchTerm) { + appState.clearSearchTerm() + } else { + goHome() + } + } +} diff --git a/apps/desktop/src/lib/utils/sync-store.ts b/apps/desktop/src/lib/utils/sync-store.ts new file mode 100644 index 0000000..667f6a5 --- /dev/null +++ b/apps/desktop/src/lib/utils/sync-store.ts @@ -0,0 +1,44 @@ +import * as evt from "@tauri-apps/api/event" +import { writable, type Writable } from "svelte/store" + +export function buildEventName(storeName: string) { + return `app://sync-store-${storeName}` +} + +export type WithSyncStore = Writable & { + listen: () => void + unlisten: evt.UnlistenFn | undefined +} + +export function createTauriSyncStore(storeName: string, initialValue: T): WithSyncStore { + const store = writable(initialValue) + let unlisten: evt.UnlistenFn | undefined + + async function listen() { + console.log("[listen] start", storeName) + if (unlisten) { + console.log("[listen] already listening, skip") + return + } + const _unlisten = await evt.listen<{ value: T }>(buildEventName(storeName), (evt) => { + console.log(`[listen] update from tauri event`, storeName, evt.payload.value) + store.set(evt.payload.value) + }) + const unsubscribe = store.subscribe((value) => { + console.log("[subscribe] got update, emit data", storeName, value) + evt.emit(buildEventName(storeName), { value }) + }) + unlisten = () => { + _unlisten() + unsubscribe() + unlisten = undefined + } + return unlisten + } + + return { + ...store, + listen, + unlisten + } +} diff --git a/apps/desktop/src/lib/utils/tauri-events.ts b/apps/desktop/src/lib/utils/tauri-events.ts new file mode 100644 index 0000000..1102342 --- /dev/null +++ b/apps/desktop/src/lib/utils/tauri-events.ts @@ -0,0 +1,57 @@ +import { DEEP_LINK_PATH_REFRESH_DEV_EXTENSION } from "@kksh/api" +import { + emit, + emitTo, + listen, + TauriEvent, + type Event, + type EventCallback, + type UnlistenFn +} from "@tauri-apps/api/event" + +export const FileDragDrop = "tauri://drag-drop" +export const FileDragEnter = "tauri://drag-enter" +export const FileDragLeave = "tauri://drag-leave" +export const FileDragOver = "tauri://drag-over" +export const NewClipboardItemAddedEvent = "new_clipboard_item_added" +export const RefreshConfigEvent = "kunkun://refresh-config" +export const RefreshExtEvent = "kunkun://refresh-extensions" +export function listenToFileDrop(cb: EventCallback<{ paths: string[] }>) { + return listen<{ paths: string[] }>(FileDragDrop, cb) +} + +export function listenToWindowBlur(cb: EventCallback) { + return listen(TauriEvent.WINDOW_BLUR, cb) +} + +export function listenToWindowFocus(cb: EventCallback) { + return listen(TauriEvent.WINDOW_FOCUS, cb) +} + +export function listenToNewClipboardItem(cb: EventCallback) { + return listen(NewClipboardItemAddedEvent, cb) +} + +export function emitRefreshConfig() { + return emit(RefreshConfigEvent) +} + +export function listenToRefreshConfig(cb: EventCallback) { + return listen(RefreshConfigEvent, cb) +} + +export function emitRefreshExt() { + return emitTo("main", RefreshExtEvent) +} + +export function listenToRefreshExt(cb: EventCallback) { + return listen(RefreshExtEvent, cb) +} + +export function emitRefreshDevExt() { + return emit(DEEP_LINK_PATH_REFRESH_DEV_EXTENSION) +} + +export function listenToRefreshDevExt(cb: EventCallback) { + return listen(DEEP_LINK_PATH_REFRESH_DEV_EXTENSION, cb) +} diff --git a/apps/desktop/src/lib/utils/updater.ts b/apps/desktop/src/lib/utils/updater.ts new file mode 100644 index 0000000..17705b0 --- /dev/null +++ b/apps/desktop/src/lib/utils/updater.ts @@ -0,0 +1,88 @@ +import { extensions } from "@/stores" +import { supabaseAPI } from "@/supabase" +import { isCompatible } from "@kksh/api" +import type { ExtPackageJsonExtra } from "@kksh/api/models" +import { greaterThan } from "@std/semver" +import { relaunch } from "@tauri-apps/plugin-process" +import { check } from "@tauri-apps/plugin-updater" +import { gt } from "semver" +import { toast } from "svelte-sonner" +import { get } from "svelte/store" + +export async function checkUpdateAndInstall({ beta }: { beta?: boolean } = {}) { + const update = await check({ + headers: { + "kk-updater-mode": beta ? "beta" : "stable" + } + }) + if (update?.available) { + const confirmUpdate = await confirm( + `A new version ${update.version} is available. Do you want to install and relaunch?` + ) + if (confirmUpdate) { + await update.downloadAndInstall() + await relaunch() + } + } else { + toast.info("You are on the latest version") + } +} + +export async function checkSingleExtensionUpdate( + installedExt: ExtPackageJsonExtra, + autoupgrade: boolean +) { + const { data: sbExt, error } = await supabaseAPI.getLatestExtPublish( + installedExt.kunkun.identifier + ) + if (error) { + return toast.error(`Failed to check update for ${installedExt.kunkun.identifier}: ${error}`) + } + + if (!sbExt) { + return null + } + + if ( + gt(sbExt.version, installedExt.version) && + (sbExt.api_version ? isCompatible(sbExt.api_version) : true) + ) { + if (autoupgrade) { + await extensions + .upgradeStoreExtension( + sbExt.identifier, + supabaseAPI.translateExtensionFilePathToUrl(sbExt.tarball_path) + ) + .then(() => { + toast.success(`${sbExt.name} upgraded`, { + description: `From ${installedExt.version} to ${sbExt.version}` + }) + }) + .catch((err) => { + toast.error(`Failed to upgrade ${sbExt.name}`, { description: err }) + }) + return true + } else { + console.log(`new version available ${installedExt.kunkun.identifier} ${sbExt.version}`) + toast.info( + `Extension ${installedExt.kunkun.identifier} has a new version ${sbExt.version}, you can upgrade in Store.`, + { duration: 10_000 } + ) + } + } + return false +} + +export async function checkExtensionUpdate(autoupgrade: boolean = false) { + let upgradedCount = 0 + for (const ext of get(extensions)) { + const upgraded = await checkSingleExtensionUpdate(ext, autoupgrade) + if (upgraded) { + upgradedCount++ + } + } + + if (upgradedCount > 0) { + toast.info(`${upgradedCount} extensions have been upgraded`) + } +} diff --git a/apps/desktop/src/routes/+error.svelte b/apps/desktop/src/routes/+error.svelte index 91a5da3..a4ae1af 100644 --- a/apps/desktop/src/routes/+error.svelte +++ b/apps/desktop/src/routes/+error.svelte @@ -1,4 +1,5 @@ - + + { + return commandScore( + value.startsWith("{") ? (JSON.parse(value) as CmdValue).cmdName : value, + search, + keywords + ) + }} + loop +> + + {#snippet rightSlot()} + + {#each $cmdQueries as cmdQuery} + {@const queryWidth = Math.max(cmdQuery.name.length, cmdQuery.value.length) + 2} + { + if (evt.key === "Enter") { + evt.preventDefault() + evt.stopPropagation() + commandLaunchers.onQuickLinkSelect( + JSON.parse($appState.highlightedCmd), + $cmdQueries + ) + } + }} + bind:value={cmdQuery.value} + /> + {/each} + + + + + + + + + Settings + + exit()}>Quit + openDevTools()}>Open Dev Tools + getCurrentWebviewWindow().hide()} + >Close Window + + + + {/snippet} + + + No results found. + {#if $appConfig.extensionsInstallDir && $devStoreExts.length > 0} + + {/if} + {#if $appConfig.extensionsInstallDir && $installedStoreExts.length > 0} + + {/if} + + + + + + diff --git a/apps/desktop/src/routes/auth/+page.svelte b/apps/desktop/src/routes/auth/+page.svelte new file mode 100644 index 0000000..4a42802 --- /dev/null +++ b/apps/desktop/src/routes/auth/+page.svelte @@ -0,0 +1,62 @@ + + + + +
+ + + + Kunkun + Sign In + + + + + + + diff --git a/apps/desktop/src/routes/auth/confirm/+page.svelte b/apps/desktop/src/routes/auth/confirm/+page.svelte new file mode 100644 index 0000000..78e2035 --- /dev/null +++ b/apps/desktop/src/routes/auth/confirm/+page.svelte @@ -0,0 +1,84 @@ + + + + +
+
+
+
+ {#if $auth.session} + Welcome, You are Logged In + {:else} + You Are Not Logged In + {/if} + + {#if $auth.session} + + + {avatarFallback} + + {/if} + + +
+
+
diff --git a/apps/desktop/src/routes/auth/confirm/+page.ts b/apps/desktop/src/routes/auth/confirm/+page.ts new file mode 100644 index 0000000..3418ef7 --- /dev/null +++ b/apps/desktop/src/routes/auth/confirm/+page.ts @@ -0,0 +1,10 @@ +import { error } from "@sveltejs/kit" +import type { PageLoad } from "./$types" + +export const load: PageLoad = async ({ params, url }) => { + const code = url.searchParams.get("code") + if (!code) { + throw error(400, "Auth Exchange Code is Required") + } + return { params, code } +} diff --git a/apps/desktop/src/routes/extension/create-quick-link/+page.svelte b/apps/desktop/src/routes/extension/create-quick-link/+page.svelte new file mode 100644 index 0000000..4e782f4 --- /dev/null +++ b/apps/desktop/src/routes/extension/create-quick-link/+page.svelte @@ -0,0 +1,108 @@ + + + + +
+
+

Create Quick Link

+
+ + + {#snippet children({ props })} + Name + + {/snippet} + + Quick Link Display Name + + + + + {#snippet children({ props })} + Link + + {/snippet} + + Quick Link URL + + + + + + + Submit + +
+{#if dev} +
+ +
+{/if} diff --git a/apps/desktop/src/routes/extension/create-quick-link/schema.ts b/apps/desktop/src/routes/extension/create-quick-link/schema.ts new file mode 100644 index 0000000..c4de730 --- /dev/null +++ b/apps/desktop/src/routes/extension/create-quick-link/schema.ts @@ -0,0 +1,7 @@ +import { z } from "zod" + +export const formSchema = z.object({ + username: z.string().min(2).max(50) +}) + +export type FormSchema = typeof formSchema diff --git a/apps/desktop/src/routes/extension/permission-inspector/+page.svelte b/apps/desktop/src/routes/extension/permission-inspector/+page.svelte new file mode 100644 index 0000000..2294407 --- /dev/null +++ b/apps/desktop/src/routes/extension/permission-inspector/+page.svelte @@ -0,0 +1,100 @@ + + + + +
+ +

Extension Permission Inspector

+ +
+ {#each pkgJsons as pkgJson} + + + {pkgJson.kunkun.name} + {pkgJson.kunkun.shortDescription} + + + + + +

+ Identifier: {pkgJson.kunkun.identifier} +

+

+ Extension Path: {pkgJson.extPath} +

+
+
+ {/each} +
+
+ + diff --git a/apps/desktop/src/routes/extension/store/+page.svelte b/apps/desktop/src/routes/extension/store/+page.svelte index 7f53ef9..d8d7a1f 100644 --- a/apps/desktop/src/routes/extension/store/+page.svelte +++ b/apps/desktop/src/routes/extension/store/+page.svelte @@ -2,8 +2,8 @@ import { getExtensionsFolder } from "@/constants" import { appState, extensions } from "@/stores" import { supabaseAPI } from "@/supabase" - import { goBackOnEscapeClearSearchTerm } from "@/utils/key" - import { goBack } from "@/utils/route" + import { goBackOnEscapeClearSearchTerm, goHomeOnEscapeClearSearchTerm } from "@/utils/key" + import { goBack, goHome } from "@/utils/route" import { SBExt } from "@kksh/api/supabase" import { isUpgradable } from "@kksh/extension" import { Button, Command } from "@kksh/svelte5" @@ -64,20 +64,19 @@ } - - + {#snippet leftSlot()} {/snippet} - + - + goto("/")} + onGoBack={() => goto("/")} rawJsonError={JSON.stringify($page, null, 2)} /> diff --git a/apps/desktop/src/routes/extension/store/[identifier]/+page.svelte b/apps/desktop/src/routes/extension/store/[identifier]/+page.svelte index 67ce854..55a7f97 100644 --- a/apps/desktop/src/routes/extension/store/[identifier]/+page.svelte +++ b/apps/desktop/src/routes/extension/store/[identifier]/+page.svelte @@ -9,13 +9,15 @@ import { StoreExtDetail } from "@kksh/ui/extension" import { greaterThan, parse as parseSemver } from "@std/semver" import { error } from "@tauri-apps/plugin-log" + import { goto } from "$app/navigation" import { ArrowLeftIcon } from "lucide-svelte" import { onMount } from "svelte" import { toast } from "svelte-sonner" import { get, derived as storeDerived } from "svelte/store" const { data } = $props() - let { ext, manifest } = data + const ext = $derived(data.ext) + const manifest = $derived(data.manifest) const installedExt = storeDerived(installedStoreExts, ($e) => { return $e.find((e) => e.kunkun.identifier === ext.identifier) }) @@ -36,9 +38,9 @@ onMount(() => { showBtn = { - install: !installedExt, + install: !$installedExt, upgrade: isUpgradable, - uninstall: !!installedExt + uninstall: !!$installedExt } }) @@ -114,43 +116,44 @@ .uninstallStoreExtensionByIdentifier(ext.identifier) .then((uninstalledExt) => { toast.success(`${uninstalledExt.name} Uninstalled`) + loading.uninstall = false + showBtn.uninstall = false + showBtn.install = true }) .catch((err) => { toast.error("Fail to uninstall extension", { description: err }) error(`Fail to uninstall store extension (${ext.identifier}): ${err}`) }) - .finally(() => { - loading.uninstall = false - showBtn.uninstall = false - showBtn.install = true - }) + .finally(() => {}) } function onEnterPressed() { - return onInstallSelected() + if (showBtn.install) { + return onInstallSelected() + } } function handleKeydown(e: KeyboardEvent) { if (e.key === "Escape") { if (!delayedImageDialogOpen) { - goBack() + goto("/extension/store") } } } - manifest: KunkunExtManifest + params: { + identifier: string + } }> => { + console.log("store[identifier] params", params) + const { error: dbError, data: ext } = await supabaseAPI.getLatestExtPublish(params.identifier) if (dbError) { return error(400, { @@ -30,6 +35,7 @@ export const load: PageLoad = async ({ return { ext, + params, manifest: parseManifest.output } } diff --git a/apps/desktop/src/routes/extension/ui-worker/+page.svelte b/apps/desktop/src/routes/extension/ui-worker/+page.svelte index 5bf03f4..2dffffc 100644 --- a/apps/desktop/src/routes/extension/ui-worker/+page.svelte +++ b/apps/desktop/src/routes/extension/ui-worker/+page.svelte @@ -1 +1,289 @@ - + + +{#if loadingBar} + +{/if} +{#if loaded && listViewContent !== undefined} + { + workerAPI?.onListScrolledToBottom() + }} + onEnterKeyPressed={() => { + workerAPI?.onEnterPressedOnSearchBar() + }} + onListItemSelected={(value: string) => { + workerAPI?.onListItemSelected(value) + }} + onSearchTermChange={(searchTerm) => { + workerAPI?.onSearchTermChange(searchTerm) + }} + onHighlightedItemChanged={(value) => { + workerAPI?.onHighlightedListItemChanged(value) + if (listViewContent?.defaultAction) { + appState.setDefaultAction(listViewContent.defaultAction) + } + if (listViewContent?.actions) { + appState.setActionPanel(listViewContent.actions) + } + }} + > + {#snippet footer()} + { + workerAPI?.onEnterPressedOnSearchBar() + }} + onActionSelected={(value) => { + workerAPI?.onActionSelected(value) + }} + /> + {/snippet} + +{:else if loaded && formViewContent !== undefined} + +{:else if loaded && markdownViewContent !== undefined} + +{/if} diff --git a/apps/desktop/src/routes/extension/ui-worker/+page.ts b/apps/desktop/src/routes/extension/ui-worker/+page.ts new file mode 100644 index 0000000..711ac4a --- /dev/null +++ b/apps/desktop/src/routes/extension/ui-worker/+page.ts @@ -0,0 +1,76 @@ +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 } from "@sveltejs/kit" +import { join } from "@tauri-apps/api/path" +import { exists, readTextFile } from "@tauri-apps/plugin-fs" +import { error } from "@tauri-apps/plugin-log" +import { goto } from "$app/navigation" +import { toast } from "svelte-sonner" +import type { PageLoad } from "./$types" + +// : Promise<{ +// extPath: string +// scriptPath: string +// // workerScript: string +// pkgJsonPath: string +// cmdName: string +// loadedExt: ExtPackageJsonExtra +// extInfoInDB: ExtInfoInDB +// }> + +export const load: PageLoad = async ({ url }) => { + // both query parameter must exist + const extPath = url.searchParams.get("extPath") + const cmdName = url.searchParams.get("cmdName") + if (!extPath || !cmdName) { + toast.error("Invalid extension path or url") + error("Invalid extension path or url") + goto("/") + } + + let _loadedExt: ExtPackageJsonExtra | undefined + try { + _loadedExt = await loadExtensionManifestFromDisk(await join(extPath!, "package.json")) + } catch (err) { + error(`Error loading extension manifest: ${err}`) + toast.error("Error loading extension manifest", { + description: `${err}` + }) + goto("/") + } + const loadedExt = _loadedExt! + const extInfoInDB = await db.getUniqueExtensionByPath(loadedExt.extPath) + if (!extInfoInDB) { + toast.error("Unexpected Error", { + description: `Extension ${loadedExt.kunkun.identifier} not found in database. Run Troubleshooter.` + }) + goto("/") + } + const pkgJsonPath = await join(extPath!, "package.json") + if (!(await exists(extPath!))) { + sbError(404, `Extension not found at ${extPath}`) + } + if (!(await exists(pkgJsonPath))) { + sbError(404, `Extension package.json not found at ${pkgJsonPath}`) + } + + const cmd = loadedExt.kunkun.templateUiCmds.find((cmd) => cmd.name === cmdName) + if (!cmd) { + sbError(404, `Command ${cmdName} not found in extension ${loadedExt.kunkun.identifier}`) + } + const scriptPath = await join(loadedExt.extPath, cmd.main) + if (!(await exists(scriptPath))) { + sbError(404, `Command script not found at ${scriptPath}`) + } + // const workerScript = await readTextFile(scriptPath) + return { + extPath: extPath!, + pkgJsonPath, + scriptPath, + // workerScript, + cmdName: cmdName!, + loadedExt, + extInfoInDB: extInfoInDB! + } +} diff --git a/apps/desktop/src/routes/settings/add-dev-extension/+page.svelte b/apps/desktop/src/routes/settings/add-dev-extension/+page.svelte new file mode 100644 index 0000000..e8d9d1b --- /dev/null +++ b/apps/desktop/src/routes/settings/add-dev-extension/+page.svelte @@ -0,0 +1,31 @@ + + + + +
+
+

Add Dev Extension

+ + There are 4 options to install an extension in developer mode. Either load it from your local + tarball file, a tarball remote URL, npm package name or load from a remote URL. + + +
diff --git a/apps/desktop/src/routes/settings/set-dev-ext-path/+page.svelte b/apps/desktop/src/routes/settings/set-dev-ext-path/+page.svelte index 867100e..5d5d0ee 100644 --- a/apps/desktop/src/routes/settings/set-dev-ext-path/+page.svelte +++ b/apps/desktop/src/routes/settings/set-dev-ext-path/+page.svelte @@ -6,8 +6,8 @@ import { ArrowLeftIcon } from "lucide-svelte" - -
diff --git a/apps/desktop/src/routes/troubleshooters/extension-loading/+page.svelte b/apps/desktop/src/routes/troubleshooters/extension-loading/+page.svelte new file mode 100644 index 0000000..df4901d --- /dev/null +++ b/apps/desktop/src/routes/troubleshooters/extension-loading/+page.svelte @@ -0,0 +1,129 @@ + + + + +
+
+

Extension Loading Troubleshooter

+ + + + + Error Details + + {errorMsg} + + + + A list of your extensions. + + + Identifier + Path + Error + + + + {#each sortedResults as row} + +
{row.identifier}
+ + + + + + +
+ {/each} +
+
+
+ + diff --git a/apps/desktop/src/routes/troubleshooters/extension-window/+page.svelte b/apps/desktop/src/routes/troubleshooters/extension-window/+page.svelte new file mode 100644 index 0000000..08c0044 --- /dev/null +++ b/apps/desktop/src/routes/troubleshooters/extension-window/+page.svelte @@ -0,0 +1,111 @@ + + + + +
+
+
+ + +
+ + + Refreshed {refreshCount} times + +
+ + {#each Object.entries(winLabelMap) as [label, content]} +
  • + + Label: +
    {label}
    +
    +
      + {#each Object.entries(content) as [key, value]} +
    • + + {key}: +
      {value}
      +
      +
    • + {/each} +
    + +
  • + {/each} +
    +
    diff --git a/apps/desktop/tailwind.config.ts b/apps/desktop/tailwind.config.ts index fbc57f7..95c4e88 100644 --- a/apps/desktop/tailwind.config.ts +++ b/apps/desktop/tailwind.config.ts @@ -1,3 +1,4 @@ +import typography from "@tailwindcss/typography" import type { Config } from "tailwindcss" import tailwindcssAnimate from "tailwindcss-animate" import { fontFamily } from "tailwindcss/defaultTheme" @@ -9,7 +10,7 @@ const config: Config = { "./node_modules/@kksh/ui/src/**/*.{html,js,svelte,ts}", "../../node_modules/@kksh/svelte5/src/**/*.{html,js,svelte,ts}" ], - safelist: ["dark"], + safelist: ["dark", "bg-red-500/30"], theme: { container: { center: true, @@ -94,7 +95,7 @@ const config: Config = { } } }, - plugins: [tailwindcssAnimate] + plugins: [tailwindcssAnimate, typography] } export default config diff --git a/apps/desktop/tsconfig.json b/apps/desktop/tsconfig.json index 593dc19..a2288c7 100644 --- a/apps/desktop/tsconfig.json +++ b/apps/desktop/tsconfig.json @@ -1,19 +1,20 @@ { - "extends": "./.svelte-kit/tsconfig.json", - "compilerOptions": { - "allowJs": true, - "checkJs": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "sourceMap": true, - "strict": 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 - // - // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes - // from the referenced tsconfig.json - TypeScript does not merge them in + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": 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 + // + // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes + // from the referenced tsconfig.json - TypeScript does not merge them in + "include": ["src/**/*"] } diff --git a/apps/desktop/uno.config.ts b/apps/desktop/uno.config.ts new file mode 100644 index 0000000..7ca4602 --- /dev/null +++ b/apps/desktop/uno.config.ts @@ -0,0 +1,5 @@ +import { defineConfig, presetAttributify, presetTagify, presetUno } from "unocss" + +export default defineConfig({ + presets: [presetUno(), presetAttributify(), presetTagify()] +}) diff --git a/apps/desktop/vite.config.js b/apps/desktop/vite.config.js index 7aa392c..09ea201 100644 --- a/apps/desktop/vite.config.js +++ b/apps/desktop/vite.config.js @@ -1,4 +1,5 @@ import { sveltekit } from "@sveltejs/kit/vite" +import UnoCSS from "unocss/vite" import { defineConfig } from "vite" // @ts-expect-error process is a nodejs global @@ -6,8 +7,7 @@ const host = process.env.TAURI_DEV_HOST // https://vitejs.dev/config/ export default defineConfig(async () => ({ - plugins: [sveltekit()], - + plugins: [UnoCSS(), sveltekit()], // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` // // 1. prevent vite from obscuring rust errors diff --git a/package.json b/package.json index c980dcc..382d7fb 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "devDependencies": { "@ianvs/prettier-plugin-sort-imports": "^4.3.1", "@kksh/api": "workspace:*", - "@kksh/svelte5": "0.1.2-beta.4", + "@kksh/svelte5": "0.1.2-beta.8", "prettier": "^3.2.5", "prettier-plugin-svelte": "^3.2.7", "prettier-plugin-tailwindcss": "^0.6.8", diff --git a/packages/api/jsr.json b/packages/api/jsr.json index 334de1a..37cc4c3 100644 --- a/packages/api/jsr.json +++ b/packages/api/jsr.json @@ -1,7 +1,7 @@ { "$schema": "https://jsr.io/schema/config-file.v1.json", "name": "@kunkun/api", - "version": "0.0.27", + "version": "0.0.28", "license": "MIT", "exports": { ".": "./src/index.ts", diff --git a/packages/api/package.json b/packages/api/package.json index f171610..96ea468 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@kksh/api", - "version": "0.0.27", + "version": "0.0.28", "type": "module", "exports": { ".": "./src/index.ts", diff --git a/packages/api/src/commands/extension.ts b/packages/api/src/commands/extension.ts index d7b02d0..66481bb 100644 --- a/packages/api/src/commands/extension.ts +++ b/packages/api/src/commands/extension.ts @@ -26,7 +26,6 @@ export function registerExtensionWindow(options: { } export function unregisterExtensionWindow(label: string): Promise { - console.log("unregisterExtensionWindow", label) return invoke(generateJarvisPluginCommand("unregister_extension_window"), { label }) diff --git a/packages/api/src/commands/system.ts b/packages/api/src/commands/system.ts index 953f7fb..40746be 100644 --- a/packages/api/src/commands/system.ts +++ b/packages/api/src/commands/system.ts @@ -295,7 +295,7 @@ export const rawSystemCommands = [ } ] -export async function getSystemCommands(): Promise { +export function getSystemCommands(): SysCommand[] { return rawSystemCommands .filter(async (cmd) => cmd.platforms.includes(platform())) // Filter out system commands that are not supported on the current platform .map((cmd) => ({ diff --git a/packages/api/src/constants.ts b/packages/api/src/constants.ts index a3b8789..83fda7a 100644 --- a/packages/api/src/constants.ts +++ b/packages/api/src/constants.ts @@ -7,7 +7,7 @@ export enum KUNKUN_EXT_IDENTIFIER { } export const KUNKUN_DESKTOP_APP_SERVER_PORTS = [1566, 1567, 1568, 9559, 9560, 9561] -export const DESKTOP_SERVICE_NAME = "Kunkun" +export const DESKTOP_SERVICE_NAME = "kunkun" /* -------------------------------------------------------------------------- */ /* Deep Link */ diff --git a/packages/api/src/dev/index.ts b/packages/api/src/dev/index.ts index 196fc7b..dd04977 100644 --- a/packages/api/src/dev/index.ts +++ b/packages/api/src/dev/index.ts @@ -15,7 +15,7 @@ export function checkLocalKunkunService(port: number): Promise { return res.json() }) .then((data) => { - return data["service_name"] === DESKTOP_SERVICE_NAME + return data["service_name"].toLowerCase() === DESKTOP_SERVICE_NAME.toLowerCase() }) .catch((err) => { // fetch fail, i.e. server not on this port diff --git a/packages/api/src/models/extension.ts b/packages/api/src/models/extension.ts index 0b1a150..4c7f2e6 100644 --- a/packages/api/src/models/extension.ts +++ b/packages/api/src/models/extension.ts @@ -41,6 +41,8 @@ export type Ext = InferOutput export enum CmdTypeEnum { HeadlessWorker = "headless_worker", + Builtin = "builtin", + System = "system", UiWorker = "ui_worker", UiIframe = "ui_iframe", QuickLink = "quick_link", @@ -55,12 +57,18 @@ export const ExtCmd = object({ name: string(), type: CmdType, data: string(), - alias: optional(string()), - hotkey: optional(string()), + alias: nullable(optional(string())), + hotkey: nullable(optional(string())), enabled: boolean() }) export type ExtCmd = InferOutput +export const QuickLinkCmd = object({ + ...ExtCmd.entries, + data: object({ link: string(), icon: Icon }) +}) +export type QuickLinkCmd = InferOutput + export const ExtData = object({ dataId: number(), extId: number(), diff --git a/packages/api/src/models/icon.ts b/packages/api/src/models/icon.ts index 6639ba5..26872e9 100644 --- a/packages/api/src/models/icon.ts +++ b/packages/api/src/models/icon.ts @@ -1,4 +1,13 @@ -import { enum_, literal, object, string, type InferOutput } from "valibot" +import { + boolean, + enum_, + literal, + nullable, + object, + optional, + string, + type InferOutput +} from "valibot" import { NodeName, NodeNameEnum } from "./constants" /* -------------------------------------------------------------------------- */ @@ -16,7 +25,8 @@ export type IconType = InferOutput export const Icon = object({ type: IconType, - value: string() + value: string(), + invert: optional(boolean()) }) export type Icon = InferOutput export const IconNode = object({ diff --git a/packages/api/src/ui/client.ts b/packages/api/src/ui/client.ts index c4aff94..d2a0d44 100644 --- a/packages/api/src/ui/client.ts +++ b/packages/api/src/ui/client.ts @@ -33,6 +33,7 @@ import type { fileSearch } from "../commands/fileSearch" import { type AppInfo } from "../models/apps" import type { LightMode, Position, Radius, ThemeColor } from "../models/styles" import type { DenoSysOptions } from "../permissions/schema" +import type { MarkdownSchema } from "./worker" import { type IComponent } from "./worker/components/interfaces" import type { Markdown } from "./worker/components/markdown" import * as FormSchema from "./worker/schema/form" @@ -116,7 +117,7 @@ export interface IToast { } export interface IUiWorker { - render: (view: IComponent) => Promise + render: (view: IComponent) => Promise goBack: () => Promise showLoadingBar: (loading: boolean) => Promise setScrollLoading: (loading: boolean) => Promise diff --git a/packages/api/src/ui/worker/components/form-view.ts b/packages/api/src/ui/worker/components/form-view.ts index 5f25a7c..7cb661c 100644 --- a/packages/api/src/ui/worker/components/form-view.ts +++ b/packages/api/src/ui/worker/components/form-view.ts @@ -145,6 +145,9 @@ export class Form implements IComponent { constructor(model: OmitNodeName) { this.fields = model.fields this.key = model.key + this.title = model.title + this.description = model.description + this.submitBtnText = model.submitBtnText } toModel(): FormSchema.Form { diff --git a/packages/api/src/ui/worker/schema/form.ts b/packages/api/src/ui/worker/schema/form.ts index 711dc82..7b1c125 100644 --- a/packages/api/src/ui/worker/schema/form.ts +++ b/packages/api/src/ui/worker/schema/form.ts @@ -65,7 +65,8 @@ export type BaseField = InferOutput export const InputField = object({ ...BaseField.entries, type: optional(InputTypes), - component: optional(union([literal("textarea"), literal("default")])) + component: optional(union([literal("textarea"), literal("default")])), + default: optional(string()) }) export type InputField = InferOutput @@ -74,7 +75,8 @@ export type InputField = InferOutput /* -------------------------------------------------------------------------- */ export const NumberField = object({ ...BaseField.entries, - nodeName: FormNodeName + nodeName: FormNodeName, + default: optional(number()) }) export type NumberField = InferOutput @@ -84,7 +86,8 @@ export type NumberField = InferOutput // with zod enum export const SelectField = object({ ...BaseField.entries, - options: array(string()) + options: array(string()), + default: optional(string()) }) export type SelectField = InferOutput @@ -101,7 +104,8 @@ export type BooleanField = InferOutput /* Date */ /* -------------------------------------------------------------------------- */ export const DateField = object({ - ...BaseField.entries + ...BaseField.entries, + default: optional(string()) }) export type DateField = InferOutput @@ -121,14 +125,22 @@ export type ArrayField = InferOutput /* -------------------------------------------------------------------------- */ export const FormField = union([ ArrayField, // this must be placed first, otherwise its content field won't be parsed + SelectField, InputField, NumberField, - SelectField, BooleanField, DateField ]) export type FormField = InferOutput // export type Form = InferOutput +export const Form: GenericSchema
    = object({ + nodeName: FormNodeName, + key: string(), + fields: array(union([lazy(() => Form), FormField])), + title: optional(string()), + description: optional(string()), + submitBtnText: optional(string()) +}) export type Form = { nodeName: FormNodeName title?: string @@ -137,8 +149,3 @@ export type Form = { key: string fields: (FormField | Form)[] } -export const Form: GenericSchema = object({ - nodeName: FormNodeName, - key: string(), - fields: array(union([lazy(() => Form), FormField])) -}) diff --git a/packages/api/src/version.ts b/packages/api/src/version.ts index ec7bba0..e056254 100644 --- a/packages/api/src/version.ts +++ b/packages/api/src/version.ts @@ -13,7 +13,7 @@ export const breakingChangesVersionCheckpoints = [ const checkpointVersions = breakingChangesVersionCheckpoints.map((c) => c.version) const sortedCheckpointVersions = sort(checkpointVersions) -export const version = "0.0.27" +export const version = "0.0.28" export function isVersionBetween(v: string, start: string, end: string) { const vCleaned = clean(v) diff --git a/packages/extension/src/db.ts b/packages/extension/src/db.ts index a6246ba..b76d7e4 100644 --- a/packages/extension/src/db.ts +++ b/packages/extension/src/db.ts @@ -1,5 +1,13 @@ import { db } from "@kksh/api/commands" -import { ExtPackageJson, ExtPackageJsonExtra } from "@kksh/api/models" +import { + CmdTypeEnum, + ExtCmd, + ExtPackageJson, + ExtPackageJsonExtra, + Icon, + QuickLinkCmd +} from "@kksh/api/models" +import * as v from "valibot" export async function upsertExtension(extPkgJson: ExtPackageJson, extFullPath: string) { const extInDb = await db.getUniqueExtensionByIdentifier(extPkgJson.kunkun.identifier) @@ -12,3 +20,38 @@ export async function upsertExtension(extPkgJson: ExtPackageJson, extFullPath: s }) } } + +export async function createQuickLinkCommand(name: string, link: string, icon: Icon) { + const extension = await db.getExtQuickLinks() + return db.createCommand({ + extId: extension.extId, + name, + cmdType: CmdTypeEnum.QuickLink, + data: JSON.stringify({ + link, + icon + }), + enabled: true + }) +} + +export async function getAllQuickLinkCommands(): Promise { + const extension = await db.getExtQuickLinks() + const cmds = await db.getCommandsByExtId(extension.extId) + return cmds + .map((cmd) => { + try { + cmd.data = JSON.parse(cmd.data) + const parsedData = v.safeParse(QuickLinkCmd, cmd) + if (!parsedData.success) { + console.warn("Fail to parse quick link command", cmd) + console.error(v.flatten(parsedData.issues)) + return null + } + return parsedData.output + } catch (error) { + return null + } + }) + .filter((cmd) => cmd !== null) +} diff --git a/packages/extension/src/install.ts b/packages/extension/src/install.ts index 0704f26..4a65756 100644 --- a/packages/extension/src/install.ts +++ b/packages/extension/src/install.ts @@ -88,6 +88,11 @@ export async function installTarballUrl(tarballUrl: string, extsDir: string): Pr } } +/** + * Install dev extension from a local directory + * @param extPath Path to the extension directory + * @returns + */ export async function installDevExtensionDir(extPath: string): Promise { const manifestPath = await path.join(extPath, "package.json") if (!(await fs.exists(manifestPath))) { diff --git a/packages/extensions/demo-worker-template-ext/.gitignore b/packages/extensions/demo-worker-template-ext/.gitignore new file mode 100644 index 0000000..dc364a9 --- /dev/null +++ b/packages/extensions/demo-worker-template-ext/.gitignore @@ -0,0 +1,176 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Caches + +.cache + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store +extensions_support/ diff --git a/packages/extensions/demo-worker-template-ext/CHANGELOG.md b/packages/extensions/demo-worker-template-ext/CHANGELOG.md new file mode 100644 index 0000000..f8fbdad --- /dev/null +++ b/packages/extensions/demo-worker-template-ext/CHANGELOG.md @@ -0,0 +1,8 @@ +# demo-template-extension + +## 0.0.3 + +### Patch Changes + +- Updated dependencies + - @kksh/api@0.0.9 diff --git a/packages/extensions/demo-worker-template-ext/README.md b/packages/extensions/demo-worker-template-ext/README.md new file mode 100644 index 0000000..eb93d75 --- /dev/null +++ b/packages/extensions/demo-worker-template-ext/README.md @@ -0,0 +1,15 @@ +# tempalte-ext-worker + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +This project was created using `bun init` in bun v1.1.20. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/packages/extensions/demo-worker-template-ext/buffer.ts b/packages/extensions/demo-worker-template-ext/buffer.ts new file mode 100644 index 0000000..4f1715a --- /dev/null +++ b/packages/extensions/demo-worker-template-ext/buffer.ts @@ -0,0 +1,3 @@ +import Buffer from "node:buffer" + +console.log(Buffer) diff --git a/packages/extensions/demo-worker-template-ext/build.ts b/packages/extensions/demo-worker-template-ext/build.ts new file mode 100644 index 0000000..d85f263 --- /dev/null +++ b/packages/extensions/demo-worker-template-ext/build.ts @@ -0,0 +1,31 @@ +import { watch } from "fs" +import { join } from "path" +import { refreshTemplateWorkerExtension } from "@kksh/api/dev" +import { $ } from "bun" + +async function build() { + try { + // await $`bun build --minify --target=browser --outdir=./dist ./src/index.ts` + const output = await Bun.build({ + entrypoints: ["./src/index.ts"], + outdir: "./dist", + minify: true, + target: "browser" + }) + console.log(output) + await refreshTemplateWorkerExtension() + } catch (error) { + console.error(error) + } +} + +const srcDir = join(import.meta.dir, "src") + +await build() + +if (Bun.argv.includes("dev")) { + console.log(`Watching ${srcDir} for changes...`) + watch(srcDir, { recursive: true }, async (event, filename) => { + await build() + }) +} diff --git a/packages/extensions/demo-worker-template-ext/deno-src/deno.json b/packages/extensions/demo-worker-template-ext/deno-src/deno.json new file mode 100644 index 0000000..5861f34 --- /dev/null +++ b/packages/extensions/demo-worker-template-ext/deno-src/deno.json @@ -0,0 +1,5 @@ +{ + "imports": { + "@kunkun/api": "jsr:@kunkun/api@^0.0.14" + } +} diff --git a/packages/extensions/demo-worker-template-ext/deno-src/deno.lock b/packages/extensions/demo-worker-template-ext/deno-src/deno.lock new file mode 100644 index 0000000..be78ed3 --- /dev/null +++ b/packages/extensions/demo-worker-template-ext/deno-src/deno.lock @@ -0,0 +1,23 @@ +{ + "version": "4", + "specifiers": { + "jsr:@hk/comlink-stdio@~0.1.5": "0.1.5", + "jsr:@kunkun/api@^0.0.14": "0.0.14" + }, + "jsr": { + "@hk/comlink-stdio@0.1.5": { + "integrity": "1fd67d5d53ab4571e745584d66b480b5be402f6ca6b2c9e591230fa1d23f85ee" + }, + "@kunkun/api@0.0.14": { + "integrity": "a21a255748164992ca93fc292451677261dffca336922a6bed7eb8703c6e880b", + "dependencies": [ + "jsr:@hk/comlink-stdio" + ] + } + }, + "workspace": { + "dependencies": [ + "jsr:@kunkun/api@^0.0.14" + ] + } +} diff --git a/packages/extensions/demo-worker-template-ext/deno-src/rpc.ts b/packages/extensions/demo-worker-template-ext/deno-src/rpc.ts new file mode 100644 index 0000000..0e09884 --- /dev/null +++ b/packages/extensions/demo-worker-template-ext/deno-src/rpc.ts @@ -0,0 +1,13 @@ +import { expose } from "@kunkun/api/runtime/deno" + +export interface API { + add(a: number, b: number): Promise + subtract(a: number, b: number): Promise +} + +// Define your API methods +export const apiMethods: API = { + add: async (a: number, b: number) => a + b, + subtract: async (a: number, b: number) => a - b +} +expose(apiMethods) diff --git a/packages/extensions/demo-worker-template-ext/package.json b/packages/extensions/demo-worker-template-ext/package.json new file mode 100644 index 0000000..68de2ad --- /dev/null +++ b/packages/extensions/demo-worker-template-ext/package.json @@ -0,0 +1,114 @@ +{ + "$schema": "../../schema/manifest-json-schema.json", + "name": "demo-template-extension", + "version": "0.0.3", + "type": "module", + "kunkun": { + "name": "Demo Template Extension", + "shortDescription": "Demo Template Extension", + "longDescription": "Demo Template Extension", + "identifier": "demo-worker-template-ext", + "permissions": [ + "fetch:all", + "shell:kill", + "security:mac:all", + { + "permission": "shell:deno:execute", + "allow": [ + { + "path": "$EXTENSION/deno-src/deno-script.ts", + "env": [ + "npm_package_config_libvips", + "CWD" + ], + "ffi": "*", + "read": [ + "$DESKTOP" + ] + }, + { + "path": "$EXTENSION/deno-src/rpc.ts", + "ffi": "*" + } + ] + }, + { + "permission": "open:file", + "allow": [ + { + "path": "$EXTENSION/src/deno-script.ts" + } + ] + }, + "shell:stdin-write", + { + "permission": "shell:execute", + "allow": [ + { + "cmd": { + "program": "ls", + "args": [ + "-l" + ] + } + }, + { + "cmd": { + "program": "bash", + "args": [ + "-c", + ".+" + ] + } + }, + { + "cmd": { + "program": "deno", + "args": [ + "-A", + ".+", + ".+" + ] + } + } + ] + } + ], + "demoImages": [], + "icon": { + "type": "iconify", + "value": "carbon:demo" + }, + "customUiCmds": [], + "templateUiCmds": [ + { + "name": "Demo Worker Template", + "main": "dist/index.js", + "cmds": [] + } + ] + }, + "scripts": { + "dev": "bun build.ts dev", + "build": "bun build.ts" + }, + "dependencies": { + "@hk/comlink-stdio": "npm:@jsr/hk__comlink-stdio@^0.1.6", + "@kksh/api": "workspace:*", + "@kunkun/api": "npm:@jsr/kunkun__api@^0.0.13" + }, + "devDependencies": { + "@rollup/plugin-commonjs": "^26.0.1", + "@rollup/plugin-node-resolve": "^15.2.3", + "@rollup/plugin-typescript": "^11.1.6", + "@types/bun": "latest", + "rollup-plugin-visualizer": "^5.12.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "files": [ + "./dist", + ".gitignore" + ] +} diff --git a/packages/extensions/demo-worker-template-ext/rollup.config.js b/packages/extensions/demo-worker-template-ext/rollup.config.js new file mode 100644 index 0000000..640b20b --- /dev/null +++ b/packages/extensions/demo-worker-template-ext/rollup.config.js @@ -0,0 +1,20 @@ +import { visualizer } from "rollup-plugin-visualizer"; +import resolve from '@rollup/plugin-node-resolve'; +import typescript from "@rollup/plugin-typescript"; +import commonjs from '@rollup/plugin-commonjs'; + + +export default { + input: "src/index.ts", + output: { + dir: "dist", + format: "esm", + }, + plugins: [ + typescript(), + resolve(), + commonjs(), + // put it the last one + visualizer(), + ], +}; diff --git a/packages/extensions/demo-worker-template-ext/src/index.ts b/packages/extensions/demo-worker-template-ext/src/index.ts new file mode 100644 index 0000000..60105e1 --- /dev/null +++ b/packages/extensions/demo-worker-template-ext/src/index.ts @@ -0,0 +1,181 @@ +import type { RPCChannel } from "@hk/comlink-stdio/browser" +import { + Action, + app, + Child, + expose, + Form, + fs, + Icon, + IconEnum, + List, + Markdown, + open, + path, + security, + shell, + toast, + ui, + WorkerExtension +} from "@kksh/api/ui/worker" +import { IconType } from "@kunkun/api/models" + +const nums = Array.from({ length: 20 }, (_, i) => i + 1) +const categories = ["Suggestion", "Advice", "Idea"] +const itemsTitle = nums.map((n) => categories.map((c) => `${c} ${n}`)).flat() +const allItems: List.Item[] = itemsTitle.map( + (title) => + new List.Item({ + title, + value: title, + defaultAction: "Item Default Action" + }) +) + +class ExtensionTemplate extends WorkerExtension { + async onBeforeGoBack() { + console.log("onBeforeGoBack") + // console.log(`Try killing pid: ${this.apiProcess?.pid}`) + // await this.apiProcess?.kill() + // console.log("apiProcess killed") + } + async onFormSubmit(value: Record): Promise { + console.log("Form submitted", value) + } + + async onEnterPressedOnSearchBar(): Promise { + console.log("Enter pressed on search bar") + } + + async load() { + // console.log("Check screen capture permission:", await security.mac.checkScreenCapturePermission()) + // await security.mac.revealSecurityPane("AllFiles") + // console.log(await security.mac.verifyFingerprint()) + ui.setSearchBarPlaceholder("Search for items") + ui.showLoadingBar(true) + setTimeout(() => { + ui.showLoadingBar(false) + }, 2000) + const { rpcChannel, process } = await shell.createDenoRpcChannel< + {}, + { + add(a: number, b: number): Promise + subtract(a: number, b: number): Promise + } + >("$EXTENSION/deno-src/rpc.ts", [], {}, {}) + const api = rpcChannel.getApi() + await api.add(1, 2).then(console.log) + await api.subtract(1, 2).then(console.log) + await process.kill() + const extPath = await path.extensionDir() + // console.log("Extension path:", extPath) + const tagList = new List.ItemDetailMetadataTagList({ + title: "Tag List Title", + tags: [ + new List.ItemDetailMetadataTagListItem({ + text: "red", + color: "#ff0000" + }), + new List.ItemDetailMetadataTagListItem({ + text: "yellow", + color: "#ffff00" + }) + ] + }) + const list = new List.List({ + items: allItems, + defaultAction: "Top Default Action", + detail: new List.ItemDetail({ + children: [ + new List.ItemDetailMetadata([ + new List.ItemDetailMetadataLabel({ + title: "Label Title", + text: "Label Text" + }), + new List.ItemDetailMetadataLabel({ + title: "Label Title", + text: "Label Text", + icon: new Icon({ + type: IconType.enum.Iconify, + value: "mingcute:appstore-fill" + }) + }), + new List.ItemDetailMetadataSeparator(), + new List.ItemDetailMetadataLabel({ + title: "Label Title", + text: "Label Text" + }), + new List.ItemDetailMetadataLink({ + title: "Link Title", + text: "Link Text", + url: "https://github.com/huakunshen" + }), + new List.ItemDetailMetadataLabel({ + title: "Label Title", + text: "Label Text" + }), + tagList + ]), + new Markdown(` +# Hello World + + + + `) + ], + width: 50 + }), + actions: new Action.ActionPanel({ + items: [ + new Action.Action({ + title: "Action 1", + value: "action 1", + icon: new Icon({ type: IconType.enum.Iconify, value: "material-symbols:add-reaction" }) + }), + new Action.Action({ title: "Action 2", value: "action 2" }), + new Action.Action({ title: "Action 3", value: "action 3" }), + new Action.Action({ title: "Action 4", value: "action 4" }) + ] + }) + }) + + return ui.render(list) + } + + async onSearchTermChange(term: string): Promise { + return ui.render( + new List.List({ + // items: allItems.filter((item) => item.title.toLowerCase().includes(term.toLowerCase())), + inherits: ["items", "sections"], + defaultAction: "Top Default Action", + detail: new List.ItemDetail({ + children: [ + new List.ItemDetailMetadata([ + new List.ItemDetailMetadataLabel({ + title: "Label Title", + text: "Label Text" + }) + ]) + // new Markdown(` + // ## Search results for "${term}" + // + // + // + // `) + ], + width: term.length > 3 ? 70 : 30 + }) + }) + ) + } + + async onListItemSelected(value: string): Promise { + console.log("Item selected:", value) + } + + async onActionSelected(value: string): Promise { + console.log("Action selected:", value) + } +} + +expose(new ExtensionTemplate()) diff --git a/packages/extensions/demo-worker-template-ext/tsconfig.json b/packages/extensions/demo-worker-template-ext/tsconfig.json new file mode 100644 index 0000000..49ca356 --- /dev/null +++ b/packages/extensions/demo-worker-template-ext/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": [ + "ESNext", + "DOM" + ], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": false, + "verbatimModuleSyntax": true, + "noEmit": true, + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} \ No newline at end of file diff --git a/packages/extensions/form-view/.gitignore b/packages/extensions/form-view/.gitignore new file mode 100644 index 0000000..8c67a06 --- /dev/null +++ b/packages/extensions/form-view/.gitignore @@ -0,0 +1,177 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Caches + +.cache + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store +extensions_support/ + diff --git a/packages/extensions/form-view/README.md b/packages/extensions/form-view/README.md new file mode 100644 index 0000000..e1c7cc7 --- /dev/null +++ b/packages/extensions/form-view/README.md @@ -0,0 +1,125 @@ +# Kunkun Template UI Extension + +This is a template for a template UI extension. (UI follows pre-defined template) + +[./src/index.ts](./src/index.ts) is the default entrypoint for the extension. You can import any other files in this file, but the build process will bundle them into a single file. + +## Pros and Cons + +This type of extension is suitable for simple use cases, such as a list or form. All components are pre-defined, so there is not much room for customization. If you want more flexibility on the UI, consider using [Custom UI Extension](https://docs.kunkun.sh/extensions/custom-ui-ext/), which requires some frontend knowledge but gives you full control over the UI. + +Read documentation at https://docs.kunkun.sh/extensions/worker-template/ + +Make sure you understand what this type of extension is capable of. + +### Pros + +- Simple to develop, no need for any frontend knowledge. +- Small bundle size (~40KB) + - [Custom UI Extension](https://docs.kunkun.sh/extensions/custom-ui-ext/) are usually larger than 300KB. + +### Cons + +- Limited UI customization. Not suitable for complex use cases. + +Consider [Custom UI Extension](https://docs.kunkun.sh/extensions/custom-ui-ext/) if you need more complex UI. + +## Development + +```bash +pnpm install +``` + +Start extension in development mode. Every save will trigger a hot reload in Kunkun. + +```bash +pnpm dev +``` + +- During development, right click in Kunkun to open the developer tools. + - Error messages will be shown in the console. + - If you got any permission error while calling Kunknu's APIs, make sure you've declared the permission in `package.json`. Then go back to home page and enter the extension again to re-apply the permission. +- To develop and preview the extension in Kunkun, you need to run the `Add Dev Extension` command in Kunkun, and register this extension's path. + +Build the extension. Your extension source code can contain many files, but the build process will bundle them into a single file. + +```bash +pnpm build +# Due to Bun's bug, if you are on windows, and install dependencies with pnpm, you may get error during build. +# Try install dependencies with bun or npm instead. +``` + +## i18n + +[./src/i18n](./src/i18n/) contains optional internationalization support starter code. + +If you want to support i18n, you can use the `t` function to translate the strings in the extension. + +User's language setting is available via `app.language()`. + +```ts +import { app } from "@kksh/api/ui/worker" +import { setupI18n, t } from "./src/i18n" + +setupI18n("zh") +console.log(t("welcome")) + +setupI18n(await app.language()) +console.log(t("welcome")) +``` + +## Add More Commands + +If you want to add more template worker extension commands, simply modify the `entrypoints` array in [./build.ts](./build.ts). + +Then in `package.json`, register the new command. + +## Verify Build and Publish + +```bash +pnpm build # make sure the build npm script works +npx kksh@latest verify # Verify some basic settings +npx kksh@latest verify --publish # Verify some basic settings before publishing +``` + +It is recommended to build the extension with the same environment our CI uses. + +The docker image used by our CI is `huakunshen/kunkun-ext-builder:latest`. + +You can use the following command to build the extension with the same environment our CI uses. +This requires you to have docker installed, and the shell you are using has access to it via `docker` command. + +```bash +npx kksh@latest build # Build the extension with +``` + +`pnpm` is used to install dependencies and build the extension. + +The docker image environment also has `node`, `pnpm`, `npm`, `bun`, `deno` installed. +If your build failed, try debug with `huakunshen/kunkun-ext-builder:latest` image in interative mode and bind your extension volume to `/workspace`. + +After build successfully, you should find a tarball file ends with `.tgz` in the root of your extension. +The tarball is packaged with `npm pack` command. You can uncompress it to see if it contains all the necessary files. + +This tarball is the final product that will be published and installed in Kunkun. You can further verify your extension by installing this tarball directly in Kunkun. + +After verifying the tarball, it's ready to be published. + +Fork [KunkunExtensions](https://github.com/kunkunsh/KunkunExtensions) repo, add your extension to the `extensions` directory, and create a PR. + +Once CI passed and PR merged, you can use your extension in Kunkun. + +## Potential Error + +Our CI uses `pnpm` to install dependencies. If you are on Windows, you may get error during build. + +See issue https://github.com/kunkunsh/kunkun/issues/78 + +`bun` had problem building the extension when `pnpm` is used to install dependencies. + +### Options + +1. Install an older version of `bun` (1.1.27 should work) +2. Install dependencies with `bun` or `npm` instead of `pnpm` + +Our CI always builds the extension with on Linux and shouldn't have this problem. diff --git a/packages/extensions/form-view/build.ts b/packages/extensions/form-view/build.ts new file mode 100644 index 0000000..a99deb5 --- /dev/null +++ b/packages/extensions/form-view/build.ts @@ -0,0 +1,30 @@ +import { watch } from "fs" +import { join } from "path" +import { refreshTemplateWorkerExtension } from "@kksh/api/dev" +import { $ } from "bun" + +const entrypoints = ["./src/index.ts"] + +async function build() { + try { + for (const entrypoint of entrypoints) { + await $`bun build --minify --target=browser --outdir=./dist ${entrypoint}` + } + if (Bun.argv.includes("dev")) { + await refreshTemplateWorkerExtension() + } + } catch (error) { + console.error(error) + } +} + +const srcDir = join(import.meta.dir, "src") + +await build() + +if (Bun.argv.includes("dev")) { + console.log(`Watching ${srcDir} for changes...`) + watch(srcDir, { recursive: true }, async (event, filename) => { + await build() + }) +} diff --git a/packages/extensions/form-view/package.json b/packages/extensions/form-view/package.json new file mode 100644 index 0000000..d31bc05 --- /dev/null +++ b/packages/extensions/form-view/package.json @@ -0,0 +1,47 @@ +{ + "$schema": "https://schema.kunkun.sh", + "name": "form-view", + "version": "0.0.2", + "type": "module", + "kunkun": { + "name": "Form View", + "shortDescription": "A Worker Extension Template", + "longDescription": "A Worker Extension Template", + "identifier": "form-view", + "permissions": [ + "fetch:all", + "clipboard:read-all" + ], + "demoImages": [], + "icon": { + "type": "iconify", + "value": "fluent:form-multiple-28-filled" + }, + "customUiCmds": [], + "templateUiCmds": [ + { + "name": "Dev Form View", + "main": "dist/index.js", + "cmds": [] + } + ] + }, + "scripts": { + "dev": "bun build.ts dev", + "build": "bun build.ts" + }, + "dependencies": { + "@kksh/api": "workspace:*", + "i18next": "^23.15.1" + }, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "files": [ + "./dist", + ".gitignore" + ] +} diff --git a/packages/extensions/form-view/src/i18n/en.ts b/packages/extensions/form-view/src/i18n/en.ts new file mode 100644 index 0000000..331e9f1 --- /dev/null +++ b/packages/extensions/form-view/src/i18n/en.ts @@ -0,0 +1,5 @@ +const en = { + welcome: "Welcome to Kunkun" +} +export default en +export type Translation = typeof en diff --git a/packages/extensions/form-view/src/i18n/index.ts b/packages/extensions/form-view/src/i18n/index.ts new file mode 100644 index 0000000..2f0e1c8 --- /dev/null +++ b/packages/extensions/form-view/src/i18n/index.ts @@ -0,0 +1,20 @@ +import i18next from "i18next" +import en, { type Translation } from "./en" +import zh from "./zh" + +export function setupI18n(language: "en" | "zh" = "en") { + i18next.init({ + resources: { + en: { + translation: en + }, + zh: { + translation: zh + } + }, + lng: language, // default language + fallbackLng: "en" + }) +} + +export const t = (key: keyof Translation, options?: any) => i18next.t(key, options) diff --git a/packages/extensions/form-view/src/i18n/zh.ts b/packages/extensions/form-view/src/i18n/zh.ts new file mode 100644 index 0000000..dc3ab95 --- /dev/null +++ b/packages/extensions/form-view/src/i18n/zh.ts @@ -0,0 +1,5 @@ +import type { Translation } from "./en" + +export default { + welcome: "欢迎来到Kunkun" +} satisfies Translation diff --git a/packages/extensions/form-view/src/index.ts b/packages/extensions/form-view/src/index.ts new file mode 100644 index 0000000..f6b4d2c --- /dev/null +++ b/packages/extensions/form-view/src/index.ts @@ -0,0 +1,95 @@ +import { + Action, + app, + expose, + Form, + fs, + Icon, + IconEnum, + List, + Markdown, + path, + shell, + toast, + ui, + WorkerExtension +} from "@kksh/api/ui/worker" + +class ExtensionTemplate extends WorkerExtension { + async onFormSubmit(value: Record): Promise { + console.log("Form submitted", value) + toast.success(`Form submitted: ${JSON.stringify(value)}`) + } + async load() { + const markdown = new Markdown(`# Hello World +`) + // markdown.toModel + return ui.render(markdown) + const form = new Form.Form({ + title: "Form 1", + key: "form1", + submitBtnText: "Download", + fields: [ + new Form.DateField({ + key: "birthday", + label: "Date of Birth", + hideLabel: false, + description: "Enter your date of birth" + }), + new Form.NumberField({ + key: "age", + label: "Age", + default: 18, + placeholder: "Enter your age", + optional: true, + description: "Enter your age" + }), + new Form.InputField({ + key: "name", + label: "Name", + default: "Huakun" + }), + new Form.InputField({ + key: "name2", + label: "Name 2" + }), + new Form.BooleanField({ + key: "isActive", + label: "Is Active", + description: "Is the user active?" + }), + new Form.SelectField({ + key: "gender", + label: "Gender", + options: ["Male", "Female", "Other"], + description: "Select your gender" + }) + ] + }) + console.log(form) + console.log(form.toModel()) + return ui.render(form) + } + + async onActionSelected(actionValue: string): Promise { + switch (actionValue) { + case "open": + break + + default: + break + } + } + + onSearchTermChange(term: string): Promise { + console.log("Search term changed to:", term) + return Promise.resolve() + } + + onListItemSelected(value: string): Promise { + console.log("Item selected:", value) + return Promise.resolve() + } +} + +expose(new ExtensionTemplate()) diff --git a/packages/extensions/form-view/tsconfig.json b/packages/extensions/form-view/tsconfig.json new file mode 100644 index 0000000..49ca356 --- /dev/null +++ b/packages/extensions/form-view/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": [ + "ESNext", + "DOM" + ], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": false, + "verbatimModuleSyntax": true, + "noEmit": true, + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} \ No newline at end of file diff --git a/packages/schema/package.json b/packages/schema/package.json index e3070c8..0b33136 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -15,8 +15,7 @@ }, "devDependencies": { "@gcornut/valibot-json-schema": "^0.42.0", - "@types/bun": "latest", - "supabase": ">=1.8.1" + "@types/bun": "latest" }, "peerDependencies": { "@kksh/supabase": "workspace:*", @@ -25,7 +24,6 @@ "dependencies": { "@aws-sdk/client-s3": "^3.583.0", "@kksh/api": "workspace:*", - "@supabase/supabase-js": "^2.43.4", "valibot": "^0.40.0" } } diff --git a/packages/schema/scripts/upload-schema-to-supabase.ts b/packages/schema/scripts/upload-schema-to-supabase.ts index f4bf48a..eddd837 100644 --- a/packages/schema/scripts/upload-schema-to-supabase.ts +++ b/packages/schema/scripts/upload-schema-to-supabase.ts @@ -1,10 +1,9 @@ import { ExtPackageJson } from "@kksh/api/models" -import { type Database } from "@kksh/supabase" -import { createClient } from "@supabase/supabase-js" +import { createSB } from "@kksh/supabase" import { parse, string } from "valibot" import { getJsonSchema } from "../src" -const supabase = createClient( +const supabase = createSB( parse(string(), process.env.SUPABASE_URL), parse(string(), process.env.SUPABASE_SERVICE_ROLE_KEY) ) diff --git a/packages/supabase/package.json b/packages/supabase/package.json index 4248fdd..d4cf8bf 100644 --- a/packages/supabase/package.json +++ b/packages/supabase/package.json @@ -8,7 +8,8 @@ ".": "./src/index.ts" }, "dependencies": { - "@kksh/api": "workspace:*" + "@kksh/api": "workspace:*", + "@supabase/supabase-js": "^2.46.1" }, "devDependencies": { "@types/bun": "latest" diff --git a/packages/supabase/src/index.ts b/packages/supabase/src/index.ts index 9285d65..09aae5e 100644 --- a/packages/supabase/src/index.ts +++ b/packages/supabase/src/index.ts @@ -2,7 +2,11 @@ import type { Database } from "@kksh/api/supabase/types" import { createClient } from "@supabase/supabase-js" export function createSB(supabaseUrl: string, supabaseAnonKey: string) { - return createClient(supabaseUrl, supabaseAnonKey) + return createClient(supabaseUrl, supabaseAnonKey, { + auth: { + flowType: "pkce" + } + }) } export { SupabaseAPI } from "./api" diff --git a/packages/types/src/appConfig.ts b/packages/types/src/appConfig.ts index 31a2dec..289adc0 100644 --- a/packages/types/src/appConfig.ts +++ b/packages/types/src/appConfig.ts @@ -23,6 +23,6 @@ export type PersistedAppConfig = v.InferOutput export type AppConfig = PersistedAppConfig & { isInitialized: boolean - extensionPath?: string + extensionsInstallDir?: string platform: Platform } diff --git a/packages/types/src/appState.ts b/packages/types/src/appState.ts index dd9c9f4..4a0539c 100644 --- a/packages/types/src/appState.ts +++ b/packages/types/src/appState.ts @@ -1,4 +1,9 @@ +import { Action as ActionSchema } from "@kksh/api/models" + export interface AppState { searchTerm: string highlightedCmd: string + loadingBar: boolean + defaultAction: string + actionPanel?: ActionSchema.ActionPanel } diff --git a/packages/ui/components.json b/packages/ui/components.json index f10b563..33c48e0 100644 --- a/packages/ui/components.json +++ b/packages/ui/components.json @@ -8,10 +8,10 @@ }, "aliases": { "components": "@kksh/ui/src/components", - "utils": "@kksh/ui/src/utils", + "utils": "@kksh/ui/utils", "ui": "@kksh/ui/src/components/ui", "hooks": "@kksh/ui/src/hooks" }, "typescript": true, "registry": "https://next.shadcn-svelte.com/registry" -} +} \ No newline at end of file diff --git a/packages/ui/package.json b/packages/ui/package.json index 55cca6d..2a6149b 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -34,24 +34,31 @@ "lint": "eslint ." }, "devDependencies": { + "@iconify/svelte": "^4.0.2", "@kksh/api": "workspace:*", + "@kksh/svelte5": "^0.1.2-beta.8", "@types/bun": "latest", - "bits-ui": "1.0.0-next.36", + "bits-ui": "1.0.0-next.45", "clsx": "^2.1.1", + "formsnap": "2.0.0-next.1", "lucide-svelte": "^0.454.0", "mode-watcher": "^0.4.1", "paneforge": "1.0.0-next.1", "shiki": "^1.22.2", "svelte-radix": "^2.0.1", "svelte-sonner": "^0.3.28", + "sveltekit-superforms": "^2.20.0", "tailwind-merge": "^2.5.4", "tailwind-variants": "^0.2.1", "tailwindcss": "^3.4.14", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "zod": "^3.23.8" }, "dependencies": { "@formkit/auto-animate": "^0.8.2", + "@internationalized/date": "^3.5.6", "@std/semver": "npm:@jsr/std__semver@^1.0.3", - "gsap": "^3.12.5" + "gsap": "^3.12.5", + "svelte-markdown": "^0.4.1" } } diff --git a/packages/ui/src/components/common/IconMultiplexer.svelte b/packages/ui/src/components/common/IconMultiplexer.svelte index e76760e..74996ee 100644 --- a/packages/ui/src/components/common/IconMultiplexer.svelte +++ b/packages/ui/src/components/common/IconMultiplexer.svelte @@ -12,17 +12,37 @@ {#if icon.type === IconEnum.RemoteUrl} - + {:else if icon.type === IconEnum.Iconify} - + {:else if icon.type === IconEnum.Base64PNG} - + {:else if icon.type === IconEnum.Text} - {:else if icon.type === IconEnum.Svg} - {@html icon.value} + {@html icon.value} {:else} - + {/if} diff --git a/packages/ui/src/components/common/IconSelector.svelte b/packages/ui/src/components/common/IconSelector.svelte new file mode 100644 index 0000000..70576ab --- /dev/null +++ b/packages/ui/src/components/common/IconSelector.svelte @@ -0,0 +1,56 @@ + + +
    + + + {triggerContent} + + + + Icon Type + {#each iconOptionsArray as [label, value]} + {label} + {/each} + + + +