diff --git a/package.json b/package.json new file mode 100644 index 0000000..9194aec --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "codenomad-workspace", + "version": "0.4.0", + "private": true, + "description": "CodeNomad monorepo workspace", + "workspaces": { + "packages": [ + "packages/*" + ] + }, + "scripts": { + "dev": "npm run dev --workspace @neuralnomads/codenomad-electron-app", + "dev:electron": "npm run dev --workspace @neuralnomads/codenomad-electron-app", + "dev:tauri": "npm run dev --workspace @codenomad/tauri-app", + "build": "npm run build --workspace @neuralnomads/codenomad-electron-app", + "build:tauri": "npm run build --workspace @codenomad/tauri-app", + "build:ui": "npm run build --workspace @codenomad/ui", + "build:mac-x64": "npm run build:mac-x64 --workspace @neuralnomads/codenomad-electron-app", + "build:binaries": "npm run build:binaries --workspace @neuralnomads/codenomad-electron-app", + "typecheck": "npm run typecheck --workspace @codenomad/ui && npm run typecheck --workspace @neuralnomads/codenomad-electron-app", + "bumpVersion": "npm version --workspaces --include-workspace-root --no-git-tag-version" + }, + "dependencies": { + "7zip-bin": "^5.2.0", + "google-auth-library": "^10.5.0" + } +} diff --git a/packages/electron-app/.gitignore b/packages/electron-app/.gitignore new file mode 100644 index 0000000..2dfa475 --- /dev/null +++ b/packages/electron-app/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +release/ +.vite/ diff --git a/packages/electron-app/README.md b/packages/electron-app/README.md new file mode 100644 index 0000000..6e9a680 --- /dev/null +++ b/packages/electron-app/README.md @@ -0,0 +1,40 @@ +# CodeNomad App + +This package contains the native desktop application shell for CodeNomad, built with [Electron](https://www.electronjs.org/). + +## Overview + +The Electron app wraps the CodeNomad UI and Server into a standalone executable. It provides deeper system integration, such as: +- Native window management +- Global keyboard shortcuts +- Application menu integration + +## Development + +To run the Electron app in development mode: + +```bash +npm run dev +``` + +This will start the renderer (UI) and the main process with hot reloading. + +## Building + +To build the application for your current platform: + +```bash +npm run build +``` + +To build for specific platforms (requires appropriate build tools): + +- **macOS**: `npm run build:mac` +- **Windows**: `npm run build:win` +- **Linux**: `npm run build:linux` + +## Structure + +- `electron/main`: Main process code (window creation, IPC). +- `electron/preload`: Preload scripts for secure bridge between main and renderer. +- `electron/resources`: Static assets like icons. diff --git a/packages/electron-app/electron.vite.config.ts b/packages/electron-app/electron.vite.config.ts new file mode 100644 index 0000000..1161176 --- /dev/null +++ b/packages/electron-app/electron.vite.config.ts @@ -0,0 +1,72 @@ +import { defineConfig, externalizeDepsPlugin } from "electron-vite" +import solid from "vite-plugin-solid" +import { resolve } from "path" + +const uiRoot = resolve(__dirname, "../ui") +const uiSrc = resolve(uiRoot, "src") +const uiRendererRoot = resolve(uiRoot, "src/renderer") +const uiRendererEntry = resolve(uiRendererRoot, "index.html") +const uiRendererLoadingEntry = resolve(uiRendererRoot, "loading.html") + +export default defineConfig({ + main: { + plugins: [externalizeDepsPlugin()], + build: { + outDir: "dist/main", + lib: { + entry: resolve(__dirname, "electron/main/main.ts"), + }, + rollupOptions: { + external: ["electron"], + }, + }, + }, + preload: { + plugins: [externalizeDepsPlugin()], + build: { + outDir: "dist/preload", + lib: { + entry: resolve(__dirname, "electron/preload/index.cjs"), + formats: ["cjs"], + fileName: () => "index.js", + }, + rollupOptions: { + external: ["electron"], + output: { + entryFileNames: "index.js", + }, + }, + }, + }, + renderer: { + root: uiRendererRoot, + plugins: [solid()], + css: { + postcss: resolve(uiRoot, "postcss.config.js"), + }, + resolve: { + alias: { + "@": uiSrc, + }, + }, + server: { + port: 3000, + }, + build: { + minify: false, + cssMinify: false, + sourcemap: true, + outDir: resolve(__dirname, "dist/renderer"), + rollupOptions: { + input: { + main: uiRendererEntry, + loading: uiRendererLoadingEntry, + }, + output: { + compact: false, + minifyInternalExports: false, + }, + }, + }, + }, +}) diff --git a/packages/electron-app/electron/main/ipc.ts b/packages/electron-app/electron/main/ipc.ts new file mode 100644 index 0000000..26f6499 --- /dev/null +++ b/packages/electron-app/electron/main/ipc.ts @@ -0,0 +1,65 @@ +import { BrowserWindow, dialog, ipcMain, type OpenDialogOptions } from "electron" +import type { CliProcessManager, CliStatus } from "./process-manager" + +interface DialogOpenRequest { + mode: "directory" | "file" + title?: string + defaultPath?: string + filters?: Array<{ name?: string; extensions: string[] }> +} + +interface DialogOpenResult { + canceled: boolean + paths: string[] +} + +export function setupCliIPC(mainWindow: BrowserWindow, cliManager: CliProcessManager) { + cliManager.on("status", (status: CliStatus) => { + if (!mainWindow.isDestroyed()) { + mainWindow.webContents.send("cli:status", status) + } + }) + + cliManager.on("ready", (status: CliStatus) => { + if (!mainWindow.isDestroyed()) { + mainWindow.webContents.send("cli:ready", status) + } + }) + + cliManager.on("error", (error: Error) => { + if (!mainWindow.isDestroyed()) { + mainWindow.webContents.send("cli:error", { message: error.message }) + } + }) + + ipcMain.handle("cli:getStatus", async () => cliManager.getStatus()) + + ipcMain.handle("cli:restart", async () => { + const devMode = process.env.NODE_ENV === "development" + await cliManager.stop() + return cliManager.start({ dev: devMode }) + }) + + ipcMain.handle("dialog:open", async (_, request: DialogOpenRequest): Promise => { + const properties: OpenDialogOptions["properties"] = + request.mode === "directory" ? ["openDirectory", "createDirectory"] : ["openFile"] + + const filters = request.filters?.map((filter) => ({ + name: filter.name ?? "Files", + extensions: filter.extensions, + })) + + const windowTarget = mainWindow.isDestroyed() ? undefined : mainWindow + const dialogOptions: OpenDialogOptions = { + title: request.title, + defaultPath: request.defaultPath, + properties, + filters, + } + const result = windowTarget + ? await dialog.showOpenDialog(windowTarget, dialogOptions) + : await dialog.showOpenDialog(dialogOptions) + + return { canceled: result.canceled, paths: result.filePaths } + }) +} diff --git a/packages/electron-app/electron/main/main.ts b/packages/electron-app/electron/main/main.ts new file mode 100644 index 0000000..f4e8be1 --- /dev/null +++ b/packages/electron-app/electron/main/main.ts @@ -0,0 +1,381 @@ +import { app, BrowserView, BrowserWindow, nativeImage, session, shell } from "electron" +import { existsSync } from "fs" +import { dirname, join } from "path" +import { fileURLToPath } from "url" +import { createApplicationMenu } from "./menu" +import { setupCliIPC } from "./ipc" +import { CliProcessManager } from "./process-manager" + +const mainFilename = fileURLToPath(import.meta.url) +const mainDirname = dirname(mainFilename) + +const isMac = process.platform === "darwin" + +const cliManager = new CliProcessManager() +let mainWindow: BrowserWindow | null = null +let currentCliUrl: string | null = null +let pendingCliUrl: string | null = null +let showingLoadingScreen = false +let preloadingView: BrowserView | null = null + +if (isMac) { + app.commandLine.appendSwitch("disable-spell-checking") +} + +function getIconPath() { + if (app.isPackaged) { + return join(process.resourcesPath, "icon.png") + } + + return join(mainDirname, "../resources/icon.png") +} + +type LoadingTarget = + | { type: "url"; source: string } + | { type: "file"; source: string } + +function resolveDevLoadingUrl(): string | null { + if (app.isPackaged) { + return null + } + const devBase = process.env.VITE_DEV_SERVER_URL || process.env.ELECTRON_RENDERER_URL + if (!devBase) { + return null + } + + try { + const normalized = devBase.endsWith("/") ? devBase : `${devBase}/` + return new URL("loading.html", normalized).toString() + } catch (error) { + console.warn("[cli] failed to construct dev loading URL", devBase, error) + return null + } +} + +function resolveLoadingTarget(): LoadingTarget { + const devUrl = resolveDevLoadingUrl() + if (devUrl) { + return { type: "url", source: devUrl } + } + const filePath = resolveLoadingFilePath() + return { type: "file", source: filePath } +} + +function resolveLoadingFilePath() { + const candidates = [ + join(app.getAppPath(), "dist/renderer/loading.html"), + join(process.resourcesPath, "dist/renderer/loading.html"), + join(mainDirname, "../dist/renderer/loading.html"), + ] + + for (const candidate of candidates) { + if (existsSync(candidate)) { + return candidate + } + } + + return join(app.getAppPath(), "dist/renderer/loading.html") +} + +function loadLoadingScreen(window: BrowserWindow) { + const target = resolveLoadingTarget() + const loader = + target.type === "url" + ? window.loadURL(target.source) + : window.loadFile(target.source) + + loader.catch((error) => { + console.error("[cli] failed to load loading screen:", error) + }) +} + +function getAllowedRendererOrigins(): string[] { + const origins = new Set() + const rendererCandidates = [currentCliUrl, process.env.VITE_DEV_SERVER_URL, process.env.ELECTRON_RENDERER_URL] + for (const candidate of rendererCandidates) { + if (!candidate) { + continue + } + try { + origins.add(new URL(candidate).origin) + } catch (error) { + console.warn("[cli] failed to parse origin for", candidate, error) + } + } + return Array.from(origins) +} + +function shouldOpenExternally(url: string): boolean { + try { + const parsed = new URL(url) + if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { + return true + } + const allowedOrigins = getAllowedRendererOrigins() + return !allowedOrigins.includes(parsed.origin) + } catch { + return false + } +} + +function setupNavigationGuards(window: BrowserWindow) { + const handleExternal = (url: string) => { + shell.openExternal(url).catch((error) => console.error("[cli] failed to open external URL", url, error)) + } + + window.webContents.setWindowOpenHandler(({ url }) => { + if (shouldOpenExternally(url)) { + handleExternal(url) + return { action: "deny" } + } + return { action: "allow" } + }) + + window.webContents.on("will-navigate", (event, url) => { + if (shouldOpenExternally(url)) { + event.preventDefault() + handleExternal(url) + } + }) +} + +let cachedPreloadPath: string | null = null +function getPreloadPath() { + if (cachedPreloadPath && existsSync(cachedPreloadPath)) { + return cachedPreloadPath + } + + const candidates = [ + join(process.resourcesPath, "preload/index.js"), + join(mainDirname, "../preload/index.js"), + join(mainDirname, "../preload/index.cjs"), + join(mainDirname, "../../preload/index.cjs"), + join(mainDirname, "../../electron/preload/index.cjs"), + join(app.getAppPath(), "preload/index.cjs"), + join(app.getAppPath(), "electron/preload/index.cjs"), + ] + + for (const candidate of candidates) { + if (existsSync(candidate)) { + cachedPreloadPath = candidate + return candidate + } + } + + return join(mainDirname, "../preload/index.js") +} + +function destroyPreloadingView(target?: BrowserView | null) { + const view = target ?? preloadingView + if (!view) { + return + } + + try { + const contents = view.webContents as any + contents?.destroy?.() + } catch (error) { + console.warn("[cli] failed to destroy preloading view", error) + } + + if (!target || view === preloadingView) { + preloadingView = null + } +} + +function createWindow() { + const prefersDark = true + const backgroundColor = prefersDark ? "#1a1a1a" : "#ffffff" + const iconPath = getIconPath() + + mainWindow = new BrowserWindow({ + width: 1400, + height: 900, + minWidth: 800, + minHeight: 600, + backgroundColor, + icon: iconPath, + webPreferences: { + preload: getPreloadPath(), + contextIsolation: true, + nodeIntegration: false, + spellcheck: !isMac, + }, + }) + + setupNavigationGuards(mainWindow) + + if (isMac) { + mainWindow.webContents.session.setSpellCheckerEnabled(false) + } + + showingLoadingScreen = true + currentCliUrl = null + loadLoadingScreen(mainWindow) + + if (process.env.NODE_ENV === "development") { + mainWindow.webContents.openDevTools({ mode: "detach" }) + } + + createApplicationMenu(mainWindow) + setupCliIPC(mainWindow, cliManager) + + mainWindow.on("closed", () => { + destroyPreloadingView() + mainWindow = null + currentCliUrl = null + pendingCliUrl = null + showingLoadingScreen = false + }) + + if (pendingCliUrl) { + const url = pendingCliUrl + pendingCliUrl = null + startCliPreload(url) + } +} + +function showLoadingScreen(force = false) { + if (!mainWindow || mainWindow.isDestroyed()) { + return + } + + if (showingLoadingScreen && !force) { + return + } + + destroyPreloadingView() + showingLoadingScreen = true + currentCliUrl = null + pendingCliUrl = null + loadLoadingScreen(mainWindow) +} + +function startCliPreload(url: string) { + if (!mainWindow || mainWindow.isDestroyed()) { + pendingCliUrl = url + return + } + + if (currentCliUrl === url && !showingLoadingScreen) { + return + } + + pendingCliUrl = url + destroyPreloadingView() + + if (!showingLoadingScreen) { + showLoadingScreen(true) + } + + const view = new BrowserView({ + webPreferences: { + contextIsolation: true, + nodeIntegration: false, + spellcheck: !isMac, + }, + }) + + preloadingView = view + + view.webContents.once("did-finish-load", () => { + if (preloadingView !== view) { + destroyPreloadingView(view) + return + } + finalizeCliSwap(url) + }) + + view.webContents.loadURL(url).catch((error) => { + console.error("[cli] failed to preload CLI view:", error) + if (preloadingView === view) { + destroyPreloadingView(view) + } + }) +} + +function finalizeCliSwap(url: string) { + destroyPreloadingView() + + if (!mainWindow || mainWindow.isDestroyed()) { + pendingCliUrl = url + return + } + + showingLoadingScreen = false + currentCliUrl = url + pendingCliUrl = null + mainWindow.loadURL(url).catch((error) => console.error("[cli] failed to load CLI view:", error)) +} + + +async function startCli() { + try { + const devMode = process.env.NODE_ENV === "development" + console.info("[cli] start requested (dev mode:", devMode, ")") + await cliManager.start({ dev: devMode }) + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + console.error("[cli] start failed:", message) + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send("cli:error", { message }) + } + } +} + +cliManager.on("ready", (status) => { + if (!status.url) { + return + } + startCliPreload(status.url) +}) + +cliManager.on("status", (status) => { + if (status.state !== "ready") { + showLoadingScreen() + } +}) + +if (isMac) { + app.on("web-contents-created", (_, contents) => { + contents.session.setSpellCheckerEnabled(false) + }) +} + +app.whenReady().then(() => { + startCli() + + if (isMac) { + session.defaultSession.setSpellCheckerEnabled(false) + app.on("browser-window-created", (_, window) => { + window.webContents.session.setSpellCheckerEnabled(false) + }) + + if (app.dock) { + const dockIcon = nativeImage.createFromPath(getIconPath()) + if (!dockIcon.isEmpty()) { + app.dock.setIcon(dockIcon) + } + } + } + + createWindow() + + app.on("activate", () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow() + } + }) +}) + +app.on("before-quit", async (event) => { + event.preventDefault() + await cliManager.stop().catch(() => {}) + app.exit(0) +}) + +app.on("window-all-closed", () => { + if (process.platform !== "darwin") { + app.quit() + } +}) diff --git a/packages/electron-app/electron/main/menu.ts b/packages/electron-app/electron/main/menu.ts new file mode 100644 index 0000000..37f9e34 --- /dev/null +++ b/packages/electron-app/electron/main/menu.ts @@ -0,0 +1,84 @@ +import { Menu, BrowserWindow, MenuItemConstructorOptions } from "electron" + +export function createApplicationMenu(mainWindow: BrowserWindow) { + const isMac = process.platform === "darwin" + + const template: MenuItemConstructorOptions[] = [ + ...(isMac + ? [ + { + label: "CodeNomad", + submenu: [ + { role: "about" as const }, + { type: "separator" as const }, + { role: "hide" as const }, + { role: "hideOthers" as const }, + { role: "unhide" as const }, + { type: "separator" as const }, + { role: "quit" as const }, + ], + }, + ] + : []), + { + label: "File", + submenu: [ + { + label: "New Instance", + accelerator: "CmdOrCtrl+N", + click: () => { + mainWindow.webContents.send("menu:newInstance") + }, + }, + { type: "separator" as const }, + isMac ? { role: "close" as const } : { role: "quit" as const }, + ], + }, + { + label: "Edit", + submenu: [ + { role: "undo" as const }, + { role: "redo" as const }, + { type: "separator" as const }, + { role: "cut" as const }, + { role: "copy" as const }, + { role: "paste" as const }, + ...(isMac + ? [{ role: "pasteAndMatchStyle" as const }, { role: "delete" as const }, { role: "selectAll" as const }] + : [{ role: "delete" as const }, { type: "separator" as const }, { role: "selectAll" as const }]), + ], + }, + { + label: "View", + submenu: [ + { role: "reload" as const }, + { role: "forceReload" as const }, + { role: "toggleDevTools" as const }, + { type: "separator" as const }, + { role: "resetZoom" as const }, + { role: "zoomIn" as const }, + { role: "zoomOut" as const }, + { type: "separator" as const }, + { role: "togglefullscreen" as const }, + ], + }, + { + label: "Window", + submenu: [ + { role: "minimize" as const }, + { role: "zoom" as const }, + ...(isMac + ? [ + { type: "separator" as const }, + { role: "front" as const }, + { type: "separator" as const }, + { role: "window" as const }, + ] + : [{ role: "close" as const }]), + ], + }, + ] + + const menu = Menu.buildFromTemplate(template) + Menu.setApplicationMenu(menu) +} diff --git a/packages/electron-app/electron/main/process-manager.ts b/packages/electron-app/electron/main/process-manager.ts new file mode 100644 index 0000000..1630b87 --- /dev/null +++ b/packages/electron-app/electron/main/process-manager.ts @@ -0,0 +1,364 @@ +import { spawn, type ChildProcess } from "child_process" +import { app } from "electron" +import { createRequire } from "module" +import { EventEmitter } from "events" +import { existsSync, readFileSync } from "fs" +import os from "os" +import path from "path" +import { buildUserShellCommand, getUserShellEnv, supportsUserShell } from "./user-shell" + +const nodeRequire = createRequire(import.meta.url) + + +type CliState = "starting" | "ready" | "error" | "stopped" +type ListeningMode = "local" | "all" + +export interface CliStatus { + state: CliState + pid?: number + port?: number + url?: string + error?: string +} + +export interface CliLogEntry { + stream: "stdout" | "stderr" + message: string +} + +interface StartOptions { + dev: boolean +} + +interface CliEntryResolution { + entry: string + runner: "node" | "tsx" + runnerPath?: string +} + +const DEFAULT_CONFIG_PATH = "~/.config/codenomad/config.json" + +function resolveConfigPath(configPath?: string): string { + const target = configPath && configPath.trim().length > 0 ? configPath : DEFAULT_CONFIG_PATH + if (target.startsWith("~/")) { + return path.join(os.homedir(), target.slice(2)) + } + return path.resolve(target) +} + +function resolveHostForMode(mode: ListeningMode): string { + return mode === "local" ? "127.0.0.1" : "0.0.0.0" +} + +function readListeningModeFromConfig(): ListeningMode { + try { + const configPath = resolveConfigPath(process.env.CLI_CONFIG) + if (!existsSync(configPath)) return "local" + const content = readFileSync(configPath, "utf-8") + const parsed = JSON.parse(content) + const mode = parsed?.preferences?.listeningMode + if (mode === "local" || mode === "all") { + return mode + } + } catch (error) { + console.warn("[cli] failed to read listening mode from config", error) + } + return "local" +} + +export declare interface CliProcessManager { + on(event: "status", listener: (status: CliStatus) => void): this + on(event: "ready", listener: (status: CliStatus) => void): this + on(event: "log", listener: (entry: CliLogEntry) => void): this + on(event: "exit", listener: (status: CliStatus) => void): this + on(event: "error", listener: (error: Error) => void): this +} + +export class CliProcessManager extends EventEmitter { + private child?: ChildProcess + private status: CliStatus = { state: "stopped" } + private stdoutBuffer = "" + private stderrBuffer = "" + + async start(options: StartOptions): Promise { + if (this.child) { + await this.stop() + } + + this.stdoutBuffer = "" + this.stderrBuffer = "" + this.updateStatus({ state: "starting", port: undefined, pid: undefined, url: undefined, error: undefined }) + + const cliEntry = this.resolveCliEntry(options) + const listeningMode = this.resolveListeningMode() + const host = resolveHostForMode(listeningMode) + const args = this.buildCliArgs(options, host) + + console.info( + `[cli] launching CodeNomad CLI (${options.dev ? "dev" : "prod"}) using ${cliEntry.runner} at ${cliEntry.entry} (host=${host})`, + ) + + const env = supportsUserShell() ? getUserShellEnv() : { ...process.env } + env.ELECTRON_RUN_AS_NODE = "1" + + const spawnDetails = supportsUserShell() + ? buildUserShellCommand(`ELECTRON_RUN_AS_NODE=1 exec ${this.buildCommand(cliEntry, args)}`) + : this.buildDirectSpawn(cliEntry, args) + + const child = spawn(spawnDetails.command, spawnDetails.args, { + cwd: process.cwd(), + stdio: ["ignore", "pipe", "pipe"], + env, + shell: false, + }) + + console.info(`[cli] spawn command: ${spawnDetails.command} ${spawnDetails.args.join(" ")}`) + if (!child.pid) { + console.error("[cli] spawn failed: no pid") + } + + this.child = child + this.updateStatus({ pid: child.pid ?? undefined }) + + child.stdout?.on("data", (data: Buffer) => { + this.handleStream(data.toString(), "stdout") + }) + + child.stderr?.on("data", (data: Buffer) => { + this.handleStream(data.toString(), "stderr") + }) + + child.on("error", (error) => { + console.error("[cli] failed to start CLI:", error) + this.updateStatus({ state: "error", error: error.message }) + this.emit("error", error) + }) + + child.on("exit", (code, signal) => { + const failed = this.status.state !== "ready" + const error = failed ? this.status.error ?? `CLI exited with code ${code ?? 0}${signal ? ` (${signal})` : ""}` : undefined + console.info(`[cli] exit (code=${code}, signal=${signal || ""})${error ? ` error=${error}` : ""}`) + this.updateStatus({ state: failed ? "error" : "stopped", error }) + if (failed && error) { + this.emit("error", new Error(error)) + } + this.emit("exit", this.status) + this.child = undefined + }) + + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + this.handleTimeout() + reject(new Error("CLI startup timeout")) + }, 60000) + + this.once("ready", (status) => { + clearTimeout(timeout) + resolve(status) + }) + + this.once("error", (error) => { + clearTimeout(timeout) + reject(error) + }) + }) + } + + async stop(): Promise { + const child = this.child + if (!child) { + this.updateStatus({ state: "stopped" }) + return + } + + return new Promise((resolve) => { + const killTimeout = setTimeout(() => { + child.kill("SIGKILL") + }, 4000) + + child.on("exit", () => { + clearTimeout(killTimeout) + this.child = undefined + console.info("[cli] CLI process exited") + this.updateStatus({ state: "stopped" }) + resolve() + }) + + child.kill("SIGTERM") + }) + } + + getStatus(): CliStatus { + return { ...this.status } + } + + private resolveListeningMode(): ListeningMode { + return readListeningModeFromConfig() + } + + private handleTimeout() { + if (this.child) { + this.child.kill("SIGKILL") + this.child = undefined + } + this.updateStatus({ state: "error", error: "CLI did not start in time" }) + this.emit("error", new Error("CLI did not start in time")) + } + + private handleStream(chunk: string, stream: "stdout" | "stderr") { + if (stream === "stdout") { + this.stdoutBuffer += chunk + this.processBuffer("stdout") + } else { + this.stderrBuffer += chunk + this.processBuffer("stderr") + } + } + + private processBuffer(stream: "stdout" | "stderr") { + const buffer = stream === "stdout" ? this.stdoutBuffer : this.stderrBuffer + const lines = buffer.split("\n") + const trailing = lines.pop() ?? "" + + if (stream === "stdout") { + this.stdoutBuffer = trailing + } else { + this.stderrBuffer = trailing + } + + for (const line of lines) { + if (!line.trim()) continue + console.info(`[cli][${stream}] ${line}`) + this.emit("log", { stream, message: line }) + + const port = this.extractPort(line) + if (port && this.status.state === "starting") { + const url = `http://127.0.0.1:${port}` + console.info(`[cli] ready on ${url}`) + this.updateStatus({ state: "ready", port, url }) + this.emit("ready", this.status) + } + } + } + + private extractPort(line: string): number | null { + const readyMatch = line.match(/CodeNomad Server is ready at http:\/\/[^:]+:(\d+)/i) + if (readyMatch) { + return parseInt(readyMatch[1], 10) + } + + if (line.toLowerCase().includes("http server listening")) { + const httpMatch = line.match(/:(\d{2,5})(?!.*:\d)/) + if (httpMatch) { + return parseInt(httpMatch[1], 10) + } + try { + const parsed = JSON.parse(line) + if (typeof parsed.port === "number") { + return parsed.port + } + } catch { + // not JSON, ignore + } + } + + return null + } + + private updateStatus(patch: Partial) { + this.status = { ...this.status, ...patch } + this.emit("status", this.status) + } + + private buildCliArgs(options: StartOptions, host: string): string[] { + const args = ["serve", "--host", host, "--port", "0"] + + if (options.dev) { + args.push("--ui-dev-server", "http://localhost:3000", "--log-level", "debug") + } + + return args + } + + private buildCommand(cliEntry: CliEntryResolution, args: string[]): string { + const parts = [JSON.stringify(process.execPath)] + if (cliEntry.runner === "tsx" && cliEntry.runnerPath) { + parts.push(JSON.stringify(cliEntry.runnerPath)) + } + parts.push(JSON.stringify(cliEntry.entry)) + args.forEach((arg) => parts.push(JSON.stringify(arg))) + return parts.join(" ") + } + + private buildDirectSpawn(cliEntry: CliEntryResolution, args: string[]) { + if (cliEntry.runner === "tsx") { + return { command: process.execPath, args: [cliEntry.runnerPath!, cliEntry.entry, ...args] } + } + + return { command: process.execPath, args: [cliEntry.entry, ...args] } + } + + private resolveCliEntry(options: StartOptions): CliEntryResolution { + if (options.dev) { + const tsxPath = this.resolveTsx() + if (!tsxPath) { + throw new Error("tsx is required to run the CLI in development mode. Please install dependencies.") + } + const devEntry = this.resolveDevEntry() + return { entry: devEntry, runner: "tsx", runnerPath: tsxPath } + } + + const distEntry = this.resolveProdEntry() + return { entry: distEntry, runner: "node" } + } + + private resolveTsx(): string | null { + const candidates: Array string)> = [ + () => nodeRequire.resolve("tsx/cli"), + () => nodeRequire.resolve("tsx/dist/cli.mjs"), + () => nodeRequire.resolve("tsx/dist/cli.cjs"), + path.resolve(process.cwd(), "node_modules", "tsx", "dist", "cli.mjs"), + path.resolve(process.cwd(), "node_modules", "tsx", "dist", "cli.cjs"), + path.resolve(process.cwd(), "..", "node_modules", "tsx", "dist", "cli.mjs"), + path.resolve(process.cwd(), "..", "node_modules", "tsx", "dist", "cli.cjs"), + path.resolve(process.cwd(), "..", "..", "node_modules", "tsx", "dist", "cli.mjs"), + path.resolve(process.cwd(), "..", "..", "node_modules", "tsx", "dist", "cli.cjs"), + path.resolve(app.getAppPath(), "..", "node_modules", "tsx", "dist", "cli.mjs"), + path.resolve(app.getAppPath(), "..", "node_modules", "tsx", "dist", "cli.cjs"), + ] + + for (const candidate of candidates) { + try { + const resolved = typeof candidate === "function" ? candidate() : candidate + if (resolved && existsSync(resolved)) { + return resolved + } + } catch { + continue + } + } + + return null + } + + private resolveDevEntry(): string { + const entry = path.resolve(process.cwd(), "..", "server", "src", "index.ts") + if (!existsSync(entry)) { + throw new Error(`Dev CLI entry not found at ${entry}. Run npm run dev:electron from the repository root after installing dependencies.`) + } + return entry + } + + private resolveProdEntry(): string { + try { + const entry = nodeRequire.resolve("@neuralnomads/codenomad/dist/bin.js") + if (existsSync(entry)) { + return entry + } + } catch { + // fall through to error below + } + throw new Error("Unable to locate CodeNomad CLI build (dist/bin.js). Run npm run build --workspace @neuralnomads/codenomad.") + } +} + diff --git a/packages/electron-app/electron/main/storage.ts b/packages/electron-app/electron/main/storage.ts new file mode 100644 index 0000000..a93cc19 --- /dev/null +++ b/packages/electron-app/electron/main/storage.ts @@ -0,0 +1,121 @@ +import { app, ipcMain } from "electron" +import { join } from "path" +import { readFile, writeFile, mkdir, unlink, stat } from "fs/promises" +import { existsSync } from "fs" + +const CONFIG_DIR = join(app.getPath("home"), ".config", "codenomad") +const CONFIG_FILE = join(CONFIG_DIR, "config.json") +const INSTANCES_DIR = join(CONFIG_DIR, "instances") + +// File watching for config changes +let configWatchers = new Set() +let configLastModified = 0 +let configCache: string | null = null + +async function ensureDirectories() { + try { + await mkdir(CONFIG_DIR, { recursive: true }) + await mkdir(INSTANCES_DIR, { recursive: true }) + } catch (error) { + console.error("Failed to create directories:", error) + } +} + +async function readConfigWithCache(): Promise { + try { + const stats = await stat(CONFIG_FILE) + const currentModified = stats.mtime.getTime() + + // If file hasn't been modified since last read, return cache + if (configCache && configLastModified >= currentModified) { + return configCache + } + + const content = await readFile(CONFIG_FILE, "utf-8") + configCache = content + configLastModified = currentModified + return content + } catch (error) { + // File doesn't exist or can't be read + configCache = null + configLastModified = 0 + throw error + } +} + +function invalidateConfigCache() { + configCache = null + configLastModified = 0 +} + +export function setupStorageIPC() { + ensureDirectories() + + ipcMain.handle("storage:getConfigPath", async () => CONFIG_FILE) + ipcMain.handle("storage:getInstancesDir", async () => INSTANCES_DIR) + + ipcMain.handle("storage:readConfigFile", async () => { + try { + return await readConfigWithCache() + } catch (error) { + // Return empty config if file doesn't exist + return JSON.stringify({ preferences: { showThinkingBlocks: false, thinkingBlocksExpansion: "expanded" }, recentFolders: [] }, null, 2) + } + }) + + ipcMain.handle("storage:writeConfigFile", async (_, content: string) => { + try { + await writeFile(CONFIG_FILE, content, "utf-8") + invalidateConfigCache() + + // Notify other renderer processes about config change + const windows = require("electron").BrowserWindow.getAllWindows() + windows.forEach((win: any) => { + if (win.webContents && !win.webContents.isDestroyed()) { + win.webContents.send("storage:configChanged") + } + }) + } catch (error) { + console.error("Failed to write config file:", error) + throw error + } + }) + + ipcMain.handle("storage:readInstanceFile", async (_, filename: string) => { + const instanceFile = join(INSTANCES_DIR, `${filename}.json`) + try { + return await readFile(instanceFile, "utf-8") + } catch (error) { + // Return empty instance data if file doesn't exist + return JSON.stringify({ messageHistory: [] }, null, 2) + } + }) + + ipcMain.handle("storage:writeInstanceFile", async (_, filename: string, content: string) => { + const instanceFile = join(INSTANCES_DIR, `${filename}.json`) + try { + await writeFile(instanceFile, content, "utf-8") + } catch (error) { + console.error(`Failed to write instance file for ${filename}:`, error) + throw error + } + }) + + ipcMain.handle("storage:deleteInstanceFile", async (_, filename: string) => { + const instanceFile = join(INSTANCES_DIR, `${filename}.json`) + try { + if (existsSync(instanceFile)) { + await unlink(instanceFile) + } + } catch (error) { + console.error(`Failed to delete instance file for ${filename}:`, error) + throw error + } + }) +} + +// Clean up on app quit +app.on("before-quit", () => { + configCache = null + configLastModified = 0 +}) diff --git a/packages/electron-app/electron/main/user-shell.ts b/packages/electron-app/electron/main/user-shell.ts new file mode 100644 index 0000000..ee49e7c --- /dev/null +++ b/packages/electron-app/electron/main/user-shell.ts @@ -0,0 +1,139 @@ +import { spawn, spawnSync } from "child_process" +import path from "path" + +interface ShellCommand { + command: string + args: string[] +} + +const isWindows = process.platform === "win32" + +function getDefaultShellPath(): string { + if (process.env.SHELL && process.env.SHELL.trim().length > 0) { + return process.env.SHELL + } + + if (process.platform === "darwin") { + return "/bin/zsh" + } + + return "/bin/bash" +} + +function wrapCommandForShell(command: string, shellPath: string): string { + const shellName = path.basename(shellPath) + + if (shellName.includes("bash")) { + return 'if [ -f ~/.bashrc ]; then source ~/.bashrc >/dev/null 2>&1; fi; ' + command + } + + if (shellName.includes("zsh")) { + return 'if [ -f ~/.zshrc ]; then source ~/.zshrc >/dev/null 2>&1; fi; ' + command + } + + return command +} + +function buildShellArgs(shellPath: string): string[] { + const shellName = path.basename(shellPath) + if (shellName.includes("zsh")) { + return ["-l", "-i", "-c"] + } + return ["-l", "-c"] +} + +function sanitizeShellEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv { + const cleaned = { ...env } + delete cleaned.npm_config_prefix + delete cleaned.NPM_CONFIG_PREFIX + return cleaned +} + +export function supportsUserShell(): boolean { + return !isWindows +} + +export function buildUserShellCommand(userCommand: string): ShellCommand { + if (!supportsUserShell()) { + throw new Error("User shell invocation is only supported on POSIX platforms") + } + + const shellPath = getDefaultShellPath() + const script = wrapCommandForShell(userCommand, shellPath) + const args = buildShellArgs(shellPath) + + return { + command: shellPath, + args: [...args, script], + } +} + +export function getUserShellEnv(): NodeJS.ProcessEnv { + if (!supportsUserShell()) { + throw new Error("User shell invocation is only supported on POSIX platforms") + } + return sanitizeShellEnv(process.env) +} + +export function runUserShellCommand(userCommand: string, timeoutMs = 5000): Promise { + if (!supportsUserShell()) { + return Promise.reject(new Error("User shell invocation is only supported on POSIX platforms")) + } + + const { command, args } = buildUserShellCommand(userCommand) + const env = getUserShellEnv() + + return new Promise((resolve, reject) => { + const child = spawn(command, args, { + stdio: ["ignore", "pipe", "pipe"], + env, + }) + + let stdout = "" + let stderr = "" + + const timeout = setTimeout(() => { + child.kill("SIGTERM") + reject(new Error(`Shell command timed out after ${timeoutMs}ms`)) + }, timeoutMs) + + child.stdout?.on("data", (data) => { + stdout += data.toString() + }) + + child.stderr?.on("data", (data) => { + stderr += data.toString() + }) + + child.on("error", (error) => { + clearTimeout(timeout) + reject(error) + }) + + child.on("close", (code) => { + clearTimeout(timeout) + if (code === 0) { + resolve(stdout.trim()) + } else { + reject(new Error(stderr.trim() || `Shell command exited with code ${code}`)) + } + }) + }) +} + +export function runUserShellCommandSync(userCommand: string): string { + if (!supportsUserShell()) { + throw new Error("User shell invocation is only supported on POSIX platforms") + } + + const { command, args } = buildUserShellCommand(userCommand) + const env = getUserShellEnv() + const result = spawnSync(command, args, { encoding: "utf-8", env }) + + if (result.status !== 0) { + const stderr = (result.stderr || "").toString().trim() + throw new Error(stderr || "Shell command failed") + } + + return (result.stdout || "").toString().trim() +} diff --git a/packages/electron-app/electron/preload/index.cjs b/packages/electron-app/electron/preload/index.cjs new file mode 100644 index 0000000..8a7d6bf --- /dev/null +++ b/packages/electron-app/electron/preload/index.cjs @@ -0,0 +1,17 @@ +const { contextBridge, ipcRenderer } = require("electron") + +const electronAPI = { + onCliStatus: (callback) => { + ipcRenderer.on("cli:status", (_, data) => callback(data)) + return () => ipcRenderer.removeAllListeners("cli:status") + }, + onCliError: (callback) => { + ipcRenderer.on("cli:error", (_, data) => callback(data)) + return () => ipcRenderer.removeAllListeners("cli:error") + }, + getCliStatus: () => ipcRenderer.invoke("cli:getStatus"), + restartCli: () => ipcRenderer.invoke("cli:restart"), + openDialog: (options) => ipcRenderer.invoke("dialog:open", options), +} + +contextBridge.exposeInMainWorld("electronAPI", electronAPI) diff --git a/packages/electron-app/electron/resources/icon.icns b/packages/electron-app/electron/resources/icon.icns new file mode 100644 index 0000000..4431bb4 Binary files /dev/null and b/packages/electron-app/electron/resources/icon.icns differ diff --git a/packages/electron-app/electron/resources/icon.ico b/packages/electron-app/electron/resources/icon.ico new file mode 100644 index 0000000..7d50243 Binary files /dev/null and b/packages/electron-app/electron/resources/icon.ico differ diff --git a/packages/electron-app/electron/resources/icon.png b/packages/electron-app/electron/resources/icon.png new file mode 100644 index 0000000..4c08915 Binary files /dev/null and b/packages/electron-app/electron/resources/icon.png differ diff --git a/packages/electron-app/electron/tsconfig.json b/packages/electron-app/electron/tsconfig.json new file mode 100644 index 0000000..aa4873c --- /dev/null +++ b/packages/electron-app/electron/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "noEmit": true + }, + "include": ["./**/*.ts", "./**/*.tsx"] +} diff --git a/packages/electron-app/package.json b/packages/electron-app/package.json new file mode 100644 index 0000000..0a72cf7 --- /dev/null +++ b/packages/electron-app/package.json @@ -0,0 +1,137 @@ +{ + "name": "@neuralnomads/codenomad-electron-app", + "version": "0.4.0", + "description": "CodeNomad - AI coding assistant", + "author": { + "name": "Neural Nomads", + "email": "codenomad@neuralnomads.ai" + }, + "type": "module", + "main": "dist/main/main.js", + "repository": { + "type": "git", + "url": "https://github.com/NeuralNomadsAI/CodeNomad.git" + }, + "homepage": "https://github.com/NeuralNomadsAI/CodeNomad", + "scripts": { + "dev": "electron-vite dev", + "dev:electron": "NODE_ENV=development ELECTRON_ENABLE_LOGGING=1 NODE_OPTIONS=\"--import tsx\" electron electron/main/main.ts", + "build": "electron-vite build", + "typecheck": "tsc --noEmit -p tsconfig.json", + "preview": "electron-vite preview", + "build:binaries": "node scripts/build.js", + "build:mac": "node scripts/build.js mac", + "build:mac-x64": "node scripts/build.js mac-x64", + "build:mac-arm64": "node scripts/build.js mac-arm64", + "build:win": "node scripts/build.js win", + "build:win-arm64": "node scripts/build.js win-arm64", + "build:linux": "node scripts/build.js linux", + "build:linux-arm64": "node scripts/build.js linux-arm64", + "build:linux-rpm": "node scripts/build.js linux-rpm", + "build:all": "node scripts/build.js all", + "package:mac": "electron-builder --mac", + "package:win": "electron-builder --win", + "package:linux": "electron-builder --linux" + }, + "dependencies": { + "@neuralnomads/codenomad": "file:../server", + "@codenomad/ui": "file:../ui" + }, + "devDependencies": { + "7zip-bin": "^5.2.0", + "app-builder-bin": "^4.2.0", + "electron": "39.0.0", + "electron-builder": "^24.0.0", + "electron-vite": "4.0.1", + "png2icons": "^2.0.1", + "pngjs": "^7.0.0", + "tsx": "^4.20.6", + "typescript": "^5.3.0", + "vite": "^5.0.0", + "vite-plugin-solid": "^2.10.0" + }, + "build": { + "appId": "ai.opencode.client", + "productName": "CodeNomad", + "directories": { + "output": "release", + "buildResources": "electron/resources" + }, + "files": [ + "dist/**/*", + "package.json" + ], + "extraResources": [ + { + "from": "electron/resources", + "to": "", + "filter": [ + "!icon.icns", + "!icon.ico" + ] + } + ], + "mac": { + "category": "public.app-category.developer-tools", + "target": [ + { + "target": "zip", + "arch": [ + "x64", + "arm64" + ] + } + ], + "artifactName": "CodeNomad-${version}-${os}-${arch}.${ext}", + "icon": "electron/resources/icon.icns" + }, + "dmg": { + "contents": [ + { + "x": 130, + "y": 220 + }, + { + "x": 410, + "y": 220, + "type": "link", + "path": "/Applications" + } + ] + }, + "win": { + "target": [ + { + "target": "zip", + "arch": [ + "x64", + "arm64" + ] + } + ], + "artifactName": "CodeNomad-${version}-${os}-${arch}.${ext}", + "icon": "electron/resources/icon.ico" + }, + "nsis": { + "oneClick": false, + "allowToChangeInstallationDirectory": true, + "createDesktopShortcut": true, + "createStartMenuShortcut": true + }, + "linux": { + "target": [ + { + "target": "zip", + "arch": [ + "x64", + "arm64" + ] + } + ], + "artifactName": "CodeNomad-${version}-${os}-${arch}.${ext}", + "category": "Development", + "icon": "electron/resources/icon.png" + } + }, + "private": true +} diff --git a/packages/electron-app/scripts/build.js b/packages/electron-app/scripts/build.js new file mode 100644 index 0000000..17f58d0 --- /dev/null +++ b/packages/electron-app/scripts/build.js @@ -0,0 +1,131 @@ +#!/usr/bin/env node + +import { spawn } from "child_process" +import { existsSync } from "fs" +import { join } from "path" +import { fileURLToPath } from "url" + +const __dirname = fileURLToPath(new URL(".", import.meta.url)) +const appDir = join(__dirname, "..") +const workspaceRoot = join(appDir, "..", "..") + +const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm" +const npxCmd = process.platform === "win32" ? "npx.cmd" : "npx" +const nodeModulesPath = join(appDir, "node_modules") +const workspaceNodeModulesPath = join(workspaceRoot, "node_modules") + +const platforms = { + mac: { + args: ["--mac", "--x64", "--arm64"], + description: "macOS (Intel & Apple Silicon)", + }, + "mac-x64": { + args: ["--mac", "--x64"], + description: "macOS (Intel only)", + }, + "mac-arm64": { + args: ["--mac", "--arm64"], + description: "macOS (Apple Silicon only)", + }, + win: { + args: ["--win", "--x64"], + description: "Windows (x64)", + }, + "win-arm64": { + args: ["--win", "--arm64"], + description: "Windows (ARM64)", + }, + linux: { + args: ["--linux", "--x64"], + description: "Linux (x64)", + }, + "linux-arm64": { + args: ["--linux", "--arm64"], + description: "Linux (ARM64)", + }, + "linux-rpm": { + args: ["--linux", "rpm", "--x64", "--arm64"], + description: "Linux RPM packages (x64 & ARM64)", + }, + all: { + args: ["--mac", "--win", "--linux", "--x64", "--arm64"], + description: "All platforms (macOS, Windows, Linux)", + }, +} + +function run(command, args, options = {}) { + return new Promise((resolve, reject) => { + const spawnOptions = { + cwd: appDir, + stdio: "inherit", + shell: process.platform === "win32", + ...options, + env: { ...process.env, NODE_PATH: nodeModulesPath, ...(options.env || {}) }, + } + + const child = spawn(command, args, spawnOptions) + + child.on("error", reject) + child.on("exit", (code) => { + if (code === 0) { + resolve(undefined) + } else { + reject(new Error(`${command} ${args.join(" ")} exited with code ${code}`)) + } + }) + }) +} + +function printAvailablePlatforms() { + console.error(`\nAvailable platforms:`) + for (const [name, cfg] of Object.entries(platforms)) { + console.error(` - ${name.padEnd(12)} : ${cfg.description}`) + } +} + +async function build(platform) { + const config = platforms[platform] + + if (!config) { + console.error(`āŒ Unknown platform: ${platform}`) + printAvailablePlatforms() + process.exit(1) + } + + console.log(`\nšŸ”Ø Building for: ${config.description}\n`) + + try { + console.log("šŸ“¦ Step 1/3: Building CLI dependency...\n") + await run(npmCmd, ["run", "build", "--workspace", "@neuralnomads/codenomad"], { + cwd: workspaceRoot, + env: { NODE_PATH: workspaceNodeModulesPath }, + }) + + console.log("\nšŸ“¦ Step 2/3: Building Electron app...\n") + await run(npmCmd, ["run", "build"]) + + console.log("\nšŸ“¦ Step 3/3: Packaging binaries...\n") + const distPath = join(appDir, "dist") + if (!existsSync(distPath)) { + throw new Error("dist/ directory not found. Build failed.") + } + + await run(npxCmd, ["electron-builder", "--publish=never", ...config.args]) + + console.log("\nāœ… Build complete!") + console.log(`šŸ“ Binaries available in: ${join(appDir, "release")}\n`) + } catch (error) { + console.error("\nāŒ Build failed:", error) + process.exit(1) + } +} + +const platform = process.argv[2] || "mac" + +console.log(` +╔════════════════════════════════════════╗ +ā•‘ CodeNomad - Binary Builder ā•‘ +ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā• +`) + +await build(platform) diff --git a/packages/electron-app/scripts/dev.sh b/packages/electron-app/scripts/dev.sh new file mode 100644 index 0000000..acab779 --- /dev/null +++ b/packages/electron-app/scripts/dev.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +if ! command -v node >/dev/null 2>&1; then + echo "Node.js is required to run the development environment." >&2 + exit 1 +fi + +# Resolve the Electron binary via Node to avoid Bun resolution hiccups +ELECTRON_EXEC_PATH="$(node -p "require('electron')")" + +if [[ -z "${ELECTRON_EXEC_PATH}" ]]; then + echo "Failed to resolve the Electron binary path." >&2 + exit 1 +fi + +export NODE_ENV="${NODE_ENV:-development}" +export ELECTRON_EXEC_PATH + +# ELECTRON_VITE_BIN="$ROOT_DIR/node_modules/.bin/electron-vite" + +if [[ ! -x "${ELECTRON_VITE_BIN}" ]]; then + echo "electron-vite binary not found. Have you installed dependencies?" >&2 + exit 1 +fi + +exec "${ELECTRON_VITE_BIN}" dev "$@" diff --git a/packages/electron-app/scripts/generate-icons.js b/packages/electron-app/scripts/generate-icons.js new file mode 100644 index 0000000..c049ef6 --- /dev/null +++ b/packages/electron-app/scripts/generate-icons.js @@ -0,0 +1,155 @@ +#!/usr/bin/env node + +import { mkdirSync, readFileSync, writeFileSync } from "fs" +import { resolve, join, basename } from "path" +import { PNG } from "pngjs" +import png2icons from "png2icons" + +function printUsage() { + console.log(`\nUsage: node scripts/generate-icons.js [outputDir] [--name icon] [--radius 0.22]\n\nOptions:\n --name Base filename for generated assets (default: icon)\n --radius Corner radius ratio between 0 and 0.5 (default: 0.22)\n --help Show this message\n`) +} + +function parseArgs(argv) { + const args = [...argv] + const options = { + name: "icon", + radius: 0.22, + } + + for (let i = 0; i < args.length; i++) { + const token = args[i] + if (token === "--help" || token === "-h") { + options.help = true + continue + } + if (token === "--name" && i + 1 < args.length) { + options.name = args[i + 1] + i++ + continue + } + if (token === "--radius" && i + 1 < args.length) { + options.radius = Number(args[i + 1]) + i++ + continue + } + if (!options.input) { + options.input = token + continue + } + if (!options.output) { + options.output = token + continue + } + } + + return options +} + +function applyRoundedCorners(png, ratio) { + const { width, height, data } = png + const clamped = Math.max(0, Math.min(ratio, 0.5)) + if (clamped === 0) return png + + const radius = Math.max(1, Math.min(width, height) * clamped) + const radiusSq = radius * radius + const rightThreshold = width - radius + const bottomThreshold = height - radius + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const idx = (width * y + x) * 4 + if (data[idx + 3] === 0) continue + + const px = x + 0.5 + const py = y + 0.5 + + const inLeft = px < radius + const inRight = px > rightThreshold + const inTop = py < radius + const inBottom = py > bottomThreshold + + let outside = false + + if (inLeft && inTop) { + outside = (px - radius) ** 2 + (py - radius) ** 2 > radiusSq + } else if (inRight && inTop) { + outside = (px - rightThreshold) ** 2 + (py - radius) ** 2 > radiusSq + } else if (inLeft && inBottom) { + outside = (px - radius) ** 2 + (py - bottomThreshold) ** 2 > radiusSq + } else if (inRight && inBottom) { + outside = (px - rightThreshold) ** 2 + (py - bottomThreshold) ** 2 > radiusSq + } + + if (outside) { + data[idx + 3] = 0 + } + } + } + + return png +} + +async function main() { + const args = parseArgs(process.argv.slice(2)) + + if (args.help || !args.input) { + printUsage() + process.exit(args.help ? 0 : 1) + } + + const inputPath = resolve(args.input) + const outputDir = resolve(args.output || "electron/resources") + const baseName = args.name || basename(inputPath, ".png") + const radiusRatio = Number.isFinite(args.radius) ? args.radius : 0.22 + + let buffer + try { + buffer = readFileSync(inputPath) + } catch (error) { + console.error(`Failed to read ${inputPath}:`, error.message) + process.exit(1) + } + + let png + try { + png = PNG.sync.read(buffer) + } catch (error) { + console.error("Input must be a valid PNG:", error.message) + process.exit(1) + } + + applyRoundedCorners(png, radiusRatio) + + const roundedBuffer = PNG.sync.write(png) + + try { + mkdirSync(outputDir, { recursive: true }) + } catch (error) { + console.error("Failed to create output directory:", error.message) + process.exit(1) + } + + const pngPath = join(outputDir, `${baseName}.png`) + writeFileSync(pngPath, roundedBuffer) + + const icns = png2icons.createICNS(roundedBuffer, png2icons.BICUBIC, false) + if (!icns) { + console.error("Failed to create ICNS file. Make sure the source PNG is at least 256x256.") + process.exit(1) + } + writeFileSync(join(outputDir, `${baseName}.icns`), icns) + + const ico = png2icons.createICO(roundedBuffer, png2icons.BICUBIC, false) + if (!ico) { + console.error("Failed to create ICO file. Make sure the source PNG is at least 256x256.") + process.exit(1) + } + writeFileSync(join(outputDir, `${baseName}.ico`), ico) + + console.log(`\nGenerated assets in ${outputDir}:`) + console.log(`- ${baseName}.png`) + console.log(`- ${baseName}.icns`) + console.log(`- ${baseName}.ico`) +} + +main() diff --git a/packages/electron-app/tsconfig.json b/packages/electron-app/tsconfig.json new file mode 100644 index 0000000..af51792 --- /dev/null +++ b/packages/electron-app/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "lib": ["ES2020"], + "moduleResolution": "bundler", + "resolveJsonModule": true, + "allowImportingTsExtensions": true, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "noEmit": true + }, + "include": ["electron/**/*.ts", "electron.vite.config.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/opencode-config/opencode.jsonc b/packages/opencode-config/opencode.jsonc new file mode 100644 index 0000000..c3eb6a5 --- /dev/null +++ b/packages/opencode-config/opencode.jsonc @@ -0,0 +1,3 @@ +{ + "$schema": "https://opencode.ai/config.json" +} \ No newline at end of file diff --git a/packages/opencode-config/plugin/hello.js b/packages/opencode-config/plugin/hello.js new file mode 100644 index 0000000..f37564d --- /dev/null +++ b/packages/opencode-config/plugin/hello.js @@ -0,0 +1,18 @@ +import { tool } from "@opencode-ai/plugin/tool" + +export async function HelloPlugin() { + return { + tool: { + hello: tool({ + description: "Return a friendly greeting", + args: { + name: tool.schema.string().optional().describe("Name to greet"), + }, + async execute(args) { + const target = args.name?.trim() || "CodeNomad" + return `Hello, ${target}!` + }, + }), + }, + } +} diff --git a/packages/server/.gitignore b/packages/server/.gitignore new file mode 100644 index 0000000..364fdec --- /dev/null +++ b/packages/server/.gitignore @@ -0,0 +1 @@ +public/ diff --git a/packages/server/.npmignore b/packages/server/.npmignore new file mode 100644 index 0000000..d50860b --- /dev/null +++ b/packages/server/.npmignore @@ -0,0 +1,5 @@ +node_modules +scripts/ +src/ +tsconfig.json +*.tsbuildinfo diff --git a/packages/server/README.md b/packages/server/README.md new file mode 100644 index 0000000..0649aa2 --- /dev/null +++ b/packages/server/README.md @@ -0,0 +1,58 @@ +# CodeNomad Server + +**CodeNomad Server** is the high-performance engine behind the CodeNomad cockpit. It transforms your machine into a robust development host, managing the lifecycle of multiple OpenCode instances and providing the low-latency data streams that long-haul builders demand. It bridges your local filesystem with the UI, ensuring that whether you are on localhost or a remote tunnel, you have the speed, clarity, and control of a native workspace. + +## Features & Capabilities + +### šŸŒ Deployment Freedom +- **Remote Access**: Host CodeNomad on a powerful workstation and access it from your lightweight laptop. +- **Code Anywhere**: Tunnel in via VPN or SSH to code securely from coffee shops or while traveling. +- **Multi-Device**: The responsive web client works on tablets and iPads, turning any screen into a dev terminal. +- **Always-On**: Run as a background service so your sessions are always ready when you connect. + +### āš”ļø Workspace Power +- **Multi-Instance**: Juggle multiple OpenCode sessions side-by-side with per-instance tabs. +- **Long-Context Native**: Scroll through massive transcripts without hitches. +- **Deep Task Awareness**: Monitor background tasks and child sessions without losing your flow. +- **Command Palette**: A single, global palette to jump tabs, launch tools, and fire shortcuts. + +## Prerequisites +- **OpenCode**: `opencode` must be installed and configured on your system. +- Node.js 18+ and npm (for running or building from source). +- A workspace folder on disk you want to serve. +- Optional: a Chromium-based browser if you want `--launch` to open the UI automatically. + +## Usage + +### Run via npx (Recommended) +You can run CodeNomad directly without installing it: + +```sh +npx @neuralnomads/codenomad --launch +``` + +### Install Globally +Or install it globally to use the `codenomad` command: + +```sh +npm install -g @neuralnomads/codenomad +codenomad --launch +``` + +### Common Flags +You can configure the server using flags or environment variables: + +| Flag | Env Variable | Description | +|------|--------------|-------------| +| `--port ` | `CLI_PORT` | HTTP port (default 9898) | +| `--host ` | `CLI_HOST` | Interface to bind (default 127.0.0.1) | +| `--workspace-root ` | `CLI_WORKSPACE_ROOT` | Default root for new workspaces | +| `--unrestricted-root` | `CLI_UNRESTRICTED_ROOT` | Allow full-filesystem browsing | +| `--config ` | `CLI_CONFIG` | Config file location | +| `--launch` | `CLI_LAUNCH` | Open the UI in a Chromium-based browser | +| `--log-level ` | `CLI_LOG_LEVEL` | Logging level (trace, debug, info, warn, error) | + +### Data Storage +- **Config**: `~/.config/codenomad/config.json` +- **Instance Data**: `~/.config/codenomad/instances` (chat history, etc.) + diff --git a/packages/server/package-lock.json b/packages/server/package-lock.json new file mode 100644 index 0000000..ca16fa3 --- /dev/null +++ b/packages/server/package-lock.json @@ -0,0 +1,1333 @@ +{ + "name": "@neuralnomads/codenomad", + "version": "0.4.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@neuralnomads/codenomad", + "version": "0.4.0", + "dependencies": { + "@fastify/cors": "^8.5.0", + "commander": "^12.1.0", + "fastify": "^4.28.1", + "pino": "^9.4.0", + "zod": "^3.23.8" + }, + "devDependencies": { + "ts-node": "^10.9.2", + "tsx": "^4.20.6", + "typescript": "^5.6.3" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fastify/ajv-compiler": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-3.6.0.tgz", + "integrity": "sha512-LwdXQJjmMD+GwLOkP7TVC68qa+pSSogeWWmznRJ/coyTcfe9qA05AHFSe1eZFwK6q+xVRpChnvFUkf1iYaSZsQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.11.0", + "ajv-formats": "^2.1.1", + "fast-uri": "^2.0.0" + } + }, + "node_modules/@fastify/cors": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-8.5.0.tgz", + "integrity": "sha512-/oZ1QSb02XjP0IK1U0IXktEsw/dUBTxJOW7IpIeO8c/tNalw/KjoNSJv1Sf6eqoBPO+TDGkifq6ynFK3v68HFQ==", + "license": "MIT", + "dependencies": { + "fastify-plugin": "^4.0.0", + "mnemonist": "0.39.6" + } + }, + "node_modules/@fastify/error": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-3.4.1.tgz", + "integrity": "sha512-wWSvph+29GR783IhmvdwWnN4bUxTD01Vm5Xad4i7i1VuAOItLvbPAb69sb0IQ2N57yprvhNIwAP5B6xfKTmjmQ==", + "license": "MIT" + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-4.3.0.tgz", + "integrity": "sha512-aZAXGYo6m22Fk1zZzEUKBvut/CIIQe/BapEORnxiD5Qr0kPHqqI69NtEMCme74h+at72sPhbkb4ZrLd1W3KRLA==", + "license": "MIT", + "dependencies": { + "fast-json-stringify": "^5.7.0" + } + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.1.1.tgz", + "integrity": "sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv/node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/avvio": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-8.4.0.tgz", + "integrity": "sha512-CDSwaxINFy59iNwhYnkvALBwZiTydGkOecZyPkqBpABYR1KqGEsET0VOOYDwtleZSUIdeY36DC2bSZ24CO1igA==", + "license": "MIT", + "dependencies": { + "@fastify/error": "^3.3.0", + "fastq": "^1.17.1" + } + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/fast-content-type-parse": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-1.1.0.tgz", + "integrity": "sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==", + "license": "MIT" + }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stringify": { + "version": "5.16.1", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-5.16.1.tgz", + "integrity": "sha512-KAdnLvy1yu/XrRtP+LJnxbBGrhN+xXu+gt3EUvZhYGKCr3lFHq/7UFJHHFgmJKoqlh6B40bZLEv7w46B0mqn1g==", + "license": "MIT", + "dependencies": { + "@fastify/merge-json-schemas": "^0.1.0", + "ajv": "^8.10.0", + "ajv-formats": "^3.0.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^2.1.0", + "json-schema-ref-resolver": "^1.0.1", + "rfdc": "^1.2.0" + } + }, + "node_modules/fast-json-stringify/node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "license": "MIT", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-uri": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-2.4.0.tgz", + "integrity": "sha512-ypuAmmMKInk5q7XcepxlnUWDLWv4GFtaJqAzWKqn62IpQ3pejtr5dTVbt3vwqVaMKmkNR55sTT+CqUKIaT21BA==", + "license": "MIT" + }, + "node_modules/fastify": { + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-4.29.1.tgz", + "integrity": "sha512-m2kMNHIG92tSNWv+Z3UeTR9AWLLuo7KctC7mlFPtMEVrfjIhmQhkQnT9v15qA/BfVq3vvj134Y0jl9SBje3jXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/ajv-compiler": "^3.5.0", + "@fastify/error": "^3.4.0", + "@fastify/fast-json-stringify-compiler": "^4.3.0", + "abstract-logging": "^2.0.1", + "avvio": "^8.3.0", + "fast-content-type-parse": "^1.1.0", + "fast-json-stringify": "^5.8.0", + "find-my-way": "^8.0.0", + "light-my-request": "^5.11.0", + "pino": "^9.0.0", + "process-warning": "^3.0.0", + "proxy-addr": "^2.0.7", + "rfdc": "^1.3.0", + "secure-json-parse": "^2.7.0", + "semver": "^7.5.4", + "toad-cache": "^3.3.0" + } + }, + "node_modules/fastify-plugin": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.5.1.tgz", + "integrity": "sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==", + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/find-my-way": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-8.2.2.tgz", + "integrity": "sha512-Dobi7gcTEq8yszimcfp/R7+owiT4WncAJ7VTTgFH1jYJ5GaG1FbhjwDG820hptN0QDFvzVY3RfCzdInvGPGzjA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^3.1.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/json-schema-ref-resolver": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-1.0.1.tgz", + "integrity": "sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/light-my-request": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-5.14.0.tgz", + "integrity": "sha512-aORPWntbpH5esaYpGOOmri0OHDOe3wC5M2MQxZ9dvMLZm6DnaAn0kJlcbU9hwsQgLzmZyReKwFwwPkR+nHu5kA==", + "license": "BSD-3-Clause", + "dependencies": { + "cookie": "^0.7.0", + "process-warning": "^3.0.0", + "set-cookie-parser": "^2.4.1" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/mnemonist": { + "version": "0.39.6", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.39.6.tgz", + "integrity": "sha512-A/0v5Z59y63US00cRSLiloEIw3t5G+MiKz4BhX21FI+YBJXBOGW0ohFxTxO08dsOYlzxo87T7vGfZKYp2bcAWA==", + "license": "MIT", + "dependencies": { + "obliterator": "^2.0.1" + } + }, + "node_modules/obliterator": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.5.tgz", + "integrity": "sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==", + "license": "MIT" + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/pino": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", + "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", + "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==", + "license": "MIT" + }, + "node_modules/pino/node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/process-warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", + "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/ret": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.4.3.tgz", + "integrity": "sha512-0f4Memo5QP7WQyUEAYUO3esD/XjOc3Zjjg5CPsAq1p8sIu0XPeMbHJemKA0BO7tV0X7+A0FoEpbmHXWxPyD3wQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/safe-regex2": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-3.1.0.tgz", + "integrity": "sha512-RAAZAGbap2kBfbVhvmnTFv73NWLMvDGOITFYTZBAaY8eR+Ir4ef7Up/e7amo+y1+AH+3PtLkrt9mvcTsG9LXug==", + "license": "MIT", + "dependencies": { + "ret": "~0.4.0" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", + "license": "BSD-3-Clause" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/sonic-boom": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", + "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/toad-cache": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", + "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tsx": { + "version": "4.20.6", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", + "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/packages/server/package.json b/packages/server/package.json new file mode 100644 index 0000000..1257f82 --- /dev/null +++ b/packages/server/package.json @@ -0,0 +1,43 @@ +{ + "name": "@neuralnomads/codenomad", + "version": "0.4.0", + "description": "CodeNomad Server", + "author": { + "name": "Neural Nomads", + "email": "codenomad@neuralnomads.ai" + }, + "repository": { + "type": "git", + "url": "https://github.com/NeuralNomadsAI/CodeNomad.git" + }, + "type": "module", + "main": "dist/index.js", + "bin": { + "codenomad": "dist/bin.js" + }, + "scripts": { + "build": "npm run build:ui && npm run prepare-ui && tsc -p tsconfig.json && npm run prepare-config", + "build:ui": "npm run build --prefix ../ui", + "prepare-ui": "node ./scripts/copy-ui-dist.mjs", + "prepare-config": "node ./scripts/copy-opencode-config.mjs", + "dev": "cross-env CODENOMAD_DEV=1 CLI_UI_DEV_SERVER=http://localhost:3000 tsx src/index.ts", + "typecheck": "tsc --noEmit -p tsconfig.json" + }, + "dependencies": { + "@fastify/cors": "^8.5.0", + "@fastify/reply-from": "^9.8.0", + "@fastify/static": "^7.0.4", + "commander": "^12.1.0", + "fastify": "^4.28.1", + "fuzzysort": "^2.0.4", + "pino": "^9.4.0", + "undici": "^6.19.8", + "zod": "^3.23.8" + }, + "devDependencies": { + "cross-env": "^7.0.3", + "ts-node": "^10.9.2", + "tsx": "^4.20.6", + "typescript": "^5.6.3" + } +} diff --git a/packages/server/scripts/copy-opencode-config.mjs b/packages/server/scripts/copy-opencode-config.mjs new file mode 100644 index 0000000..b63b862 --- /dev/null +++ b/packages/server/scripts/copy-opencode-config.mjs @@ -0,0 +1,21 @@ +#!/usr/bin/env node +import { cpSync, existsSync, mkdirSync, rmSync } from "fs" +import path from "path" +import { fileURLToPath } from "url" + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) +const cliRoot = path.resolve(__dirname, "..") +const sourceDir = path.resolve(cliRoot, "../opencode-config") +const targetDir = path.resolve(cliRoot, "dist/opencode-config") + +if (!existsSync(sourceDir)) { + console.error(`[copy-opencode-config] Missing source directory at ${sourceDir}`) + process.exit(1) +} + +rmSync(targetDir, { recursive: true, force: true }) +mkdirSync(path.dirname(targetDir), { recursive: true }) +cpSync(sourceDir, targetDir, { recursive: true }) + +console.log(`[copy-opencode-config] Copied ${sourceDir} -> ${targetDir}`) diff --git a/packages/server/scripts/copy-ui-dist.mjs b/packages/server/scripts/copy-ui-dist.mjs new file mode 100644 index 0000000..fdbe481 --- /dev/null +++ b/packages/server/scripts/copy-ui-dist.mjs @@ -0,0 +1,21 @@ +#!/usr/bin/env node +import { cpSync, existsSync, mkdirSync, rmSync } from "fs" +import path from "path" +import { fileURLToPath } from "url" + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) +const cliRoot = path.resolve(__dirname, "..") +const uiDistDir = path.resolve(cliRoot, "../ui/src/renderer/dist") +const targetDir = path.resolve(cliRoot, "public") + +if (!existsSync(uiDistDir)) { + console.error(`[copy-ui-dist] Expected UI build artifacts at ${uiDistDir}. Run the UI build before bundling the CLI.`) + process.exit(1) +} + +rmSync(targetDir, { recursive: true, force: true }) +mkdirSync(targetDir, { recursive: true }) +cpSync(uiDistDir, targetDir, { recursive: true }) + +console.log(`[copy-ui-dist] Copied UI bundle from ${uiDistDir} -> ${targetDir}`) diff --git a/packages/server/src/api-types.ts b/packages/server/src/api-types.ts new file mode 100644 index 0000000..7ad858c --- /dev/null +++ b/packages/server/src/api-types.ts @@ -0,0 +1,228 @@ +import type { + AgentModelSelection, + AgentModelSelections, + ConfigFile, + ModelPreference, + OpenCodeBinary, + Preferences, + RecentFolder, +} from "./config/schema" + +/** + * Canonical HTTP/SSE contract for the CLI server. + * These types are consumed by both the CLI implementation and any UI clients. + */ + +export type WorkspaceStatus = "starting" | "ready" | "stopped" | "error" + +export interface WorkspaceDescriptor { + id: string + /** Absolute path on the server host. */ + path: string + name?: string + status: WorkspaceStatus + /** PID/port are populated when the workspace is running. */ + pid?: number + port?: number + /** Canonical proxy path the CLI exposes for this instance. */ + proxyPath: string + /** Identifier of the binary resolved from config. */ + binaryId: string + binaryLabel: string + binaryVersion?: string + createdAt: string + updatedAt: string + /** Present when `status` is "error". */ + error?: string +} + +export interface WorkspaceCreateRequest { + path: string + name?: string +} + +export type WorkspaceCreateResponse = WorkspaceDescriptor +export type WorkspaceListResponse = WorkspaceDescriptor[] +export type WorkspaceDetailResponse = WorkspaceDescriptor + +export interface WorkspaceDeleteResponse { + id: string + status: WorkspaceStatus +} + +export type LogLevel = "debug" | "info" | "warn" | "error" + +export interface WorkspaceLogEntry { + workspaceId: string + timestamp: string + level: LogLevel + message: string +} + +export interface FileSystemEntry { + name: string + /** Path relative to the CLI server root ("." represents the root itself). */ + path: string + /** Absolute path when available (unrestricted listings). */ + absolutePath?: string + type: "file" | "directory" + size?: number + /** ISO timestamp of last modification when available. */ + modifiedAt?: string +} + +export type FileSystemScope = "restricted" | "unrestricted" +export type FileSystemPathKind = "relative" | "absolute" | "drives" + +export interface FileSystemListingMetadata { + scope: FileSystemScope + /** Canonical identifier of the current view ("." for restricted roots, absolute paths otherwise). */ + currentPath: string + /** Optional parent path if navigation upward is allowed. */ + parentPath?: string + /** Absolute path representing the root or origin point for this listing. */ + rootPath: string + /** Absolute home directory of the CLI host (useful defaults for unrestricted mode). */ + homePath: string + /** Human-friendly label for the current path. */ + displayPath: string + /** Indicates whether entry paths are relative, absolute, or represent drive roots. */ + pathKind: FileSystemPathKind +} + +export interface FileSystemListResponse { + entries: FileSystemEntry[] + metadata: FileSystemListingMetadata +} + +export const WINDOWS_DRIVES_ROOT = "__drives__" + +export interface WorkspaceFileResponse { + workspaceId: string + relativePath: string + /** UTF-8 file contents; binary files should be base64 encoded by the caller. */ + contents: string +} + +export type WorkspaceFileSearchResponse = FileSystemEntry[] + +export interface InstanceData { + messageHistory: string[] + agentModelSelections: AgentModelSelection +} + +export type InstanceStreamStatus = "connecting" | "connected" | "error" | "disconnected" + +export interface InstanceStreamEvent { + type: string + properties?: Record + [key: string]: unknown +} + +export interface BinaryRecord { + id: string + path: string + label: string + version?: string + + /** Indicates that this binary will be picked when workspaces omit an explicit choice. */ + isDefault: boolean + lastValidatedAt?: string + validationError?: string +} + +export type AppConfig = ConfigFile +export type AppConfigResponse = AppConfig +export type AppConfigUpdateRequest = Partial + +export interface BinaryListResponse { + binaries: BinaryRecord[] +} + +export interface BinaryCreateRequest { + path: string + label?: string + makeDefault?: boolean +} + +export interface BinaryUpdateRequest { + label?: string + makeDefault?: boolean +} + +export interface BinaryValidationResult { + valid: boolean + version?: string + error?: string +} + +export type WorkspaceEventType = + | "workspace.created" + | "workspace.started" + | "workspace.error" + | "workspace.stopped" + | "workspace.log" + | "config.appChanged" + | "config.binariesChanged" + | "instance.dataChanged" + | "instance.event" + | "instance.eventStatus" + | "app.releaseAvailable" + +export type WorkspaceEventPayload = + | { type: "workspace.created"; workspace: WorkspaceDescriptor } + | { type: "workspace.started"; workspace: WorkspaceDescriptor } + | { type: "workspace.error"; workspace: WorkspaceDescriptor } + | { type: "workspace.stopped"; workspaceId: string } + | { type: "workspace.log"; entry: WorkspaceLogEntry } + | { type: "config.appChanged"; config: AppConfig } + | { type: "config.binariesChanged"; binaries: BinaryRecord[] } + | { type: "instance.dataChanged"; instanceId: string; data: InstanceData } + | { type: "instance.event"; instanceId: string; event: InstanceStreamEvent } + | { type: "instance.eventStatus"; instanceId: string; status: InstanceStreamStatus; reason?: string } + | { type: "app.releaseAvailable"; release: LatestReleaseInfo } + +export interface NetworkAddress { + ip: string + family: "ipv4" | "ipv6" + scope: "external" | "internal" | "loopback" + url: string +} + +export interface LatestReleaseInfo { + version: string + tag: string + url: string + channel: "stable" | "dev" + publishedAt?: string + notes?: string +} + +export interface ServerMeta { + /** Base URL clients should target for REST calls (useful for Electron embedding). */ + httpBaseUrl: string + /** SSE endpoint advertised to clients (`/api/events` by default). */ + eventsUrl: string + /** Host the server is bound to (e.g., 127.0.0.1 or 0.0.0.0). */ + host: string + /** Listening mode derived from host binding. */ + listeningMode: "local" | "all" + /** Actual port in use after binding. */ + port: number + /** Display label for the host (e.g., hostname or friendly name). */ + hostLabel: string + /** Absolute path of the filesystem root exposed to clients. */ + workspaceRoot: string + /** Reachable addresses for this server, external first. */ + addresses: NetworkAddress[] + /** Optional metadata about the most recent public release. */ + latestRelease?: LatestReleaseInfo +} + +export type { + Preferences, + ModelPreference, + AgentModelSelections, + RecentFolder, + OpenCodeBinary, +} diff --git a/packages/server/src/bin.ts b/packages/server/src/bin.ts new file mode 100644 index 0000000..9ec4fbc --- /dev/null +++ b/packages/server/src/bin.ts @@ -0,0 +1,29 @@ +#!/usr/bin/env node + +import { spawn } from "child_process" +import path from "path" +import { fileURLToPath, pathToFileURL } from "url" + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) +const cliEntry = path.join(__dirname, "index.js") +const loaderFileUrl = pathToFileURL(path.join(__dirname, "loader.js")).href +const registerScript = `import { register } from "node:module"; import { pathToFileURL } from "node:url"; register("${encodeURI(loaderFileUrl)}", pathToFileURL("./"));` +const loaderArg = `data:text/javascript,${registerScript}` + +const child = spawn(process.execPath, ["--import", loaderArg, cliEntry, ...process.argv.slice(2)], { + stdio: "inherit", +}) + +child.on("exit", (code, signal) => { + if (signal) { + process.kill(process.pid, signal) + return + } + process.exit(code ?? 0) +}) + +child.on("error", (error) => { + console.error("Failed to launch CLI runtime", error) + process.exit(1) +}) diff --git a/packages/server/src/config/binaries.ts b/packages/server/src/config/binaries.ts new file mode 100644 index 0000000..7b3d4f5 --- /dev/null +++ b/packages/server/src/config/binaries.ts @@ -0,0 +1,156 @@ +import { + BinaryCreateRequest, + BinaryRecord, + BinaryUpdateRequest, + BinaryValidationResult, +} from "../api-types" +import { ConfigStore } from "./store" +import { EventBus } from "../events/bus" +import type { ConfigFile } from "./schema" +import { Logger } from "../logger" + +export class BinaryRegistry { + constructor( + private readonly configStore: ConfigStore, + private readonly eventBus: EventBus | undefined, + private readonly logger: Logger, + ) {} + + list(): BinaryRecord[] { + return this.mapRecords() + } + + resolveDefault(): BinaryRecord { + const binaries = this.mapRecords() + if (binaries.length === 0) { + this.logger.warn("No configured binaries found, falling back to opencode") + return this.buildFallbackRecord("opencode") + } + return binaries.find((binary) => binary.isDefault) ?? binaries[0] + } + + create(request: BinaryCreateRequest): BinaryRecord { + this.logger.debug({ path: request.path }, "Registering OpenCode binary") + const entry = { + path: request.path, + version: undefined, + lastUsed: Date.now(), + label: request.label, + } + + const config = this.configStore.get() + const nextConfig = this.cloneConfig(config) + const deduped = nextConfig.opencodeBinaries.filter((binary) => binary.path !== request.path) + nextConfig.opencodeBinaries = [entry, ...deduped] + + if (request.makeDefault) { + nextConfig.preferences.lastUsedBinary = request.path + } + + this.configStore.replace(nextConfig) + const record = this.getById(request.path) + this.emitChange() + return record + } + + update(id: string, updates: BinaryUpdateRequest): BinaryRecord { + this.logger.debug({ id }, "Updating OpenCode binary") + const config = this.configStore.get() + const nextConfig = this.cloneConfig(config) + nextConfig.opencodeBinaries = nextConfig.opencodeBinaries.map((binary) => + binary.path === id ? { ...binary, label: updates.label ?? binary.label } : binary, + ) + + if (updates.makeDefault) { + nextConfig.preferences.lastUsedBinary = id + } + + this.configStore.replace(nextConfig) + const record = this.getById(id) + this.emitChange() + return record + } + + remove(id: string) { + this.logger.debug({ id }, "Removing OpenCode binary") + const config = this.configStore.get() + const nextConfig = this.cloneConfig(config) + const remaining = nextConfig.opencodeBinaries.filter((binary) => binary.path !== id) + nextConfig.opencodeBinaries = remaining + + if (nextConfig.preferences.lastUsedBinary === id) { + nextConfig.preferences.lastUsedBinary = remaining[0]?.path + } + + this.configStore.replace(nextConfig) + this.emitChange() + } + + validatePath(path: string): BinaryValidationResult { + this.logger.debug({ path }, "Validating OpenCode binary path") + return this.validateRecord({ + id: path, + path, + label: this.prettyLabel(path), + isDefault: false, + }) + } + + private cloneConfig(config: ConfigFile): ConfigFile { + return JSON.parse(JSON.stringify(config)) as ConfigFile + } + + private mapRecords(): BinaryRecord[] { + + const config = this.configStore.get() + const configuredBinaries = config.opencodeBinaries.map((binary) => ({ + id: binary.path, + path: binary.path, + label: binary.label ?? this.prettyLabel(binary.path), + version: binary.version, + isDefault: false, + })) + + const defaultPath = config.preferences.lastUsedBinary ?? configuredBinaries[0]?.path ?? "opencode" + + const annotated = configuredBinaries.map((binary) => ({ + ...binary, + isDefault: binary.path === defaultPath, + })) + + if (!annotated.some((binary) => binary.path === defaultPath)) { + annotated.unshift(this.buildFallbackRecord(defaultPath)) + } + + return annotated + } + + private getById(id: string): BinaryRecord { + return this.mapRecords().find((binary) => binary.id === id) ?? this.buildFallbackRecord(id) + } + + private emitChange() { + this.logger.debug("Emitting binaries changed event") + this.eventBus?.publish({ type: "config.binariesChanged", binaries: this.mapRecords() }) + } + + private validateRecord(record: BinaryRecord): BinaryValidationResult { + // TODO: call actual binary -v check. + return { valid: true, version: record.version } + } + + private buildFallbackRecord(path: string): BinaryRecord { + return { + id: path, + path, + label: this.prettyLabel(path), + isDefault: true, + } + } + + private prettyLabel(path: string) { + const parts = path.split(/[\\/]/) + const last = parts[parts.length - 1] || path + return last || path + } +} diff --git a/packages/server/src/config/schema.ts b/packages/server/src/config/schema.ts new file mode 100644 index 0000000..09dadf6 --- /dev/null +++ b/packages/server/src/config/schema.ts @@ -0,0 +1,64 @@ +import { z } from "zod" + +const ModelPreferenceSchema = z.object({ + providerId: z.string(), + modelId: z.string(), +}) + +const AgentModelSelectionSchema = z.record(z.string(), ModelPreferenceSchema) +const AgentModelSelectionsSchema = z.record(z.string(), AgentModelSelectionSchema) + +const PreferencesSchema = z.object({ + showThinkingBlocks: z.boolean().default(false), + thinkingBlocksExpansion: z.enum(["expanded", "collapsed"]).default("expanded"), + showTimelineTools: z.boolean().default(true), + lastUsedBinary: z.string().optional(), + environmentVariables: z.record(z.string()).default({}), + modelRecents: z.array(ModelPreferenceSchema).default([]), + diffViewMode: z.enum(["split", "unified"]).default("split"), + toolOutputExpansion: z.enum(["expanded", "collapsed"]).default("expanded"), + diagnosticsExpansion: z.enum(["expanded", "collapsed"]).default("expanded"), + showUsageMetrics: z.boolean().default(true), + autoCleanupBlankSessions: z.boolean().default(true), + listeningMode: z.enum(["local", "all"]).default("local"), +}) + +const RecentFolderSchema = z.object({ + path: z.string(), + lastAccessed: z.number().nonnegative(), +}) + +const OpenCodeBinarySchema = z.object({ + path: z.string(), + version: z.string().optional(), + lastUsed: z.number().nonnegative(), + label: z.string().optional(), +}) + +const ConfigFileSchema = z.object({ + preferences: PreferencesSchema.default({}), + recentFolders: z.array(RecentFolderSchema).default([]), + opencodeBinaries: z.array(OpenCodeBinarySchema).default([]), + theme: z.enum(["light", "dark", "system"]).optional(), +}) + +const DEFAULT_CONFIG = ConfigFileSchema.parse({}) + +export { + ModelPreferenceSchema, + AgentModelSelectionSchema, + AgentModelSelectionsSchema, + PreferencesSchema, + RecentFolderSchema, + OpenCodeBinarySchema, + ConfigFileSchema, + DEFAULT_CONFIG, +} + +export type ModelPreference = z.infer +export type AgentModelSelection = z.infer +export type AgentModelSelections = z.infer +export type Preferences = z.infer +export type RecentFolder = z.infer +export type OpenCodeBinary = z.infer +export type ConfigFile = z.infer diff --git a/packages/server/src/config/store.ts b/packages/server/src/config/store.ts new file mode 100644 index 0000000..dda49e4 --- /dev/null +++ b/packages/server/src/config/store.ts @@ -0,0 +1,78 @@ +import fs from "fs" +import path from "path" +import { EventBus } from "../events/bus" +import { Logger } from "../logger" +import { ConfigFile, ConfigFileSchema, DEFAULT_CONFIG } from "./schema" + +export class ConfigStore { + private cache: ConfigFile = DEFAULT_CONFIG + private loaded = false + + constructor( + private readonly configPath: string, + private readonly eventBus: EventBus | undefined, + private readonly logger: Logger, + ) {} + + load(): ConfigFile { + if (this.loaded) { + return this.cache + } + + try { + const resolved = this.resolvePath(this.configPath) + if (fs.existsSync(resolved)) { + const content = fs.readFileSync(resolved, "utf-8") + const parsed = JSON.parse(content) + this.cache = ConfigFileSchema.parse(parsed) + this.logger.debug({ resolved }, "Loaded existing config file") + } else { + this.cache = DEFAULT_CONFIG + this.logger.debug({ resolved }, "No config file found, using defaults") + } + } catch (error) { + this.logger.warn({ err: error }, "Failed to load config, using defaults") + this.cache = DEFAULT_CONFIG + } + + this.loaded = true + return this.cache + } + + get(): ConfigFile { + return this.load() + } + + replace(config: ConfigFile) { + const validated = ConfigFileSchema.parse(config) + this.commit(validated) + } + + private commit(next: ConfigFile) { + this.cache = next + this.loaded = true + this.persist() + const published = Boolean(this.eventBus) + this.eventBus?.publish({ type: "config.appChanged", config: this.cache }) + this.logger.debug({ broadcast: published }, "Config SSE event emitted") + this.logger.trace({ config: this.cache }, "Config payload") + } + + private persist() { + try { + const resolved = this.resolvePath(this.configPath) + fs.mkdirSync(path.dirname(resolved), { recursive: true }) + fs.writeFileSync(resolved, JSON.stringify(this.cache, null, 2), "utf-8") + this.logger.debug({ resolved }, "Persisted config file") + } catch (error) { + this.logger.warn({ err: error }, "Failed to persist config") + } + } + + private resolvePath(filePath: string) { + if (filePath.startsWith("~/")) { + return path.join(process.env.HOME ?? "", filePath.slice(2)) + } + return path.resolve(filePath) + } +} diff --git a/packages/server/src/events/bus.ts b/packages/server/src/events/bus.ts new file mode 100644 index 0000000..3d417ce --- /dev/null +++ b/packages/server/src/events/bus.ts @@ -0,0 +1,47 @@ +import { EventEmitter } from "events" +import { WorkspaceEventPayload } from "../api-types" +import { Logger } from "../logger" + +export class EventBus extends EventEmitter { + constructor(private readonly logger?: Logger) { + super() + } + + publish(event: WorkspaceEventPayload): boolean { + if (event.type !== "instance.event" && event.type !== "instance.eventStatus") { + this.logger?.debug({ type: event.type }, "Publishing workspace event") + if (this.logger?.isLevelEnabled("trace")) { + this.logger.trace({ event }, "Workspace event payload") + } + } + return super.emit(event.type, event) + } + + onEvent(listener: (event: WorkspaceEventPayload) => void) { + const handler = (event: WorkspaceEventPayload) => listener(event) + this.on("workspace.created", handler) + this.on("workspace.started", handler) + this.on("workspace.error", handler) + this.on("workspace.stopped", handler) + this.on("workspace.log", handler) + this.on("config.appChanged", handler) + this.on("config.binariesChanged", handler) + this.on("instance.dataChanged", handler) + this.on("instance.event", handler) + this.on("instance.eventStatus", handler) + this.on("app.releaseAvailable", handler) + return () => { + this.off("workspace.created", handler) + this.off("workspace.started", handler) + this.off("workspace.error", handler) + this.off("workspace.stopped", handler) + this.off("workspace.log", handler) + this.off("config.appChanged", handler) + this.off("config.binariesChanged", handler) + this.off("instance.dataChanged", handler) + this.off("instance.event", handler) + this.off("instance.eventStatus", handler) + this.off("app.releaseAvailable", handler) + } + } +} diff --git a/packages/server/src/filesystem/__tests__/search-cache.test.ts b/packages/server/src/filesystem/__tests__/search-cache.test.ts new file mode 100644 index 0000000..f1facf9 --- /dev/null +++ b/packages/server/src/filesystem/__tests__/search-cache.test.ts @@ -0,0 +1,61 @@ +import assert from "node:assert/strict" +import { beforeEach, describe, it } from "node:test" +import type { FileSystemEntry } from "../../api-types" +import { + clearWorkspaceSearchCache, + getWorkspaceCandidates, + refreshWorkspaceCandidates, + WORKSPACE_CANDIDATE_CACHE_TTL_MS, +} from "../search-cache" + +describe("workspace search cache", () => { + beforeEach(() => { + clearWorkspaceSearchCache() + }) + + it("expires cached candidates after the TTL", () => { + const workspacePath = "/tmp/workspace" + const startTime = 1_000 + + refreshWorkspaceCandidates(workspacePath, () => [createEntry("file-a")], startTime) + + const beforeExpiry = getWorkspaceCandidates( + workspacePath, + startTime + WORKSPACE_CANDIDATE_CACHE_TTL_MS - 1, + ) + assert.ok(beforeExpiry) + assert.equal(beforeExpiry.length, 1) + assert.equal(beforeExpiry[0].name, "file-a") + + const afterExpiry = getWorkspaceCandidates( + workspacePath, + startTime + WORKSPACE_CANDIDATE_CACHE_TTL_MS + 1, + ) + assert.equal(afterExpiry, undefined) + }) + + it("replaces cached entries when manually refreshed", () => { + const workspacePath = "/tmp/workspace" + + refreshWorkspaceCandidates(workspacePath, () => [createEntry("file-a")], 5_000) + const initial = getWorkspaceCandidates(workspacePath) + assert.ok(initial) + assert.equal(initial[0].name, "file-a") + + refreshWorkspaceCandidates(workspacePath, () => [createEntry("file-b")], 6_000) + const refreshed = getWorkspaceCandidates(workspacePath) + assert.ok(refreshed) + assert.equal(refreshed[0].name, "file-b") + }) +}) + +function createEntry(name: string): FileSystemEntry { + return { + name, + path: name, + absolutePath: `/tmp/${name}`, + type: "file", + size: 1, + modifiedAt: new Date().toISOString(), + } +} diff --git a/packages/server/src/filesystem/browser.ts b/packages/server/src/filesystem/browser.ts new file mode 100644 index 0000000..29ddb1c --- /dev/null +++ b/packages/server/src/filesystem/browser.ts @@ -0,0 +1,295 @@ +import fs from "fs" +import os from "os" +import path from "path" +import { + FileSystemEntry, + FileSystemListResponse, + FileSystemListingMetadata, + WINDOWS_DRIVES_ROOT, +} from "../api-types" + +interface FileSystemBrowserOptions { + rootDir: string + unrestricted?: boolean +} + +interface DirectoryReadOptions { + includeFiles: boolean + formatPath: (entryName: string) => string + formatAbsolutePath: (entryName: string) => string +} + +const WINDOWS_DRIVE_LETTERS = Array.from({ length: 26 }, (_, i) => String.fromCharCode(65 + i)) + +export class FileSystemBrowser { + private readonly root: string + private readonly unrestricted: boolean + private readonly homeDir: string + private readonly isWindows: boolean + + constructor(options: FileSystemBrowserOptions) { + this.root = path.resolve(options.rootDir) + this.unrestricted = Boolean(options.unrestricted) + this.homeDir = os.homedir() + this.isWindows = process.platform === "win32" + } + + list(relativePath = ".", options: { includeFiles?: boolean } = {}): FileSystemEntry[] { + if (this.unrestricted) { + throw new Error("Relative listing is unavailable when running with unrestricted root") + } + const includeFiles = options.includeFiles ?? true + const normalizedPath = this.normalizeRelativePath(relativePath) + const absolutePath = this.toRestrictedAbsolute(normalizedPath) + return this.readDirectoryEntries(absolutePath, { + includeFiles, + formatPath: (entryName) => this.buildRelativePath(normalizedPath, entryName), + formatAbsolutePath: (entryName) => this.resolveRestrictedAbsoluteChild(normalizedPath, entryName), + }) + } + + browse(targetPath?: string, options: { includeFiles?: boolean } = {}): FileSystemListResponse { + const includeFiles = options.includeFiles ?? true + if (this.unrestricted) { + return this.listUnrestricted(targetPath, includeFiles) + } + return this.listRestrictedWithMetadata(targetPath, includeFiles) + } + + readFile(relativePath: string): string { + if (this.unrestricted) { + throw new Error("readFile is not available in unrestricted mode") + } + const resolved = this.toRestrictedAbsolute(relativePath) + return fs.readFileSync(resolved, "utf-8") + } + + private listRestrictedWithMetadata(relativePath: string | undefined, includeFiles: boolean): FileSystemListResponse { + const normalizedPath = this.normalizeRelativePath(relativePath) + const absolutePath = this.toRestrictedAbsolute(normalizedPath) + const entries = this.readDirectoryEntries(absolutePath, { + includeFiles, + formatPath: (entryName) => this.buildRelativePath(normalizedPath, entryName), + formatAbsolutePath: (entryName) => this.resolveRestrictedAbsoluteChild(normalizedPath, entryName), + }) + + const metadata: FileSystemListingMetadata = { + scope: "restricted", + currentPath: normalizedPath, + parentPath: normalizedPath === "." ? undefined : this.getRestrictedParent(normalizedPath), + rootPath: this.root, + homePath: this.homeDir, + displayPath: this.resolveRestrictedAbsolute(normalizedPath), + pathKind: "relative", + } + + return { entries, metadata } + } + + private listUnrestricted(targetPath: string | undefined, includeFiles: boolean): FileSystemListResponse { + const resolvedPath = this.resolveUnrestrictedPath(targetPath) + + if (this.isWindows && resolvedPath === WINDOWS_DRIVES_ROOT) { + return this.listWindowsDrives() + } + + const entries = this.readDirectoryEntries(resolvedPath, { + includeFiles, + formatPath: (entryName) => this.resolveAbsoluteChild(resolvedPath, entryName), + formatAbsolutePath: (entryName) => this.resolveAbsoluteChild(resolvedPath, entryName), + }) + + const parentPath = this.getUnrestrictedParent(resolvedPath) + + const metadata: FileSystemListingMetadata = { + scope: "unrestricted", + currentPath: resolvedPath, + parentPath, + rootPath: this.homeDir, + homePath: this.homeDir, + displayPath: resolvedPath, + pathKind: "absolute", + } + + return { entries, metadata } + } + + private listWindowsDrives(): FileSystemListResponse { + if (!this.isWindows) { + throw new Error("Drive listing is only supported on Windows hosts") + } + + const entries: FileSystemEntry[] = [] + for (const letter of WINDOWS_DRIVE_LETTERS) { + const drivePath = `${letter}:\\` + try { + if (fs.existsSync(drivePath)) { + entries.push({ + name: `${letter}:`, + path: drivePath, + absolutePath: drivePath, + type: "directory", + }) + } + } catch { + // Ignore inaccessible drives + } + } + + // Provide a generic UNC root entry so users can navigate to network shares manually. + entries.push({ + name: "UNC Network", + path: "\\\\", + absolutePath: "\\\\", + type: "directory", + }) + + const metadata: FileSystemListingMetadata = { + scope: "unrestricted", + currentPath: WINDOWS_DRIVES_ROOT, + parentPath: undefined, + rootPath: this.homeDir, + homePath: this.homeDir, + displayPath: "Drives", + pathKind: "drives", + } + + return { entries, metadata } + } + + private readDirectoryEntries(directory: string, options: DirectoryReadOptions): FileSystemEntry[] { + const dirents = fs.readdirSync(directory, { withFileTypes: true }) + const results: FileSystemEntry[] = [] + + for (const entry of dirents) { + if (!options.includeFiles && !entry.isDirectory()) { + continue + } + + const absoluteEntryPath = path.join(directory, entry.name) + let stats: fs.Stats + try { + stats = fs.statSync(absoluteEntryPath) + } catch { + // Skip entries we cannot stat (insufficient permissions, etc.) + continue + } + + const isDirectory = entry.isDirectory() + if (!options.includeFiles && !isDirectory) { + continue + } + + results.push({ + name: entry.name, + path: options.formatPath(entry.name), + absolutePath: options.formatAbsolutePath(entry.name), + type: isDirectory ? "directory" : "file", + size: isDirectory ? undefined : stats.size, + modifiedAt: stats.mtime.toISOString(), + }) + } + + return results.sort((a, b) => a.name.localeCompare(b.name)) + } + + private normalizeRelativePath(input: string | undefined) { + if (!input || input === "." || input === "./" || input === "/") { + return "." + } + let normalized = input.replace(/\\+/g, "/") + if (normalized.startsWith("./")) { + normalized = normalized.replace(/^\.\/+/, "") + } + if (normalized.startsWith("/")) { + normalized = normalized.replace(/^\/+/g, "") + } + return normalized === "" ? "." : normalized + } + + private buildRelativePath(parent: string, child: string) { + if (!parent || parent === ".") { + return this.normalizeRelativePath(child) + } + return this.normalizeRelativePath(`${parent}/${child}`) + } + + private resolveRestrictedAbsolute(relativePath: string) { + return this.toRestrictedAbsolute(relativePath) + } + + private resolveRestrictedAbsoluteChild(parent: string, child: string) { + const normalized = this.buildRelativePath(parent, child) + return this.toRestrictedAbsolute(normalized) + } + + private toRestrictedAbsolute(relativePath: string) { + const normalized = this.normalizeRelativePath(relativePath) + const target = path.resolve(this.root, normalized) + const relativeToRoot = path.relative(this.root, target) + if (relativeToRoot.startsWith("..") || path.isAbsolute(relativeToRoot) && relativeToRoot !== "") { + throw new Error("Access outside of root is not allowed") + } + return target + } + + private resolveUnrestrictedPath(input: string | undefined): string { + if (!input || input === "." || input === "./") { + return this.homeDir + } + + if (this.isWindows) { + if (input === WINDOWS_DRIVES_ROOT) { + return WINDOWS_DRIVES_ROOT + } + const normalized = path.win32.normalize(input) + if (/^[a-zA-Z]:/.test(normalized) || normalized.startsWith("\\\\")) { + return normalized + } + return path.win32.resolve(this.homeDir, normalized) + } + + if (input.startsWith("/")) { + return path.posix.normalize(input) + } + + return path.posix.resolve(this.homeDir, input) + } + + private resolveAbsoluteChild(parent: string, child: string) { + if (this.isWindows) { + return path.win32.normalize(path.win32.join(parent, child)) + } + return path.posix.normalize(path.posix.join(parent, child)) + } + + private getRestrictedParent(relativePath: string) { + const normalized = this.normalizeRelativePath(relativePath) + if (normalized === ".") { + return undefined + } + const segments = normalized.split("/") + segments.pop() + return segments.length === 0 ? "." : segments.join("/") + } + + private getUnrestrictedParent(currentPath: string) { + if (this.isWindows) { + const normalized = path.win32.normalize(currentPath) + const parsed = path.win32.parse(normalized) + if (normalized === WINDOWS_DRIVES_ROOT) { + return undefined + } + if (normalized === parsed.root) { + return WINDOWS_DRIVES_ROOT + } + return path.win32.dirname(normalized) + } + + const normalized = path.posix.normalize(currentPath) + if (normalized === "/") { + return undefined + } + return path.posix.dirname(normalized) + } +} diff --git a/packages/server/src/filesystem/search-cache.ts b/packages/server/src/filesystem/search-cache.ts new file mode 100644 index 0000000..5568204 --- /dev/null +++ b/packages/server/src/filesystem/search-cache.ts @@ -0,0 +1,66 @@ +import path from "path" +import type { FileSystemEntry } from "../api-types" + +export const WORKSPACE_CANDIDATE_CACHE_TTL_MS = 30_000 + +interface WorkspaceCandidateCacheEntry { + expiresAt: number + candidates: FileSystemEntry[] +} + +const workspaceCandidateCache = new Map() + +export function getWorkspaceCandidates(rootDir: string, now = Date.now()): FileSystemEntry[] | undefined { + const key = normalizeKey(rootDir) + const cached = workspaceCandidateCache.get(key) + if (!cached) { + return undefined + } + + if (cached.expiresAt <= now) { + workspaceCandidateCache.delete(key) + return undefined + } + + return cloneEntries(cached.candidates) +} + +export function refreshWorkspaceCandidates( + rootDir: string, + builder: () => FileSystemEntry[], + now = Date.now(), +): FileSystemEntry[] { + const key = normalizeKey(rootDir) + const freshCandidates = builder() + + if (!freshCandidates || freshCandidates.length === 0) { + workspaceCandidateCache.delete(key) + return [] + } + + const storedCandidates = cloneEntries(freshCandidates) + workspaceCandidateCache.set(key, { + expiresAt: now + WORKSPACE_CANDIDATE_CACHE_TTL_MS, + candidates: storedCandidates, + }) + + return cloneEntries(storedCandidates) +} + +export function clearWorkspaceSearchCache(rootDir?: string) { + if (typeof rootDir === "undefined") { + workspaceCandidateCache.clear() + return + } + + const key = normalizeKey(rootDir) + workspaceCandidateCache.delete(key) +} + +function cloneEntries(entries: FileSystemEntry[]): FileSystemEntry[] { + return entries.map((entry) => ({ ...entry })) +} + +function normalizeKey(rootDir: string) { + return path.resolve(rootDir) +} diff --git a/packages/server/src/filesystem/search.ts b/packages/server/src/filesystem/search.ts new file mode 100644 index 0000000..77347b0 --- /dev/null +++ b/packages/server/src/filesystem/search.ts @@ -0,0 +1,184 @@ +import fs from "fs" +import path from "path" +import fuzzysort from "fuzzysort" +import type { FileSystemEntry } from "../api-types" +import { clearWorkspaceSearchCache, getWorkspaceCandidates, refreshWorkspaceCandidates } from "./search-cache" + +const DEFAULT_LIMIT = 100 +const MAX_LIMIT = 200 +const MAX_CANDIDATES = 8000 +const IGNORED_DIRECTORIES = new Set( + [".git", ".hg", ".svn", "node_modules", "dist", "build", ".next", ".nuxt", ".turbo", ".cache", "coverage"].map( + (name) => name.toLowerCase(), + ), +) + +export type WorkspaceFileSearchType = "all" | "file" | "directory" + +export interface WorkspaceFileSearchOptions { + limit?: number + type?: WorkspaceFileSearchType + refresh?: boolean +} + +interface CandidateEntry { + entry: FileSystemEntry + key: string +} + +export function searchWorkspaceFiles( + rootDir: string, + query: string, + options: WorkspaceFileSearchOptions = {}, +): FileSystemEntry[] { + const trimmedQuery = query.trim() + if (!trimmedQuery) { + throw new Error("Search query is required") + } + + const normalizedRoot = path.resolve(rootDir) + const limit = normalizeLimit(options.limit) + const typeFilter: WorkspaceFileSearchType = options.type ?? "all" + const refreshRequested = options.refresh === true + + let entries: FileSystemEntry[] | undefined + + try { + if (!refreshRequested) { + entries = getWorkspaceCandidates(normalizedRoot) + } + + if (!entries) { + entries = refreshWorkspaceCandidates(normalizedRoot, () => collectCandidates(normalizedRoot)) + } + } catch (error) { + clearWorkspaceSearchCache(normalizedRoot) + throw error + } + + if (!entries || entries.length === 0) { + clearWorkspaceSearchCache(normalizedRoot) + return [] + } + + const candidates = buildCandidateEntries(entries, typeFilter) + + if (candidates.length === 0) { + return [] + } + + const matches = fuzzysort.go(trimmedQuery, candidates, { + key: "key", + limit, + }) + + if (!matches || matches.length === 0) { + return [] + } + + return matches.map((match) => match.obj.entry) +} + + +function collectCandidates(rootDir: string): FileSystemEntry[] { + const queue: string[] = [""] + const entries: FileSystemEntry[] = [] + + while (queue.length > 0 && entries.length < MAX_CANDIDATES) { + const relativeDir = queue.pop() || "" + const absoluteDir = relativeDir ? path.join(rootDir, relativeDir) : rootDir + + let dirents: fs.Dirent[] + try { + dirents = fs.readdirSync(absoluteDir, { withFileTypes: true }) + } catch { + continue + } + + for (const dirent of dirents) { + const entryName = dirent.name + const lowerName = entryName.toLowerCase() + const relativePath = relativeDir ? `${relativeDir}/${entryName}` : entryName + const absolutePath = path.join(absoluteDir, entryName) + + if (dirent.isDirectory() && IGNORED_DIRECTORIES.has(lowerName)) { + continue + } + + let stats: fs.Stats + try { + stats = fs.statSync(absolutePath) + } catch { + continue + } + + const isDirectory = stats.isDirectory() + + if (isDirectory && !IGNORED_DIRECTORIES.has(lowerName)) { + if (entries.length < MAX_CANDIDATES) { + queue.push(relativePath) + } + } + + const entryType: FileSystemEntry["type"] = isDirectory ? "directory" : "file" + const normalizedPath = normalizeRelativeEntryPath(relativePath) + const entry: FileSystemEntry = { + name: entryName, + path: normalizedPath, + absolutePath: path.resolve(rootDir, normalizedPath === "." ? "" : normalizedPath), + type: entryType, + size: entryType === "file" ? stats.size : undefined, + modifiedAt: stats.mtime.toISOString(), + } + + entries.push(entry) + + if (entries.length >= MAX_CANDIDATES) { + break + } + } + } + + return entries +} + +function buildCandidateEntries(entries: FileSystemEntry[], filter: WorkspaceFileSearchType): CandidateEntry[] { + const filtered: CandidateEntry[] = [] + for (const entry of entries) { + if (!shouldInclude(entry.type, filter)) { + continue + } + filtered.push({ entry, key: buildSearchKey(entry) }) + } + return filtered +} + +function normalizeLimit(limit?: number) { + if (!limit || Number.isNaN(limit)) { + return DEFAULT_LIMIT + } + const clamped = Math.min(Math.max(limit, 1), MAX_LIMIT) + return clamped +} + +function shouldInclude(entryType: FileSystemEntry["type"], filter: WorkspaceFileSearchType) { + return filter === "all" || entryType === filter +} + +function normalizeRelativeEntryPath(relativePath: string): string { + if (!relativePath) { + return "." + } + let normalized = relativePath.replace(/\\+/g, "/") + if (normalized.startsWith("./")) { + normalized = normalized.replace(/^\.\/+/, "") + } + if (normalized.startsWith("/")) { + normalized = normalized.replace(/^\/+/g, "") + } + return normalized || "." +} + +function buildSearchKey(entry: FileSystemEntry) { + return entry.path.toLowerCase() +} diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts new file mode 100644 index 0000000..8144faa --- /dev/null +++ b/packages/server/src/index.ts @@ -0,0 +1,227 @@ +/** + * CLI entry point. + * For now this only wires the typed modules together; actual command handling comes later. + */ +import { Command, InvalidArgumentError, Option } from "commander" +import path from "path" +import { fileURLToPath } from "url" +import { createRequire } from "module" +import { createHttpServer } from "./server/http-server" +import { WorkspaceManager } from "./workspaces/manager" +import { ConfigStore } from "./config/store" +import { BinaryRegistry } from "./config/binaries" +import { FileSystemBrowser } from "./filesystem/browser" +import { EventBus } from "./events/bus" +import { ServerMeta } from "./api-types" +import { InstanceStore } from "./storage/instance-store" +import { InstanceEventBridge } from "./workspaces/instance-events" +import { createLogger } from "./logger" +import { launchInBrowser } from "./launcher" +import { startReleaseMonitor } from "./releases/release-monitor" + +const require = createRequire(import.meta.url) + +const packageJson = require("../package.json") as { version: string } +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) +const DEFAULT_UI_STATIC_DIR = path.resolve(__dirname, "../public") + +interface CliOptions { + port: number + host: string + rootDir: string + configPath: string + unrestrictedRoot: boolean + logLevel?: string + logDestination?: string + uiStaticDir: string + uiDevServer?: string + launch: boolean +} + +const DEFAULT_PORT = 9898 +const DEFAULT_HOST = "127.0.0.1" +const DEFAULT_CONFIG_PATH = "~/.config/codenomad/config.json" + +function parseCliOptions(argv: string[]): CliOptions { + const program = new Command() + .name("codenomad") + .description("CodeNomad CLI server") + .version(packageJson.version, "-v, --version", "Show the CLI version") + .addOption(new Option("--host ", "Host interface to bind").env("CLI_HOST").default(DEFAULT_HOST)) + .addOption(new Option("--port ", "Port for the HTTP server").env("CLI_PORT").default(DEFAULT_PORT).argParser(parsePort)) + .addOption( + new Option("--workspace-root ", "Workspace root directory").env("CLI_WORKSPACE_ROOT").default(process.cwd()), + ) + .addOption(new Option("--root ").env("CLI_ROOT").hideHelp(true)) + .addOption(new Option("--unrestricted-root", "Allow browsing the full filesystem").env("CLI_UNRESTRICTED_ROOT").default(false)) + .addOption(new Option("--config ", "Path to the config file").env("CLI_CONFIG").default(DEFAULT_CONFIG_PATH)) + .addOption(new Option("--log-level ", "Log level (trace|debug|info|warn|error)").env("CLI_LOG_LEVEL")) + .addOption(new Option("--log-destination ", "Log destination file (defaults to stdout)").env("CLI_LOG_DESTINATION")) + .addOption( + new Option("--ui-dir ", "Directory containing the built UI bundle").env("CLI_UI_DIR").default(DEFAULT_UI_STATIC_DIR), + ) + .addOption(new Option("--ui-dev-server ", "Proxy UI requests to a running dev server").env("CLI_UI_DEV_SERVER")) + .addOption(new Option("--launch", "Launch the UI in a browser after start").env("CLI_LAUNCH").default(false)) + + program.parse(argv, { from: "user" }) + const parsed = program.opts<{ + host: string + port: number + workspaceRoot?: string + root?: string + unrestrictedRoot?: boolean + config: string + logLevel?: string + logDestination?: string + uiDir: string + uiDevServer?: string + launch?: boolean + }>() + + const resolvedRoot = parsed.workspaceRoot ?? parsed.root ?? process.cwd() + + const normalizedHost = resolveHost(parsed.host) + + return { + port: parsed.port, + host: normalizedHost, + rootDir: resolvedRoot, + configPath: parsed.config, + unrestrictedRoot: Boolean(parsed.unrestrictedRoot), + logLevel: parsed.logLevel, + logDestination: parsed.logDestination, + uiStaticDir: parsed.uiDir, + uiDevServer: parsed.uiDevServer, + launch: Boolean(parsed.launch), + } +} + +function parsePort(input: string): number { + const value = Number(input) + if (!Number.isInteger(value) || value < 0 || value > 65535) { + throw new InvalidArgumentError("Port must be an integer between 0 and 65535") + } + return value +} + +function resolveHost(input: string | undefined): string { + if (input && input.trim() === "0.0.0.0") { + return "0.0.0.0" + } + return DEFAULT_HOST +} + +async function main() { + const options = parseCliOptions(process.argv.slice(2)) + const logger = createLogger({ level: options.logLevel, destination: options.logDestination, component: "app" }) + const workspaceLogger = logger.child({ component: "workspace" }) + const configLogger = logger.child({ component: "config" }) + const eventLogger = logger.child({ component: "events" }) + + logger.info({ options }, "Starting CodeNomad CLI server") + + const eventBus = new EventBus(eventLogger) + const configStore = new ConfigStore(options.configPath, eventBus, configLogger) + const binaryRegistry = new BinaryRegistry(configStore, eventBus, configLogger) + const workspaceManager = new WorkspaceManager({ + rootDir: options.rootDir, + configStore, + binaryRegistry, + eventBus, + logger: workspaceLogger, + }) + const fileSystemBrowser = new FileSystemBrowser({ rootDir: options.rootDir, unrestricted: options.unrestrictedRoot }) + const instanceStore = new InstanceStore() + const instanceEventBridge = new InstanceEventBridge({ + workspaceManager, + eventBus, + logger: logger.child({ component: "instance-events" }), + }) + + const serverMeta: ServerMeta = { + httpBaseUrl: `http://${options.host}:${options.port}`, + eventsUrl: `/api/events`, + host: options.host, + listeningMode: options.host === "0.0.0.0" ? "all" : "local", + port: options.port, + hostLabel: options.host, + workspaceRoot: options.rootDir, + addresses: [], + } + + const releaseMonitor = startReleaseMonitor({ + currentVersion: packageJson.version, + logger: logger.child({ component: "release-monitor" }), + onUpdate: (release) => { + if (release) { + serverMeta.latestRelease = release + eventBus.publish({ type: "app.releaseAvailable", release }) + } else { + delete serverMeta.latestRelease + } + }, + }) + + const server = createHttpServer({ + host: options.host, + port: options.port, + workspaceManager, + configStore, + binaryRegistry, + fileSystemBrowser, + eventBus, + serverMeta, + instanceStore, + uiStaticDir: options.uiStaticDir, + uiDevServerUrl: options.uiDevServer, + logger, + }) + + const startInfo = await server.start() + logger.info({ port: startInfo.port, host: options.host }, "HTTP server listening") + console.log(`CodeNomad Server is ready at ${startInfo.url}`) + + if (options.launch) { + await launchInBrowser(startInfo.url, logger.child({ component: "launcher" })) + } + + let shuttingDown = false + + const shutdown = async () => { + if (shuttingDown) { + logger.info("Shutdown already in progress, ignoring signal") + return + } + shuttingDown = true + logger.info("Received shutdown signal, closing server") + try { + await server.stop() + logger.info("HTTP server stopped") + } catch (error) { + logger.error({ err: error }, "Failed to stop HTTP server") + } + + try { + instanceEventBridge.shutdown() + await workspaceManager.shutdown() + logger.info("Workspace manager shutdown complete") + } catch (error) { + logger.error({ err: error }, "Workspace manager shutdown failed") + } + + releaseMonitor.stop() + + logger.info("Exiting process") + process.exit(0) + } + + process.on("SIGINT", shutdown) + process.on("SIGTERM", shutdown) +} + +main().catch((error) => { + const logger = createLogger({ component: "app" }) + logger.error({ err: error }, "CLI server crashed") + process.exit(1) +}) diff --git a/packages/server/src/launcher.ts b/packages/server/src/launcher.ts new file mode 100644 index 0000000..d28f683 --- /dev/null +++ b/packages/server/src/launcher.ts @@ -0,0 +1,177 @@ +import { spawn } from "child_process" +import os from "os" +import path from "path" +import type { Logger } from "./logger" + +interface BrowserCandidate { + name: string + command: string + args: (url: string) => string[] +} + +const APP_ARGS = (url: string) => [`--app=${url}`, "--new-window"] + +export async function launchInBrowser(url: string, logger: Logger): Promise { + const { platform, candidates, manualExamples } = buildPlatformCandidates(url) + + console.log(`Attempting to launch browser (${platform}) using:`) + candidates.forEach((candidate) => console.log(` - ${candidate.name}: ${candidate.command}`)) + + for (const candidate of candidates) { + const success = await tryLaunch(candidate, url, logger) + if (success) { + return true + } + } + + console.error( + "No supported browser found to launch. Run without --launch and use one of the commands below or install a compatible browser.", + ) + if (manualExamples.length > 0) { + console.error("Manual launch commands:") + manualExamples.forEach((line) => console.error(` ${line}`)) + } + + return false +} + +async function tryLaunch(candidate: BrowserCandidate, url: string, logger: Logger): Promise { + return new Promise((resolve) => { + let resolved = false + try { + const args = candidate.args(url) + const child = spawn(candidate.command, args, { stdio: "ignore", detached: true }) + + child.once("error", (error) => { + if (resolved) return + resolved = true + logger.debug({ err: error, candidate: candidate.name, command: candidate.command, args }, "Browser launch failed") + resolve(false) + }) + + child.once("spawn", () => { + if (resolved) return + resolved = true + logger.info( + { + browser: candidate.name, + command: candidate.command, + args, + fullCommand: [candidate.command, ...args].join(" "), + }, + "Launched browser in app mode", + ) + child.unref() + resolve(true) + }) + } catch (error) { + if (resolved) return + resolved = true + logger.debug({ err: error, candidate: candidate.name, command: candidate.command }, "Browser spawn threw") + resolve(false) + } + }) +} + +function buildPlatformCandidates(url: string) { + switch (os.platform()) { + case "darwin": + return { + platform: "macOS", + candidates: buildMacCandidates(), + manualExamples: buildMacManualExamples(url), + } + case "win32": + return { + platform: "Windows", + candidates: buildWindowsCandidates(), + manualExamples: buildWindowsManualExamples(url), + } + default: + return { + platform: "Linux", + candidates: buildLinuxCandidates(), + manualExamples: buildLinuxManualExamples(url), + } + } +} + +function buildMacCandidates(): BrowserCandidate[] { + const apps = [ + { name: "Google Chrome", path: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" }, + { name: "Google Chrome Canary", path: "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary" }, + { name: "Microsoft Edge", path: "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge" }, + { name: "Brave Browser", path: "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser" }, + { name: "Chromium", path: "/Applications/Chromium.app/Contents/MacOS/Chromium" }, + { name: "Vivaldi", path: "/Applications/Vivaldi.app/Contents/MacOS/Vivaldi" }, + { name: "Arc", path: "/Applications/Arc.app/Contents/MacOS/Arc" }, + ] + + return apps.map((entry) => ({ name: entry.name, command: entry.path, args: APP_ARGS })) +} + +function buildWindowsCandidates(): BrowserCandidate[] { + const programFiles = process.env["ProgramFiles"] + const programFilesX86 = process.env["ProgramFiles(x86)"] + const localAppData = process.env["LocalAppData"] + + const paths = [ + [programFiles, "Google/Chrome/Application/chrome.exe", "Google Chrome"], + [programFilesX86, "Google/Chrome/Application/chrome.exe", "Google Chrome (x86)"], + [localAppData, "Google/Chrome/Application/chrome.exe", "Google Chrome (User)"], + [programFiles, "Microsoft/Edge/Application/msedge.exe", "Microsoft Edge"], + [programFilesX86, "Microsoft/Edge/Application/msedge.exe", "Microsoft Edge (x86)"], + [localAppData, "Microsoft/Edge/Application/msedge.exe", "Microsoft Edge (User)"], + [programFiles, "BraveSoftware/Brave-Browser/Application/brave.exe", "Brave"], + [localAppData, "BraveSoftware/Brave-Browser/Application/brave.exe", "Brave (User)"], + [programFiles, "Chromium/Application/chromium.exe", "Chromium"], + ] as const + + return paths + .filter(([root]) => Boolean(root)) + .map(([root, rel, name]) => ({ + name, + command: path.join(root as string, rel), + args: APP_ARGS, + })) +} + +function buildLinuxCandidates(): BrowserCandidate[] { + const names = [ + "google-chrome", + "google-chrome-stable", + "chromium", + "chromium-browser", + "brave-browser", + "microsoft-edge", + "microsoft-edge-stable", + "vivaldi", + ] + + return names.map((name) => ({ name, command: name, args: APP_ARGS })) +} + +function buildMacManualExamples(url: string) { + return [ + `"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --app="${url}" --new-window`, + `"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge" --app="${url}" --new-window`, + `"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser" --app="${url}" --new-window`, + ] +} + +function buildWindowsManualExamples(url: string) { + return [ + `"%ProgramFiles%\\Google\\Chrome\\Application\\chrome.exe" --app="${url}" --new-window`, + `"%ProgramFiles%\\Microsoft\\Edge\\Application\\msedge.exe" --app="${url}" --new-window`, + `"%ProgramFiles%\\BraveSoftware\\Brave-Browser\\Application\\brave.exe" --app="${url}" --new-window`, + ] +} + +function buildLinuxManualExamples(url: string) { + return [ + `google-chrome --app="${url}" --new-window`, + `chromium --app="${url}" --new-window`, + `brave-browser --app="${url}" --new-window`, + `microsoft-edge --app="${url}" --new-window`, + ] +} diff --git a/packages/server/src/loader.ts b/packages/server/src/loader.ts new file mode 100644 index 0000000..756d594 --- /dev/null +++ b/packages/server/src/loader.ts @@ -0,0 +1,21 @@ +export async function resolve(specifier: string, context: any, defaultResolve: any) { + try { + return await defaultResolve(specifier, context, defaultResolve) + } catch (error: any) { + if (shouldRetry(specifier, error)) { + const retried = specifier.endsWith(".js") ? specifier : `${specifier}.js` + return defaultResolve(retried, context, defaultResolve) + } + throw error + } +} + +function shouldRetry(specifier: string, error: any) { + if (!error || error.code !== "ERR_MODULE_NOT_FOUND") { + return false + } + if (specifier.startsWith("./") || specifier.startsWith("../")) { + return true + } + return false +} diff --git a/packages/server/src/logger.ts b/packages/server/src/logger.ts new file mode 100644 index 0000000..86a507e --- /dev/null +++ b/packages/server/src/logger.ts @@ -0,0 +1,133 @@ +import { Transform } from "node:stream" +import pino, { Logger as PinoLogger } from "pino" + +export type Logger = PinoLogger + +interface LoggerOptions { + level?: string + destination?: string + component?: string +} + +const LEVEL_LABELS: Record = { + 10: "trace", + 20: "debug", + 30: "info", + 40: "warn", + 50: "error", + 60: "fatal", +} + +const LIFECYCLE_COMPONENTS = new Set(["app", "workspace"]) +const OMITTED_FIELDS = new Set(["time", "msg", "level", "component", "module"]) + +export function createLogger(options: LoggerOptions = {}): Logger { + const level = (options.level ?? process.env.CLI_LOG_LEVEL ?? "info").toLowerCase() + const destination = options.destination ?? process.env.CLI_LOG_DESTINATION ?? "stdout" + const baseComponent = options.component ?? "app" + const loggerOptions = { + level, + base: { component: baseComponent }, + timestamp: false, + } as const + + if (destination && destination !== "stdout") { + const stream = pino.destination({ dest: destination, mkdir: true, sync: false }) + return pino(loggerOptions, stream) + } + + const lifecycleStream = new LifecycleLogStream({ restrictInfoToLifecycle: level === "info" }) + lifecycleStream.pipe(process.stdout) + return pino(loggerOptions, lifecycleStream) +} + +interface LifecycleStreamOptions { + restrictInfoToLifecycle: boolean +} + +class LifecycleLogStream extends Transform { + private buffer = "" + + constructor(private readonly options: LifecycleStreamOptions) { + super() + } + + _transform(chunk: Buffer, _encoding: BufferEncoding, callback: () => void) { + this.buffer += chunk.toString() + let newlineIndex = this.buffer.indexOf("\n") + while (newlineIndex >= 0) { + const line = this.buffer.slice(0, newlineIndex) + this.buffer = this.buffer.slice(newlineIndex + 1) + this.pushFormatted(line) + newlineIndex = this.buffer.indexOf("\n") + } + callback() + } + + _flush(callback: () => void) { + if (this.buffer.length > 0) { + this.pushFormatted(this.buffer) + this.buffer = "" + } + callback() + } + + private pushFormatted(line: string) { + if (!line.trim()) { + return + } + + let entry: Record + try { + entry = JSON.parse(line) + } catch { + return + } + + const levelNumber = typeof entry.level === "number" ? entry.level : 30 + const levelLabel = LEVEL_LABELS[levelNumber] ?? "info" + const component = (entry.component as string | undefined) ?? (entry.module as string | undefined) ?? "app" + + if (this.options.restrictInfoToLifecycle && levelNumber <= 30 && !LIFECYCLE_COMPONENTS.has(component)) { + return + } + + const message = typeof entry.msg === "string" ? entry.msg : "" + const metadata = this.formatMetadata(entry) + const formatted = metadata.length > 0 ? `[${levelLabel.toUpperCase()}] [${component}] ${message} ${metadata}` : `[${levelLabel.toUpperCase()}] [${component}] ${message}` + this.push(`${formatted}\n`) + } + + private formatMetadata(entry: Record): string { + const pairs: string[] = [] + for (const [key, value] of Object.entries(entry)) { + if (OMITTED_FIELDS.has(key)) { + continue + } + + if (key === "err" && value && typeof value === "object") { + const err = value as { type?: string; message?: string; stack?: string } + const errLabel = err.type ?? "Error" + const errMessage = err.message ? `: ${err.message}` : "" + pairs.push(`err=${errLabel}${errMessage}`) + if (err.stack) { + pairs.push(`stack="${err.stack}"`) + } + continue + } + + pairs.push(`${key}=${this.stringifyValue(value)}`) + } + + return pairs.join(" ").trim() + } + + private stringifyValue(value: unknown): string { + if (value === undefined) return "undefined" + if (value === null) return "null" + if (typeof value === "string") return value + if (typeof value === "number" || typeof value === "boolean") return String(value) + if (value instanceof Error) return value.message ?? value.name + return JSON.stringify(value) + } +} diff --git a/packages/server/src/opencode-config.ts b/packages/server/src/opencode-config.ts new file mode 100644 index 0000000..8b90651 --- /dev/null +++ b/packages/server/src/opencode-config.ts @@ -0,0 +1,36 @@ +import { cpSync, existsSync, mkdirSync, rmSync } from "fs" +import os from "os" +import path from "path" +import { fileURLToPath } from "url" +import { createLogger } from "./logger" + +const log = createLogger({ component: "opencode-config" }) +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) +const devTemplateDir = path.resolve(__dirname, "../../opencode-config") +const prodTemplateDir = path.resolve(__dirname, "opencode-config") + +const isDevBuild = Boolean(process.env.CODENOMAD_DEV ?? process.env.CLI_UI_DEV_SERVER) || existsSync(devTemplateDir) +const templateDir = isDevBuild ? devTemplateDir : prodTemplateDir +const userConfigDir = path.join(os.homedir(), ".config", "codenomad", "opencode-config") + +export function getOpencodeConfigDir(): string { + if (!existsSync(templateDir)) { + throw new Error(`CodeNomad Opencode config template missing at ${templateDir}`) + } + + if (isDevBuild) { + log.debug({ templateDir }, "Using Opencode config template directly (dev mode)") + return templateDir + } + + refreshUserConfig() + return userConfigDir +} + +function refreshUserConfig() { + log.debug({ templateDir, userConfigDir }, "Syncing Opencode config template") + rmSync(userConfigDir, { recursive: true, force: true }) + mkdirSync(path.dirname(userConfigDir), { recursive: true }) + cpSync(templateDir, userConfigDir, { recursive: true }) +} diff --git a/packages/server/src/releases/release-monitor.ts b/packages/server/src/releases/release-monitor.ts new file mode 100644 index 0000000..2fd80c9 --- /dev/null +++ b/packages/server/src/releases/release-monitor.ts @@ -0,0 +1,141 @@ +import { fetch } from "undici" +import type { LatestReleaseInfo } from "../api-types" +import type { Logger } from "../logger" + +const RELEASES_API_URL = "https://api.github.com/repos/NeuralNomadsAI/CodeNomad/releases/latest" +interface ReleaseMonitorOptions { + currentVersion: string + logger: Logger + onUpdate: (release: LatestReleaseInfo | null) => void +} + +interface GithubReleaseResponse { + tag_name?: string + name?: string + html_url?: string + body?: string + published_at?: string + created_at?: string + prerelease?: boolean +} + +interface NormalizedVersion { + major: number + minor: number + patch: number + prerelease: string | null +} + +export interface ReleaseMonitor { + stop(): void +} + +export function startReleaseMonitor(options: ReleaseMonitorOptions): ReleaseMonitor { + let stopped = false + + const refreshRelease = async () => { + if (stopped) return + try { + const release = await fetchLatestRelease(options) + options.onUpdate(release) + } catch (error) { + options.logger.warn({ err: error }, "Failed to refresh release information") + } + } + + void refreshRelease() + + return { + stop() { + stopped = true + }, + } +} + +async function fetchLatestRelease(options: ReleaseMonitorOptions): Promise { + const response = await fetch(RELEASES_API_URL, { + headers: { + Accept: "application/vnd.github+json", + "User-Agent": "CodeNomad-CLI", + }, + }) + + if (!response.ok) { + throw new Error(`Release API responded with ${response.status}`) + } + + const json = (await response.json()) as GithubReleaseResponse + const tagFromServer = json.tag_name || json.name + if (!tagFromServer) { + return null + } + + const normalizedVersion = stripTagPrefix(tagFromServer) + if (!normalizedVersion) { + return null + } + + const current = parseVersion(options.currentVersion) + const remote = parseVersion(normalizedVersion) + + if (compareVersions(remote, current) <= 0) { + return null + } + + return { + version: normalizedVersion, + tag: tagFromServer, + url: json.html_url ?? `https://github.com/NeuralNomadsAI/CodeNomad/releases/tag/${encodeURIComponent(tagFromServer)}`, + channel: json.prerelease || normalizedVersion.includes("-") ? "dev" : "stable", + publishedAt: json.published_at ?? json.created_at, + notes: json.body, + } +} + +function stripTagPrefix(tag: string | undefined): string | null { + if (!tag) return null + const trimmed = tag.trim() + if (!trimmed) return null + return trimmed.replace(/^v/i, "") +} + +function parseVersion(value: string): NormalizedVersion { + const normalized = stripTagPrefix(value) ?? "0.0.0" + const [core, prerelease = null] = normalized.split("-", 2) + const [major = 0, minor = 0, patch = 0] = core.split(".").map((segment) => { + const parsed = Number.parseInt(segment, 10) + return Number.isFinite(parsed) ? parsed : 0 + }) + return { + major, + minor, + patch, + prerelease, + } +} + +function compareVersions(a: NormalizedVersion, b: NormalizedVersion): number { + if (a.major !== b.major) { + return a.major > b.major ? 1 : -1 + } + if (a.minor !== b.minor) { + return a.minor > b.minor ? 1 : -1 + } + if (a.patch !== b.patch) { + return a.patch > b.patch ? 1 : -1 + } + + const aPre = a.prerelease && a.prerelease.length > 0 ? a.prerelease : null + const bPre = b.prerelease && b.prerelease.length > 0 ? b.prerelease : null + + if (aPre === bPre) { + return 0 + } + if (!aPre) { + return 1 + } + if (!bPre) { + return -1 + } + return aPre.localeCompare(bPre) +} diff --git a/packages/server/src/server/http-server.ts b/packages/server/src/server/http-server.ts new file mode 100644 index 0000000..eb57fb0 --- /dev/null +++ b/packages/server/src/server/http-server.ts @@ -0,0 +1,364 @@ +import Fastify, { type FastifyInstance, type FastifyReply, type FastifyRequest } from "fastify" +import cors from "@fastify/cors" +import fastifyStatic from "@fastify/static" +import replyFrom from "@fastify/reply-from" +import fs from "fs" +import path from "path" +import { fetch } from "undici" +import type { Logger } from "../logger" +import { WorkspaceManager } from "../workspaces/manager" + +import { ConfigStore } from "../config/store" +import { BinaryRegistry } from "../config/binaries" +import { FileSystemBrowser } from "../filesystem/browser" +import { EventBus } from "../events/bus" +import { registerWorkspaceRoutes } from "./routes/workspaces" +import { registerConfigRoutes } from "./routes/config" +import { registerFilesystemRoutes } from "./routes/filesystem" +import { registerMetaRoutes } from "./routes/meta" +import { registerEventRoutes } from "./routes/events" +import { registerStorageRoutes } from "./routes/storage" +import { ServerMeta } from "../api-types" +import { InstanceStore } from "../storage/instance-store" + +interface HttpServerDeps { + host: string + port: number + workspaceManager: WorkspaceManager + configStore: ConfigStore + binaryRegistry: BinaryRegistry + fileSystemBrowser: FileSystemBrowser + eventBus: EventBus + serverMeta: ServerMeta + instanceStore: InstanceStore + uiStaticDir: string + uiDevServerUrl?: string + logger: Logger +} + +interface HttpServerStartResult { + port: number + url: string + displayHost: string +} + +const DEFAULT_HTTP_PORT = 9898 + +export function createHttpServer(deps: HttpServerDeps) { + const app = Fastify({ logger: false }) + const proxyLogger = deps.logger.child({ component: "proxy" }) + const apiLogger = deps.logger.child({ component: "http" }) + const sseLogger = deps.logger.child({ component: "sse" }) + + const sseClients = new Set<() => void>() + const registerSseClient = (cleanup: () => void) => { + sseClients.add(cleanup) + return () => sseClients.delete(cleanup) + } + const closeSseClients = () => { + for (const cleanup of Array.from(sseClients)) { + cleanup() + } + sseClients.clear() + } + + app.addHook("onRequest", (request, _reply, done) => { + ;(request as FastifyRequest & { __logMeta?: { start: bigint } }).__logMeta = { + start: process.hrtime.bigint(), + } + done() + }) + + app.addHook("onResponse", (request, reply, done) => { + const meta = (request as FastifyRequest & { __logMeta?: { start: bigint } }).__logMeta + const durationMs = meta ? Number((process.hrtime.bigint() - meta.start) / BigInt(1_000_000)) : undefined + const base = { + method: request.method, + url: request.url, + status: reply.statusCode, + durationMs, + } + apiLogger.debug(base, "HTTP request completed") + if (apiLogger.isLevelEnabled("trace")) { + apiLogger.trace({ ...base, params: request.params, query: request.query, body: request.body }, "HTTP request payload") + } + done() + }) + + app.register(cors, { + origin: true, + credentials: true, + }) + + app.register(replyFrom, { + contentTypesToEncode: [], + undici: { + connections: 16, + pipelining: 1, + bodyTimeout: 0, + headersTimeout: 0, + }, + }) + + registerWorkspaceRoutes(app, { workspaceManager: deps.workspaceManager }) + registerConfigRoutes(app, { configStore: deps.configStore, binaryRegistry: deps.binaryRegistry }) + registerFilesystemRoutes(app, { fileSystemBrowser: deps.fileSystemBrowser }) + registerMetaRoutes(app, { serverMeta: deps.serverMeta }) + registerEventRoutes(app, { eventBus: deps.eventBus, registerClient: registerSseClient, logger: sseLogger }) + registerStorageRoutes(app, { + instanceStore: deps.instanceStore, + eventBus: deps.eventBus, + workspaceManager: deps.workspaceManager, + }) + registerInstanceProxyRoutes(app, { workspaceManager: deps.workspaceManager, logger: proxyLogger }) + + + if (deps.uiDevServerUrl) { + setupDevProxy(app, deps.uiDevServerUrl) + } else { + setupStaticUi(app, deps.uiStaticDir) + } + + return { + instance: app, + start: async (): Promise => { + const attemptListen = async (requestedPort: number) => { + const addressInfo = await app.listen({ port: requestedPort, host: deps.host }) + return { addressInfo, requestedPort } + } + + const autoPortRequested = deps.port === 0 + const primaryPort = autoPortRequested ? DEFAULT_HTTP_PORT : deps.port + + const shouldRetryWithEphemeral = (error: unknown) => { + if (!autoPortRequested) return false + const err = error as NodeJS.ErrnoException | undefined + return Boolean(err && err.code === "EADDRINUSE") + } + + let listenResult + + try { + listenResult = await attemptListen(primaryPort) + } catch (error) { + if (!shouldRetryWithEphemeral(error)) { + throw error + } + deps.logger.warn({ err: error, port: primaryPort }, "Preferred port unavailable, retrying on ephemeral port") + listenResult = await attemptListen(0) + } + + let actualPort = listenResult.requestedPort + + if (typeof listenResult.addressInfo === "string") { + try { + const parsed = new URL(listenResult.addressInfo) + actualPort = Number(parsed.port) || listenResult.requestedPort + } catch { + actualPort = listenResult.requestedPort + } + } else { + const address = app.server.address() + if (typeof address === "object" && address) { + actualPort = address.port + } + } + + const displayHost = deps.host === "0.0.0.0" ? "127.0.0.1" : deps.host === "127.0.0.1" ? "localhost" : deps.host + const serverUrl = `http://${displayHost}:${actualPort}` + + deps.serverMeta.httpBaseUrl = serverUrl + deps.serverMeta.host = deps.host + deps.serverMeta.port = actualPort + deps.serverMeta.listeningMode = deps.host === "0.0.0.0" ? "all" : "local" + deps.logger.info({ port: actualPort, host: deps.host }, "HTTP server listening") + console.log(`CodeNomad Server is ready at ${serverUrl}`) + + return { port: actualPort, url: serverUrl, displayHost } + }, + stop: () => { + closeSseClients() + return app.close() + }, + } +} + +interface InstanceProxyDeps { + workspaceManager: WorkspaceManager + logger: Logger +} + +function registerInstanceProxyRoutes(app: FastifyInstance, deps: InstanceProxyDeps) { + app.register(async (instance) => { + instance.removeAllContentTypeParsers() + instance.addContentTypeParser("*", (req, body, done) => done(null, body)) + + const proxyBaseHandler = async (request: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply) => { + await proxyWorkspaceRequest({ + request, + reply, + workspaceManager: deps.workspaceManager, + pathSuffix: "", + logger: deps.logger, + }) + } + + const proxyWildcardHandler = async ( + request: FastifyRequest<{ Params: { id: string; "*": string } }>, + reply: FastifyReply, + ) => { + await proxyWorkspaceRequest({ + request, + reply, + workspaceManager: deps.workspaceManager, + pathSuffix: request.params["*"] ?? "", + logger: deps.logger, + }) + } + + instance.all("/workspaces/:id/instance", proxyBaseHandler) + instance.all("/workspaces/:id/instance/*", proxyWildcardHandler) + }) +} + +const INSTANCE_PROXY_HOST = "127.0.0.1" + +async function proxyWorkspaceRequest(args: { + request: FastifyRequest + reply: FastifyReply + workspaceManager: WorkspaceManager + logger: Logger + pathSuffix?: string +}) { + const { request, reply, workspaceManager, logger } = args + const workspaceId = (request.params as { id: string }).id + const workspace = workspaceManager.get(workspaceId) + + if (!workspace) { + reply.code(404).send({ error: "Workspace not found" }) + return + } + + const port = workspaceManager.getInstancePort(workspaceId) + if (!port) { + reply.code(502).send({ error: "Workspace instance is not ready" }) + return + } + + const normalizedSuffix = normalizeInstanceSuffix(args.pathSuffix) + const queryIndex = (request.raw.url ?? "").indexOf("?") + const search = queryIndex >= 0 ? (request.raw.url ?? "").slice(queryIndex) : "" + const targetUrl = `http://${INSTANCE_PROXY_HOST}:${port}${normalizedSuffix}${search}` + + logger.debug({ workspaceId, method: request.method, targetUrl }, "Proxying request to instance") + if (logger.isLevelEnabled("trace")) { + logger.trace({ workspaceId, targetUrl, body: request.body }, "Instance proxy payload") + } + + return reply.from(targetUrl, { + onError: (proxyReply, { error }) => { + logger.error({ err: error, workspaceId, targetUrl }, "Failed to proxy workspace request") + if (!proxyReply.sent) { + proxyReply.code(502).send({ error: "Workspace instance proxy failed" }) + } + }, + }) +} + +function normalizeInstanceSuffix(pathSuffix: string | undefined) { + if (!pathSuffix || pathSuffix === "/") { + return "/" + } + const trimmed = pathSuffix.replace(/^\/+/, "") + return trimmed.length === 0 ? "/" : `/${trimmed}` +} + +function setupStaticUi(app: FastifyInstance, uiDir: string) { + if (!uiDir) { + app.log.warn("UI static directory not provided; API endpoints only") + return + } + + if (!fs.existsSync(uiDir)) { + app.log.warn({ uiDir }, "UI static directory missing; API endpoints only") + return + } + + app.register(fastifyStatic, { + root: uiDir, + prefix: "/", + decorateReply: false, + }) + + const indexPath = path.join(uiDir, "index.html") + + app.setNotFoundHandler((request: FastifyRequest, reply: FastifyReply) => { + const url = request.raw.url ?? "" + if (isApiRequest(url)) { + reply.code(404).send({ message: "Not Found" }) + return + } + + if (fs.existsSync(indexPath)) { + reply.type("text/html").send(fs.readFileSync(indexPath, "utf-8")) + } else { + reply.code(404).send({ message: "UI bundle missing" }) + } + }) +} + +function setupDevProxy(app: FastifyInstance, upstreamBase: string) { + app.log.info({ upstreamBase }, "Proxying UI requests to development server") + app.setNotFoundHandler((request: FastifyRequest, reply: FastifyReply) => { + const url = request.raw.url ?? "" + if (isApiRequest(url)) { + reply.code(404).send({ message: "Not Found" }) + return + } + void proxyToDevServer(request, reply, upstreamBase) + }) +} + +async function proxyToDevServer(request: FastifyRequest, reply: FastifyReply, upstreamBase: string) { + try { + const targetUrl = new URL(request.raw.url ?? "/", upstreamBase) + const response = await fetch(targetUrl, { + method: request.method, + headers: buildProxyHeaders(request.headers), + }) + + response.headers.forEach((value, key) => { + reply.header(key, value) + }) + + reply.code(response.status) + + if (!response.body || request.method === "HEAD") { + reply.send() + return + } + + const buffer = Buffer.from(await response.arrayBuffer()) + reply.send(buffer) + } catch (error) { + request.log.error({ err: error }, "Failed to proxy UI request to dev server") + if (!reply.sent) { + reply.code(502).send("UI dev server is unavailable") + } + } +} + +function isApiRequest(rawUrl: string | null | undefined) { + if (!rawUrl) return false + const pathname = rawUrl.split("?")[0] ?? "" + return pathname === "/api" || pathname.startsWith("/api/") +} + +function buildProxyHeaders(headers: FastifyRequest["headers"]): Record { + const result: Record = {} + for (const [key, value] of Object.entries(headers ?? {})) { + if (!value || key.toLowerCase() === "host") continue + result[key] = Array.isArray(value) ? value.join(",") : value + } + return result +} diff --git a/packages/server/src/server/routes/config.ts b/packages/server/src/server/routes/config.ts new file mode 100644 index 0000000..fed364a --- /dev/null +++ b/packages/server/src/server/routes/config.ts @@ -0,0 +1,62 @@ +import { FastifyInstance } from "fastify" +import { z } from "zod" +import { ConfigStore } from "../../config/store" +import { BinaryRegistry } from "../../config/binaries" +import { ConfigFileSchema } from "../../config/schema" + +interface RouteDeps { + configStore: ConfigStore + binaryRegistry: BinaryRegistry +} + +const BinaryCreateSchema = z.object({ + path: z.string(), + label: z.string().optional(), + makeDefault: z.boolean().optional(), +}) + +const BinaryUpdateSchema = z.object({ + label: z.string().optional(), + makeDefault: z.boolean().optional(), +}) + +const BinaryValidateSchema = z.object({ + path: z.string(), +}) + +export function registerConfigRoutes(app: FastifyInstance, deps: RouteDeps) { + app.get("/api/config/app", async () => deps.configStore.get()) + + app.put("/api/config/app", async (request) => { + const body = ConfigFileSchema.parse(request.body ?? {}) + deps.configStore.replace(body) + return deps.configStore.get() + }) + + app.get("/api/config/binaries", async () => { + return { binaries: deps.binaryRegistry.list() } + }) + + app.post("/api/config/binaries", async (request, reply) => { + const body = BinaryCreateSchema.parse(request.body ?? {}) + const binary = deps.binaryRegistry.create(body) + reply.code(201) + return { binary } + }) + + app.patch<{ Params: { id: string } }>("/api/config/binaries/:id", async (request) => { + const body = BinaryUpdateSchema.parse(request.body ?? {}) + const binary = deps.binaryRegistry.update(request.params.id, body) + return { binary } + }) + + app.delete<{ Params: { id: string } }>("/api/config/binaries/:id", async (request, reply) => { + deps.binaryRegistry.remove(request.params.id) + reply.code(204) + }) + + app.post("/api/config/binaries/validate", async (request) => { + const body = BinaryValidateSchema.parse(request.body ?? {}) + return deps.binaryRegistry.validatePath(body.path) + }) +} diff --git a/packages/server/src/server/routes/events.ts b/packages/server/src/server/routes/events.ts new file mode 100644 index 0000000..e8f2329 --- /dev/null +++ b/packages/server/src/server/routes/events.ts @@ -0,0 +1,61 @@ +import { FastifyInstance } from "fastify" +import { EventBus } from "../../events/bus" +import { WorkspaceEventPayload } from "../../api-types" +import { Logger } from "../../logger" + +interface RouteDeps { + eventBus: EventBus + registerClient: (cleanup: () => void) => () => void + logger: Logger +} + +let nextClientId = 0 + +export function registerEventRoutes(app: FastifyInstance, deps: RouteDeps) { + app.get("/api/events", (request, reply) => { + const clientId = ++nextClientId + deps.logger.debug({ clientId }, "SSE client connected") + + const origin = request.headers.origin ?? "*" + reply.raw.setHeader("Access-Control-Allow-Origin", origin) + reply.raw.setHeader("Access-Control-Allow-Credentials", "true") + reply.raw.setHeader("Content-Type", "text/event-stream") + reply.raw.setHeader("Cache-Control", "no-cache") + reply.raw.setHeader("Connection", "keep-alive") + reply.raw.flushHeaders?.() + reply.hijack() + + const send = (event: WorkspaceEventPayload) => { + deps.logger.debug({ clientId, type: event.type }, "SSE event dispatched") + if (deps.logger.isLevelEnabled("trace")) { + deps.logger.trace({ clientId, event }, "SSE event payload") + } + reply.raw.write(`data: ${JSON.stringify(event)}\n\n`) + } + + const unsubscribe = deps.eventBus.onEvent(send) + const heartbeat = setInterval(() => { + reply.raw.write(`:hb ${Date.now()}\n\n`) + }, 15000) + + let closed = false + const close = () => { + if (closed) return + closed = true + clearInterval(heartbeat) + unsubscribe() + reply.raw.end?.() + deps.logger.debug({ clientId }, "SSE client disconnected") + } + + const unregister = deps.registerClient(close) + + const handleClose = () => { + close() + unregister() + } + + request.raw.on("close", handleClose) + request.raw.on("error", handleClose) + }) +} diff --git a/packages/server/src/server/routes/filesystem.ts b/packages/server/src/server/routes/filesystem.ts new file mode 100644 index 0000000..d919c29 --- /dev/null +++ b/packages/server/src/server/routes/filesystem.ts @@ -0,0 +1,27 @@ +import { FastifyInstance } from "fastify" +import { z } from "zod" +import { FileSystemBrowser } from "../../filesystem/browser" + +interface RouteDeps { + fileSystemBrowser: FileSystemBrowser +} + +const FilesystemQuerySchema = z.object({ + path: z.string().optional(), + includeFiles: z.coerce.boolean().optional(), +}) + +export function registerFilesystemRoutes(app: FastifyInstance, deps: RouteDeps) { + app.get("/api/filesystem", async (request, reply) => { + const query = FilesystemQuerySchema.parse(request.query ?? {}) + + try { + return deps.fileSystemBrowser.browse(query.path, { + includeFiles: query.includeFiles, + }) + } catch (error) { + reply.code(400) + return { error: (error as Error).message } + } + }) +} diff --git a/packages/server/src/server/routes/meta.ts b/packages/server/src/server/routes/meta.ts new file mode 100644 index 0000000..d716198 --- /dev/null +++ b/packages/server/src/server/routes/meta.ts @@ -0,0 +1,104 @@ +import { FastifyInstance } from "fastify" +import os from "os" +import { NetworkAddress, ServerMeta } from "../../api-types" + +interface RouteDeps { + serverMeta: ServerMeta +} + +export function registerMetaRoutes(app: FastifyInstance, deps: RouteDeps) { + app.get("/api/meta", async () => buildMetaResponse(deps.serverMeta)) +} + +function buildMetaResponse(meta: ServerMeta): ServerMeta { + const port = resolvePort(meta) + const addresses = port > 0 ? resolveAddresses(port, meta.host) : [] + + return { + ...meta, + port, + listeningMode: meta.host === "0.0.0.0" ? "all" : "local", + addresses, + } +} + +function resolvePort(meta: ServerMeta): number { + if (Number.isInteger(meta.port) && meta.port > 0) { + return meta.port + } + try { + const parsed = new URL(meta.httpBaseUrl) + const port = Number(parsed.port) + return Number.isInteger(port) && port > 0 ? port : 0 + } catch { + return 0 + } +} + +function resolveAddresses(port: number, host: string): NetworkAddress[] { + const interfaces = os.networkInterfaces() + const seen = new Set() + const results: NetworkAddress[] = [] + + const addAddress = (ip: string, scope: NetworkAddress["scope"]) => { + if (!ip || ip === "0.0.0.0") return + const key = `ipv4-${ip}` + if (seen.has(key)) return + seen.add(key) + results.push({ ip, family: "ipv4", scope, url: `http://${ip}:${port}` }) + } + + const normalizeFamily = (value: string | number) => { + if (typeof value === "string") { + const lowered = value.toLowerCase() + if (lowered === "ipv4") { + return "ipv4" as const + } + } + if (value === 4) return "ipv4" as const + return null + } + + if (host === "0.0.0.0") { + // Enumerate system interfaces (IPv4 only) + for (const entries of Object.values(interfaces)) { + if (!entries) continue + for (const entry of entries) { + const family = normalizeFamily(entry.family) + if (!family) continue + if (!entry.address || entry.address === "0.0.0.0") continue + const scope: NetworkAddress["scope"] = entry.internal ? "loopback" : "external" + addAddress(entry.address, scope) + } + } + } + + // Always include loopback address + addAddress("127.0.0.1", "loopback") + + // Include explicitly configured host if it was IPv4 + if (isIPv4Address(host) && host !== "0.0.0.0") { + const isLoopback = host.startsWith("127.") + addAddress(host, isLoopback ? "loopback" : "external") + } + + const scopeWeight: Record = { external: 0, internal: 1, loopback: 2 } + + return results.sort((a, b) => { + const scopeDelta = scopeWeight[a.scope] - scopeWeight[b.scope] + if (scopeDelta !== 0) return scopeDelta + return a.ip.localeCompare(b.ip) + }) +} + +function isIPv4Address(value: string | undefined): value is string { + if (!value) return false + const parts = value.split(".") + if (parts.length !== 4) return false + return parts.every((part) => { + if (part.length === 0 || part.length > 3) return false + if (!/^[0-9]+$/.test(part)) return false + const num = Number(part) + return Number.isInteger(num) && num >= 0 && num <= 255 + }) +} diff --git a/packages/server/src/server/routes/storage.ts b/packages/server/src/server/routes/storage.ts new file mode 100644 index 0000000..a2a874e --- /dev/null +++ b/packages/server/src/server/routes/storage.ts @@ -0,0 +1,66 @@ +import { FastifyInstance } from "fastify" +import { z } from "zod" +import { InstanceStore } from "../../storage/instance-store" +import { EventBus } from "../../events/bus" +import { ModelPreferenceSchema } from "../../config/schema" +import type { InstanceData } from "../../api-types" +import { WorkspaceManager } from "../../workspaces/manager" + +interface RouteDeps { + instanceStore: InstanceStore + eventBus: EventBus + workspaceManager: WorkspaceManager +} + +const InstanceDataSchema = z.object({ + messageHistory: z.array(z.string()).default([]), + agentModelSelections: z.record(z.string(), ModelPreferenceSchema).default({}), +}) + +const EMPTY_INSTANCE_DATA: InstanceData = { + messageHistory: [], + agentModelSelections: {}, +} + +export function registerStorageRoutes(app: FastifyInstance, deps: RouteDeps) { + const resolveStorageKey = (instanceId: string): string => { + const workspace = deps.workspaceManager.get(instanceId) + return workspace?.path ?? instanceId + } + + app.get<{ Params: { id: string } }>("/api/storage/instances/:id", async (request, reply) => { + try { + const storageId = resolveStorageKey(request.params.id) + const data = await deps.instanceStore.read(storageId) + return data + } catch (error) { + reply.code(500) + return { error: error instanceof Error ? error.message : "Failed to read instance data" } + } + }) + + app.put<{ Params: { id: string } }>("/api/storage/instances/:id", async (request, reply) => { + try { + const body = InstanceDataSchema.parse(request.body ?? {}) + const storageId = resolveStorageKey(request.params.id) + await deps.instanceStore.write(storageId, body) + deps.eventBus.publish({ type: "instance.dataChanged", instanceId: request.params.id, data: body }) + reply.code(204) + } catch (error) { + reply.code(400) + return { error: error instanceof Error ? error.message : "Failed to save instance data" } + } + }) + + app.delete<{ Params: { id: string } }>("/api/storage/instances/:id", async (request, reply) => { + try { + const storageId = resolveStorageKey(request.params.id) + await deps.instanceStore.delete(storageId) + deps.eventBus.publish({ type: "instance.dataChanged", instanceId: request.params.id, data: EMPTY_INSTANCE_DATA }) + reply.code(204) + } catch (error) { + reply.code(500) + return { error: error instanceof Error ? error.message : "Failed to delete instance data" } + } + }) +} diff --git a/packages/server/src/server/routes/workspaces.ts b/packages/server/src/server/routes/workspaces.ts new file mode 100644 index 0000000..1541475 --- /dev/null +++ b/packages/server/src/server/routes/workspaces.ts @@ -0,0 +1,113 @@ +import { FastifyInstance, FastifyReply } from "fastify" +import { z } from "zod" +import { WorkspaceManager } from "../../workspaces/manager" + +interface RouteDeps { + workspaceManager: WorkspaceManager +} + +const WorkspaceCreateSchema = z.object({ + path: z.string(), + name: z.string().optional(), +}) + +const WorkspaceFilesQuerySchema = z.object({ + path: z.string().optional(), +}) + +const WorkspaceFileContentQuerySchema = z.object({ + path: z.string(), +}) + +const WorkspaceFileSearchQuerySchema = z.object({ + q: z.string().trim().min(1, "Query is required"), + limit: z.coerce.number().int().positive().max(200).optional(), + type: z.enum(["all", "file", "directory"]).optional(), + refresh: z + .string() + .optional() + .transform((value) => (value === undefined ? undefined : value === "true")), +}) + +export function registerWorkspaceRoutes(app: FastifyInstance, deps: RouteDeps) { + app.get("/api/workspaces", async () => { + return deps.workspaceManager.list() + }) + + app.post("/api/workspaces", async (request, reply) => { + try { + const body = WorkspaceCreateSchema.parse(request.body ?? {}) + const workspace = await deps.workspaceManager.create(body.path, body.name) + reply.code(201) + return workspace + } catch (error) { + request.log.error({ err: error }, "Failed to create workspace") + const message = error instanceof Error ? error.message : "Failed to create workspace" + reply.code(400).type("text/plain").send(message) + } + }) + + app.get<{ Params: { id: string } }>("/api/workspaces/:id", async (request, reply) => { + const workspace = deps.workspaceManager.get(request.params.id) + if (!workspace) { + reply.code(404) + return { error: "Workspace not found" } + } + return workspace + }) + + app.delete<{ Params: { id: string } }>("/api/workspaces/:id", async (request, reply) => { + await deps.workspaceManager.delete(request.params.id) + reply.code(204) + }) + + app.get<{ + Params: { id: string } + Querystring: { path?: string } + }>("/api/workspaces/:id/files", async (request, reply) => { + try { + const query = WorkspaceFilesQuerySchema.parse(request.query ?? {}) + return deps.workspaceManager.listFiles(request.params.id, query.path ?? ".") + } catch (error) { + return handleWorkspaceError(error, reply) + } + }) + + app.get<{ + Params: { id: string } + Querystring: { q?: string; limit?: string; type?: "all" | "file" | "directory"; refresh?: string } + }>("/api/workspaces/:id/files/search", async (request, reply) => { + try { + const query = WorkspaceFileSearchQuerySchema.parse(request.query ?? {}) + return deps.workspaceManager.searchFiles(request.params.id, query.q, { + limit: query.limit, + type: query.type, + refresh: query.refresh, + }) + } catch (error) { + return handleWorkspaceError(error, reply) + } + }) + + app.get<{ + Params: { id: string } + Querystring: { path?: string } + }>("/api/workspaces/:id/files/content", async (request, reply) => { + try { + const query = WorkspaceFileContentQuerySchema.parse(request.query ?? {}) + return deps.workspaceManager.readFile(request.params.id, query.path) + } catch (error) { + return handleWorkspaceError(error, reply) + } + }) +} + + +function handleWorkspaceError(error: unknown, reply: FastifyReply) { + if (error instanceof Error && error.message === "Workspace not found") { + reply.code(404) + return { error: "Workspace not found" } + } + reply.code(400) + return { error: error instanceof Error ? error.message : "Unable to fulfill request" } +} diff --git a/packages/server/src/storage/instance-store.ts b/packages/server/src/storage/instance-store.ts new file mode 100644 index 0000000..4855084 --- /dev/null +++ b/packages/server/src/storage/instance-store.ts @@ -0,0 +1,64 @@ +import fs from "fs" +import { promises as fsp } from "fs" +import os from "os" +import path from "path" +import type { InstanceData } from "../api-types" + +const DEFAULT_INSTANCE_DATA: InstanceData = { + messageHistory: [], + agentModelSelections: {}, +} + +export class InstanceStore { + private readonly instancesDir: string + + constructor(baseDir = path.join(os.homedir(), ".config", "codenomad", "instances")) { + this.instancesDir = baseDir + fs.mkdirSync(this.instancesDir, { recursive: true }) + } + + async read(id: string): Promise { + try { + const filePath = this.resolvePath(id) + const content = await fsp.readFile(filePath, "utf-8") + const parsed = JSON.parse(content) + return { ...DEFAULT_INSTANCE_DATA, ...parsed } + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") { + return DEFAULT_INSTANCE_DATA + } + throw error + } + } + + async write(id: string, data: InstanceData): Promise { + const filePath = this.resolvePath(id) + await fsp.mkdir(path.dirname(filePath), { recursive: true }) + await fsp.writeFile(filePath, JSON.stringify(data, null, 2), "utf-8") + } + + async delete(id: string): Promise { + try { + const filePath = this.resolvePath(id) + await fsp.unlink(filePath) + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") { + throw error + } + } + } + + private resolvePath(id: string): string { + const filename = this.sanitizeId(id) + return path.join(this.instancesDir, `${filename}.json`) + } + + private sanitizeId(id: string): string { + return id + .replace(/[\\/]/g, "_") + .replace(/[^a-zA-Z0-9_.-]/g, "_") + .replace(/_{2,}/g, "_") + .replace(/^_|_$/g, "") + .toLowerCase() + } +} diff --git a/packages/server/src/workspaces/instance-events.ts b/packages/server/src/workspaces/instance-events.ts new file mode 100644 index 0000000..5fff69a --- /dev/null +++ b/packages/server/src/workspaces/instance-events.ts @@ -0,0 +1,195 @@ +import { Agent, fetch } from "undici" +import { Agent as UndiciAgent } from "undici" +import { EventBus } from "../events/bus" +import { Logger } from "../logger" +import { WorkspaceManager } from "./manager" +import { InstanceStreamEvent, InstanceStreamStatus } from "../api-types" + +const INSTANCE_HOST = "127.0.0.1" +const STREAM_AGENT = new UndiciAgent({ bodyTimeout: 0, headersTimeout: 0 }) +const RECONNECT_DELAY_MS = 1000 + +interface InstanceEventBridgeOptions { + workspaceManager: WorkspaceManager + eventBus: EventBus + logger: Logger +} + +interface ActiveStream { + controller: AbortController + task: Promise +} + +export class InstanceEventBridge { + private readonly streams = new Map() + + constructor(private readonly options: InstanceEventBridgeOptions) { + const bus = this.options.eventBus + bus.on("workspace.started", (event) => this.startStream(event.workspace.id)) + bus.on("workspace.stopped", (event) => this.stopStream(event.workspaceId, "workspace stopped")) + bus.on("workspace.error", (event) => this.stopStream(event.workspace.id, "workspace error")) + } + + shutdown() { + for (const [id, active] of this.streams) { + active.controller.abort() + this.publishStatus(id, "disconnected") + } + this.streams.clear() + } + + private startStream(workspaceId: string) { + if (this.streams.has(workspaceId)) { + return + } + + const controller = new AbortController() + const task = this.runStream(workspaceId, controller.signal) + .catch((error) => { + if (!controller.signal.aborted) { + this.options.logger.warn({ workspaceId, err: error }, "Instance event stream failed") + this.publishStatus(workspaceId, "error", error instanceof Error ? error.message : String(error)) + } + }) + .finally(() => { + const active = this.streams.get(workspaceId) + if (active?.controller === controller) { + this.streams.delete(workspaceId) + } + }) + + this.streams.set(workspaceId, { controller, task }) + } + + private stopStream(workspaceId: string, reason?: string) { + const active = this.streams.get(workspaceId) + if (!active) { + return + } + active.controller.abort() + this.streams.delete(workspaceId) + this.publishStatus(workspaceId, "disconnected", reason) + } + + private async runStream(workspaceId: string, signal: AbortSignal) { + while (!signal.aborted) { + const port = this.options.workspaceManager.getInstancePort(workspaceId) + if (!port) { + await this.delay(RECONNECT_DELAY_MS, signal) + continue + } + + this.publishStatus(workspaceId, "connecting") + + try { + await this.consumeStream(workspaceId, port, signal) + } catch (error) { + if (signal.aborted) { + break + } + this.options.logger.warn({ workspaceId, err: error }, "Instance event stream disconnected") + this.publishStatus(workspaceId, "error", error instanceof Error ? error.message : String(error)) + await this.delay(RECONNECT_DELAY_MS, signal) + } + } + } + + private async consumeStream(workspaceId: string, port: number, signal: AbortSignal) { + const url = `http://${INSTANCE_HOST}:${port}/event` + const response = await fetch(url, { + headers: { Accept: "text/event-stream" }, + signal, + dispatcher: STREAM_AGENT, + }) + + if (!response.ok || !response.body) { + throw new Error(`Instance event stream unavailable (${response.status})`) + } + + this.publishStatus(workspaceId, "connected") + + const reader = response.body.getReader() + const decoder = new TextDecoder() + let buffer = "" + + while (!signal.aborted) { + const { done, value } = await reader.read() + if (done || !value) { + break + } + buffer += decoder.decode(value, { stream: true }) + buffer = this.flushEvents(buffer, workspaceId) + } + } + + private flushEvents(buffer: string, workspaceId: string) { + let separatorIndex = buffer.indexOf("\n\n") + + while (separatorIndex >= 0) { + const chunk = buffer.slice(0, separatorIndex) + buffer = buffer.slice(separatorIndex + 2) + this.processChunk(chunk, workspaceId) + separatorIndex = buffer.indexOf("\n\n") + } + + return buffer + } + + private processChunk(chunk: string, workspaceId: string) { + const lines = chunk.split(/\r?\n/) + const dataLines: string[] = [] + + for (const line of lines) { + if (line.startsWith(":")) { + continue + } + if (line.startsWith("data:")) { + dataLines.push(line.slice(5).trimStart()) + } + } + + if (dataLines.length === 0) { + return + } + + const payload = dataLines.join("\n").trim() + if (!payload) { + return + } + + try { + const event = JSON.parse(payload) as InstanceStreamEvent + this.options.logger.debug({ workspaceId, eventType: event.type }, "Instance SSE event received") + if (this.options.logger.isLevelEnabled("trace")) { + this.options.logger.trace({ workspaceId, event }, "Instance SSE event payload") + } + this.options.eventBus.publish({ type: "instance.event", instanceId: workspaceId, event }) + } catch (error) { + this.options.logger.warn({ workspaceId, chunk: payload, err: error }, "Failed to parse instance SSE payload") + } + } + + private publishStatus(instanceId: string, status: InstanceStreamStatus, reason?: string) { + this.options.logger.debug({ instanceId, status, reason }, "Instance SSE status updated") + this.options.eventBus.publish({ type: "instance.eventStatus", instanceId, status, reason }) + } + + private delay(duration: number, signal: AbortSignal) { + if (duration <= 0) { + return Promise.resolve() + } + return new Promise((resolve) => { + const timeout = setTimeout(() => { + signal.removeEventListener("abort", onAbort) + resolve() + }, duration) + + const onAbort = () => { + clearTimeout(timeout) + resolve() + } + + signal.addEventListener("abort", onAbort, { once: true }) + }) + } +} diff --git a/packages/server/src/workspaces/manager.ts b/packages/server/src/workspaces/manager.ts new file mode 100644 index 0000000..02af022 --- /dev/null +++ b/packages/server/src/workspaces/manager.ts @@ -0,0 +1,422 @@ +import path from "path" +import { spawnSync } from "child_process" +import { connect } from "net" +import { EventBus } from "../events/bus" +import { ConfigStore } from "../config/store" +import { BinaryRegistry } from "../config/binaries" +import { FileSystemBrowser } from "../filesystem/browser" +import { searchWorkspaceFiles, WorkspaceFileSearchOptions } from "../filesystem/search" +import { clearWorkspaceSearchCache } from "../filesystem/search-cache" +import { WorkspaceDescriptor, WorkspaceFileResponse, FileSystemEntry } from "../api-types" +import { WorkspaceRuntime, ProcessExitInfo } from "./runtime" +import { Logger } from "../logger" +import { getOpencodeConfigDir } from "../opencode-config" + +const STARTUP_STABILITY_DELAY_MS = 1500 + +interface WorkspaceManagerOptions { + rootDir: string + configStore: ConfigStore + binaryRegistry: BinaryRegistry + eventBus: EventBus + logger: Logger +} + +interface WorkspaceRecord extends WorkspaceDescriptor {} + +export class WorkspaceManager { + private readonly workspaces = new Map() + private readonly runtime: WorkspaceRuntime + private readonly opencodeConfigDir: string + + constructor(private readonly options: WorkspaceManagerOptions) { + this.runtime = new WorkspaceRuntime(this.options.eventBus, this.options.logger) + this.opencodeConfigDir = getOpencodeConfigDir() + } + + list(): WorkspaceDescriptor[] { + return Array.from(this.workspaces.values()) + } + + get(id: string): WorkspaceDescriptor | undefined { + return this.workspaces.get(id) + } + + getInstancePort(id: string): number | undefined { + return this.workspaces.get(id)?.port + } + + listFiles(workspaceId: string, relativePath = "."): FileSystemEntry[] { + const workspace = this.requireWorkspace(workspaceId) + const browser = new FileSystemBrowser({ rootDir: workspace.path }) + return browser.list(relativePath) + } + + searchFiles(workspaceId: string, query: string, options?: WorkspaceFileSearchOptions): FileSystemEntry[] { + const workspace = this.requireWorkspace(workspaceId) + return searchWorkspaceFiles(workspace.path, query, options) + } + + readFile(workspaceId: string, relativePath: string): WorkspaceFileResponse { + const workspace = this.requireWorkspace(workspaceId) + const browser = new FileSystemBrowser({ rootDir: workspace.path }) + const contents = browser.readFile(relativePath) + return { + workspaceId, + relativePath, + contents, + } + } + + async create(folder: string, name?: string): Promise { + + const id = `${Date.now().toString(36)}` + const binary = this.options.binaryRegistry.resolveDefault() + const resolvedBinaryPath = this.resolveBinaryPath(binary.path) + const workspacePath = path.isAbsolute(folder) ? folder : path.resolve(this.options.rootDir, folder) + clearWorkspaceSearchCache(workspacePath) + + this.options.logger.info({ workspaceId: id, folder: workspacePath, binary: resolvedBinaryPath }, "Creating workspace") + + const proxyPath = `/workspaces/${id}/instance` + + + const descriptor: WorkspaceRecord = { + id, + path: workspacePath, + name, + status: "starting", + proxyPath, + binaryId: resolvedBinaryPath, + binaryLabel: binary.label, + binaryVersion: binary.version, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + } + + if (!descriptor.binaryVersion) { + descriptor.binaryVersion = this.detectBinaryVersion(resolvedBinaryPath) + } + + this.workspaces.set(id, descriptor) + + + this.options.eventBus.publish({ type: "workspace.created", workspace: descriptor }) + + const preferences = this.options.configStore.get().preferences ?? {} + const userEnvironment = preferences.environmentVariables ?? {} + const environment = { + ...userEnvironment, + OPENCODE_CONFIG_DIR: this.opencodeConfigDir, + } + + try { + const { pid, port, exitPromise, getLastOutput } = await this.runtime.launch({ + workspaceId: id, + folder: workspacePath, + binaryPath: resolvedBinaryPath, + environment, + onExit: (info) => this.handleProcessExit(info.workspaceId, info), + }) + + await this.waitForWorkspaceReadiness({ workspaceId: id, port, exitPromise, getLastOutput }) + + descriptor.pid = pid + descriptor.port = port + descriptor.status = "ready" + descriptor.updatedAt = new Date().toISOString() + this.options.eventBus.publish({ type: "workspace.started", workspace: descriptor }) + this.options.logger.info({ workspaceId: id, port }, "Workspace ready") + return descriptor + } catch (error) { + descriptor.status = "error" + descriptor.error = error instanceof Error ? error.message : String(error) + descriptor.updatedAt = new Date().toISOString() + this.options.eventBus.publish({ type: "workspace.error", workspace: descriptor }) + this.options.logger.error({ workspaceId: id, err: error }, "Workspace failed to start") + throw error + } + } + + async delete(id: string): Promise { + const workspace = this.workspaces.get(id) + if (!workspace) return undefined + + this.options.logger.info({ workspaceId: id }, "Stopping workspace") + const wasRunning = Boolean(workspace.pid) + if (wasRunning) { + await this.runtime.stop(id).catch((error) => { + this.options.logger.warn({ workspaceId: id, err: error }, "Failed to stop workspace process cleanly") + }) + } + + this.workspaces.delete(id) + clearWorkspaceSearchCache(workspace.path) + if (!wasRunning) { + this.options.eventBus.publish({ type: "workspace.stopped", workspaceId: id }) + } + return workspace + } + + async shutdown() { + this.options.logger.info("Shutting down all workspaces") + for (const [id, workspace] of this.workspaces) { + if (workspace.pid) { + this.options.logger.info({ workspaceId: id }, "Stopping workspace during shutdown") + await this.runtime.stop(id).catch((error) => { + this.options.logger.error({ workspaceId: id, err: error }, "Failed to stop workspace during shutdown") + }) + } else { + this.options.logger.debug({ workspaceId: id }, "Workspace already stopped") + } + } + this.workspaces.clear() + this.options.logger.info("All workspaces cleared") + } + + private requireWorkspace(id: string): WorkspaceRecord { + const workspace = this.workspaces.get(id) + if (!workspace) { + throw new Error("Workspace not found") + } + return workspace + } + + private resolveBinaryPath(identifier: string): string { + if (!identifier) { + return identifier + } + + const looksLikePath = identifier.includes("/") || identifier.includes("\\") || identifier.startsWith(".") + if (path.isAbsolute(identifier) || looksLikePath) { + return identifier + } + + const locator = process.platform === "win32" ? "where" : "which" + + try { + const result = spawnSync(locator, [identifier], { encoding: "utf8" }) + if (result.status === 0 && result.stdout) { + const resolved = result.stdout + .split(/\r?\n/) + .map((line) => line.trim()) + .find((line) => line.length > 0) + + if (resolved) { + this.options.logger.debug({ identifier, resolved }, "Resolved binary path from system PATH") + return resolved + } + } else if (result.error) { + this.options.logger.warn({ identifier, err: result.error }, "Failed to resolve binary path via locator command") + } + } catch (error) { + this.options.logger.warn({ identifier, err: error }, "Failed to resolve binary path from system PATH") + } + + return identifier + } + + private detectBinaryVersion(resolvedPath: string): string | undefined { + if (!resolvedPath) { + return undefined + } + + try { + const result = spawnSync(resolvedPath, ["--version"], { encoding: "utf8" }) + if (result.status === 0 && result.stdout) { + const line = result.stdout.split(/\r?\n/).find((entry) => entry.trim().length > 0) + if (line) { + const normalized = line.trim() + const versionMatch = normalized.match(/([0-9]+\.[0-9]+\.[0-9A-Za-z.-]+)/) + if (versionMatch) { + const version = versionMatch[1] + this.options.logger.debug({ binary: resolvedPath, version }, "Detected binary version") + return version + } + this.options.logger.debug({ binary: resolvedPath, reported: normalized }, "Binary reported version string") + return normalized + } + } else if (result.error) { + this.options.logger.warn({ binary: resolvedPath, err: result.error }, "Failed to read binary version") + } + } catch (error) { + this.options.logger.warn({ binary: resolvedPath, err: error }, "Failed to detect binary version") + } + + return undefined + } + + private async waitForWorkspaceReadiness(params: { + workspaceId: string + port: number + exitPromise: Promise + getLastOutput: () => string + }) { + + await Promise.race([ + this.waitForPortAvailability(params.port), + params.exitPromise.then((info) => { + throw this.buildStartupError( + params.workspaceId, + "exited before becoming ready", + info, + params.getLastOutput(), + ) + }), + ]) + + await this.waitForInstanceHealth(params) + + await Promise.race([ + this.delay(STARTUP_STABILITY_DELAY_MS), + params.exitPromise.then((info) => { + throw this.buildStartupError( + params.workspaceId, + "exited shortly after start", + info, + params.getLastOutput(), + ) + }), + ]) + } + + private async waitForInstanceHealth(params: { + workspaceId: string + port: number + exitPromise: Promise + getLastOutput: () => string + }) { + const probeResult = await Promise.race([ + this.probeInstance(params.workspaceId, params.port), + params.exitPromise.then((info) => { + throw this.buildStartupError( + params.workspaceId, + "exited during health checks", + info, + params.getLastOutput(), + ) + }), + ]) + + if (probeResult.ok) { + return + } + + const latestOutput = params.getLastOutput().trim() + const outputDetails = latestOutput ? ` Last output: ${latestOutput}` : "" + const reason = probeResult.reason ?? "Health check failed" + throw new Error(`Workspace ${params.workspaceId} failed health check: ${reason}.${outputDetails}`) + } + + private async probeInstance(workspaceId: string, port: number): Promise<{ ok: boolean; reason?: string }> { + const url = `http://127.0.0.1:${port}/project/current` + + try { + const response = await fetch(url) + if (!response.ok) { + const reason = `health probe returned HTTP ${response.status}` + this.options.logger.debug({ workspaceId, status: response.status }, "Health probe returned server error") + return { ok: false, reason } + } + return { ok: true } + } catch (error) { + const reason = error instanceof Error ? error.message : String(error) + this.options.logger.debug({ workspaceId, err: error }, "Health probe failed") + return { ok: false, reason } + } + } + + private buildStartupError( + workspaceId: string, + phase: string, + exitInfo: ProcessExitInfo, + lastOutput: string, + ): Error { + const exitDetails = this.describeExit(exitInfo) + const trimmedOutput = lastOutput.trim() + const outputDetails = trimmedOutput ? ` Last output: ${trimmedOutput}` : "" + return new Error(`Workspace ${workspaceId} ${phase} (${exitDetails}).${outputDetails}`) + } + + private waitForPortAvailability(port: number, timeoutMs = 5000): Promise { + return new Promise((resolve, reject) => { + const deadline = Date.now() + timeoutMs + let settled = false + let retryTimer: NodeJS.Timeout | null = null + + const cleanup = () => { + settled = true + if (retryTimer) { + clearTimeout(retryTimer) + retryTimer = null + } + } + + const tryConnect = () => { + if (settled) { + return + } + const socket = connect({ port, host: "127.0.0.1" }, () => { + cleanup() + socket.end() + resolve() + }) + socket.once("error", () => { + socket.destroy() + if (settled) { + return + } + if (Date.now() >= deadline) { + cleanup() + reject(new Error(`Workspace port ${port} did not become ready within ${timeoutMs}ms`)) + } else { + retryTimer = setTimeout(() => { + retryTimer = null + tryConnect() + }, 100) + } + }) + } + + tryConnect() + }) + } + + private delay(durationMs: number): Promise { + if (durationMs <= 0) { + return Promise.resolve() + } + return new Promise((resolve) => setTimeout(resolve, durationMs)) + } + + private describeExit(info: ProcessExitInfo): string { + if (info.signal) { + return `signal ${info.signal}` + } + if (info.code !== null) { + return `code ${info.code}` + } + return "unknown reason" + } + + private handleProcessExit(workspaceId: string, info: { code: number | null; requested: boolean }) { + const workspace = this.workspaces.get(workspaceId) + if (!workspace) return + + this.options.logger.info({ workspaceId, ...info }, "Workspace process exited") + + workspace.pid = undefined + workspace.port = undefined + workspace.updatedAt = new Date().toISOString() + + if (info.requested || info.code === 0) { + workspace.status = "stopped" + workspace.error = undefined + this.options.eventBus.publish({ type: "workspace.stopped", workspaceId }) + } else { + workspace.status = "error" + workspace.error = `Process exited with code ${info.code}` + this.options.eventBus.publish({ type: "workspace.error", workspace }) + } + } +} diff --git a/packages/server/src/workspaces/runtime.ts b/packages/server/src/workspaces/runtime.ts new file mode 100644 index 0000000..7c1c5f1 --- /dev/null +++ b/packages/server/src/workspaces/runtime.ts @@ -0,0 +1,241 @@ +import { ChildProcess, spawn } from "child_process" +import { existsSync, statSync } from "fs" +import path from "path" +import { EventBus } from "../events/bus" +import { LogLevel, WorkspaceLogEntry } from "../api-types" +import { Logger } from "../logger" + +interface LaunchOptions { + workspaceId: string + folder: string + binaryPath: string + environment?: Record + onExit?: (info: ProcessExitInfo) => void +} + +export interface ProcessExitInfo { + workspaceId: string + code: number | null + signal: NodeJS.Signals | null + requested: boolean +} + +interface ManagedProcess { + child: ChildProcess + requestedStop: boolean +} + +export class WorkspaceRuntime { + private processes = new Map() + + constructor(private readonly eventBus: EventBus, private readonly logger: Logger) {} + + async launch(options: LaunchOptions): Promise<{ pid: number; port: number; exitPromise: Promise; getLastOutput: () => string }> { + this.validateFolder(options.folder) + + const args = ["serve", "--port", "0", "--print-logs", "--log-level", "DEBUG"] + const env = { ...process.env, ...(options.environment ?? {}) } + + let exitResolve: ((info: ProcessExitInfo) => void) | null = null + const exitPromise = new Promise((resolveExit) => { + exitResolve = resolveExit + }) + let lastOutput = "" + + return new Promise((resolve, reject) => { + this.logger.info( + { workspaceId: options.workspaceId, folder: options.folder, binary: options.binaryPath }, + "Launching OpenCode process", + ) + const child = spawn(options.binaryPath, args, { + cwd: options.folder, + env, + stdio: ["ignore", "pipe", "pipe"], + }) + + const managed: ManagedProcess = { child, requestedStop: false } + this.processes.set(options.workspaceId, managed) + + let stdoutBuffer = "" + let stderrBuffer = "" + let portFound = false + + let warningTimer: NodeJS.Timeout | null = null + + const startWarningTimer = () => { + warningTimer = setInterval(() => { + this.logger.warn({ workspaceId: options.workspaceId }, "Workspace runtime has not reported a port yet") + }, 10000) + } + + const stopWarningTimer = () => { + if (warningTimer) { + clearInterval(warningTimer) + warningTimer = null + } + } + + startWarningTimer() + + const cleanupStreams = () => { + stopWarningTimer() + child.stdout?.removeAllListeners() + child.stderr?.removeAllListeners() + } + + const handleExit = (code: number | null, signal: NodeJS.Signals | null) => { + this.logger.info({ workspaceId: options.workspaceId, code, signal }, "OpenCode process exited") + this.processes.delete(options.workspaceId) + cleanupStreams() + child.removeListener("error", handleError) + child.removeListener("exit", handleExit) + const exitInfo: ProcessExitInfo = { + workspaceId: options.workspaceId, + code, + signal, + requested: managed.requestedStop, + } + if (exitResolve) { + exitResolve(exitInfo) + exitResolve = null + } + if (!portFound) { + const reason = stderrBuffer || `Process exited with code ${code}` + reject(new Error(reason)) + } else { + options.onExit?.(exitInfo) + } + } + + const handleError = (error: Error) => { + cleanupStreams() + child.removeListener("exit", handleExit) + this.processes.delete(options.workspaceId) + this.logger.error({ workspaceId: options.workspaceId, err: error }, "Workspace runtime error") + if (exitResolve) { + exitResolve({ workspaceId: options.workspaceId, code: null, signal: null, requested: managed.requestedStop }) + exitResolve = null + } + reject(error) + } + + child.on("error", handleError) + child.on("exit", handleExit) + + child.stdout?.on("data", (data: Buffer) => { + const text = data.toString() + stdoutBuffer += text + const lines = stdoutBuffer.split("\n") + stdoutBuffer = lines.pop() ?? "" + + for (const line of lines) { + const trimmed = line.trim() + if (!trimmed) continue + lastOutput = trimmed + this.emitLog(options.workspaceId, "info", line) + + if (!portFound) { + const portMatch = line.match(/opencode server listening on http:\/\/.+:(\d+)/i) + if (portMatch) { + portFound = true + child.removeListener("error", handleError) + const port = parseInt(portMatch[1], 10) + this.logger.info({ workspaceId: options.workspaceId, port }, "Workspace runtime allocated port") + const getLastOutput = () => lastOutput.trim() + resolve({ pid: child.pid!, port, exitPromise, getLastOutput }) + } + } + } + }) + + child.stderr?.on("data", (data: Buffer) => { + const text = data.toString() + stderrBuffer += text + const lines = stderrBuffer.split("\n") + stderrBuffer = lines.pop() ?? "" + + for (const line of lines) { + const trimmed = line.trim() + if (!trimmed) continue + lastOutput = `[stderr] ${trimmed}` + this.emitLog(options.workspaceId, "error", line) + } + }) + }) + } + + async stop(workspaceId: string): Promise { + const managed = this.processes.get(workspaceId) + if (!managed) return + + managed.requestedStop = true + const child = managed.child + this.logger.info({ workspaceId }, "Stopping OpenCode process") + + await new Promise((resolve, reject) => { + const cleanup = () => { + child.removeListener("exit", onExit) + child.removeListener("error", onError) + } + + const onExit = () => { + cleanup() + resolve() + } + const onError = (error: Error) => { + cleanup() + reject(error) + } + + const resolveIfAlreadyExited = () => { + if (child.exitCode !== null || child.signalCode !== null) { + this.logger.debug({ workspaceId, exitCode: child.exitCode, signal: child.signalCode }, "Process already exited") + cleanup() + resolve() + return true + } + return false + } + + child.once("exit", onExit) + child.once("error", onError) + + if (resolveIfAlreadyExited()) { + return + } + + this.logger.debug({ workspaceId }, "Sending SIGTERM to workspace process") + child.kill("SIGTERM") + setTimeout(() => { + if (!child.killed) { + this.logger.warn({ workspaceId }, "Process did not stop after SIGTERM, force killing") + child.kill("SIGKILL") + } else { + this.logger.debug({ workspaceId }, "Workspace process stopped gracefully before SIGKILL timeout") + } + }, 2000) + }) + } + + private emitLog(workspaceId: string, level: LogLevel, message: string) { + const entry: WorkspaceLogEntry = { + workspaceId, + timestamp: new Date().toISOString(), + level, + message: message.trim(), + } + + this.eventBus.publish({ type: "workspace.log", entry }) + } + + private validateFolder(folder: string) { + const resolved = path.resolve(folder) + if (!existsSync(resolved)) { + throw new Error(`Folder does not exist: ${resolved}`) + } + const stats = statSync(resolved) + if (!stats.isDirectory()) { + throw new Error(`Path is not a directory: ${resolved}`) + } + } +} diff --git a/packages/server/tsconfig.json b/packages/server/tsconfig.json new file mode 100644 index 0000000..5f9cd23 --- /dev/null +++ b/packages/server/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "Node", + "strict": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "outDir": "dist", + "rootDir": "src", + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/packages/tauri-app/.gitignore b/packages/tauri-app/.gitignore new file mode 100644 index 0000000..2bdc757 --- /dev/null +++ b/packages/tauri-app/.gitignore @@ -0,0 +1,7 @@ +src-tauri/target +src-tauri/Cargo.lock +src-tauri/resources/ +target +node_modules +dist +.DS_Store diff --git a/packages/tauri-app/Cargo.lock b/packages/tauri-app/Cargo.lock new file mode 100644 index 0000000..abffbd4 --- /dev/null +++ b/packages/tauri-app/Cargo.lock @@ -0,0 +1,5589 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "ashpd" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cbdf310d77fd3aaee6ea2093db7011dc2d35d2eb3481e5607f1f8d942ed99df" +dependencies = [ + "enumflags2", + "futures-channel", + "futures-util", + "rand 0.9.2", + "raw-window-handle", + "serde", + "serde_repr", + "tokio", + "url", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "zbus", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix 1.1.2", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix 1.1.2", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "async-signal" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 1.1.2", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2 0.6.3", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "brotli" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "bytemuck" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.10.0", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.17", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.8", +] + +[[package]] +name = "cc" +version = "1.2.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd405d82c84ff7f35739f175f67d8b9fb7687a0e84ccdc78bd3568839827cf07" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link 0.2.1", +] + +[[package]] +name = "codenomad-tauri" +version = "0.1.0" +dependencies = [ + "anyhow", + "dirs 5.0.1", + "libc", + "once_cell", + "parking_lot", + "regex", + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-dialog", + "tauri-plugin-opener", + "thiserror 1.0.69", + "url", + "which", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +dependencies = [ + "bitflags 2.10.0", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.10.0", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.29.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93d03419cb5950ccfd3daf3ff1c7a36ace64609a1a8746d493df1ca0afde0fa" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "matches", + "phf 0.10.1", + "proc-macro2", + "quote", + "smallvec", + "syn 1.0.109", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.110", +] + +[[package]] +name = "ctor" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +dependencies = [ + "quote", + "syn 2.0.110", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.110", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.110", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys 0.4.1", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys 0.5.0", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.4.6", + "windows-sys 0.48.0", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.5.2", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags 2.10.0", + "block2 0.6.2", + "libc", + "objc2 0.6.3", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading", +] + +[[package]] +name = "dlopen2" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b54f373ccf864bf587a89e880fb7610f8d73f3045f13580948ccbcaff26febff" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "788160fb30de9cdd857af31c6a2675904b16ece8fc2737b2c7127ba368c9d0f4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "embed-resource" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55a075fc573c64510038d7ee9abc7990635863992f83ebc52c8b433b8411a02e" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 0.9.8", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "endi" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + +[[package]] +name = "flate2" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.10.0", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "html5ever" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" +dependencies = [ + "log", + "mac", + "markup5ever", + "match_token", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98" +dependencies = [ + "byteorder", + "png", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "js-sys" +version = "0.3.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.10.0", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "kuchikiki" +version = "0.8.8-speedreader" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" +dependencies = [ + "cssparser", + "html5ever", + "indexmap 2.12.1", + "selectors", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libredox" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +dependencies = [ + "bitflags 2.10.0", + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "markup5ever" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" +dependencies = [ + "log", + "phf 0.11.3", + "phf_codegen 0.11.3", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "match_token" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.61.2", +] + +[[package]] +name = "muda" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "once_cell", + "png", + "serde", + "thiserror 2.0.17", + "windows-sys 0.60.2", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.10.0", + "jni-sys", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.10.0", + "block2 0.6.2", + "libc", + "objc2 0.6.3", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-text", + "objc2-core-video", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.10.0", + "dispatch2", + "objc2 0.6.3", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.10.0", + "dispatch2", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2 0.6.3", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-core-video" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-io-surface", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.10.0", + "block2 0.5.1", + "libc", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.10.0", + "block2 0.6.2", + "libc", + "objc2 0.6.3", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-javascript-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a1e6550c4caed348956ce3370c9ffeca70bb1dbed4fa96112e7c6170e074586" +dependencies = [ + "objc2 0.6.3", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.10.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.10.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-security" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.10.0", + "block2 0.6.2", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-javascript-core", + "objc2-security", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "open" +version = "5.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc" +dependencies = [ + "dunce", + "is-wsl", + "libc", + "pathdiff", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_shared 0.8.0", +] + +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_macros 0.10.0", + "phf_shared 0.10.0", + "proc-macro-hack", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared 0.8.0", + "rand 0.7.3", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand 0.8.5", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher 1.0.1", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plist" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +dependencies = [ + "base64 0.22.1", + "indexmap 2.12.1", + "quick-xml 0.38.4", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.1.2", + "windows-sys 0.61.2", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit 0.23.7", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", +] + +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", + "rand_pcg", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror 2.0.17", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "reqwest" +version = "0.12.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "rfd" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed" +dependencies = [ + "ashpd", + "block2 0.6.2", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys 0.11.0", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.110", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "selectors" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" +dependencies = [ + "bitflags 1.3.2", + "cssparser", + "derive_more", + "fxhash", + "log", + "phf 0.8.0", + "phf_codegen 0.8.0", + "precomputed-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10574371d41b0d9b2cff89418eda27da52bcaff2cc8741db26382a77c29131f1" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.12.1", + "schemars 0.9.0", + "schemars 1.1.0", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08a72d8216842fdd57820dc78d840bef99248e35fb2554ff923319e60f2d686b" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "servo_arc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52aa42f8fdf0fed91e5ce7f23d8138441002fa31dca008acf47e6fd4721f741" +dependencies = [ + "nodrop", + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +dependencies = [ + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08" +dependencies = [ + "bytemuck", + "cfg_aliases", + "core-graphics", + "foreign-types", + "js-sys", + "log", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-quartz-core 0.2.2", + "raw-window-handle", + "redox_syscall", + "wasm-bindgen", + "web-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.11.3", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "swift-rs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.34.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7" +dependencies = [ + "bitflags 2.10.0", + "block2 0.6.2", + "core-foundation", + "core-graphics", + "crossbeam-channel", + "dispatch", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "lazy_static", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-foundation 0.3.2", + "once_cell", + "parking_lot", + "raw-window-handle", + "scopeguard", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e492485dd390b35f7497401f67694f46161a2a00ffd800938d5dd3c898fb9d8" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs 6.0.0", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-foundation 0.3.2", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.17", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87d6f8cafe6a75514ce5333f115b7b1866e8e68d9672bf4ca89fc0f35697ea9d" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs 6.0.0", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "toml 0.9.8", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ef707148f0755110ca54377560ab891d722de4d53297595380a748026f139f" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.110", + "tauri-utils", + "thiserror 2.0.17", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71664fd715ee6e382c05345ad258d6d1d50f90cf1b58c0aa726638b33c2a075d" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.110", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "076c78a474a7247c90cad0b6e87e593c4c620ed4efdb79cbe0214f0021f6c39d" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "toml 0.9.8", + "walkdir", +] + +[[package]] +name = "tauri-plugin-dialog" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "313f8138692ddc4a2127c4c9607d616a46f5c042e77b3722450866da0aad2f19" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.17", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47df422695255ecbe7bac7012440eddaeefd026656171eac9559f5243d3230d9" +dependencies = [ + "anyhow", + "dunce", + "glob", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.17", + "toml 0.9.8", + "url", +] + +[[package]] +name = "tauri-plugin-opener" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c26b72571d25dee25667940027114e60f569fc3974f8cefbe50c2cbc5fd65e3b" +dependencies = [ + "dunce", + "glob", + "objc2-app-kit", + "objc2-foundation 0.3.2", + "open", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.17", + "url", + "windows", + "zbus", +] + +[[package]] +name = "tauri-runtime" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9368f09358496f2229313fccb37682ad116b7f46fa76981efe116994a0628926" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2 0.6.3", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.17", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "929f5df216f5c02a9e894554401bcdab6eec3e39ec6a4a7731c7067fc8688a93" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-foundation 0.3.2", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b8bbe426abdbf52d050e52ed693130dbd68375b9ad82a3fb17efb4c8d85673" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dunce", + "glob", + "html5ever", + "http", + "infer", + "json-patch", + "kuchikiki", + "log", + "memchr", + "phf 0.11.3", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.17", + "toml 0.9.8", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1087b111fe2b005e42dbdc1990fc18593234238d47453b0c99b7de1c9ab2c1e0" +dependencies = [ + "dunce", + "embed-resource", + "toml 0.9.8", +] + +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix 1.1.2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-util" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" +dependencies = [ + "indexmap 2.12.1", + "serde_core", + "serde_spanned 1.0.3", + "toml_datetime 0.7.3", + "toml_parser", + "toml_writer", + "winnow 0.7.13", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.12.1", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.12.1", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.23.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" +dependencies = [ + "indexmap 2.12.1", + "toml_datetime 0.7.3", + "toml_parser", + "winnow 0.7.13", +] + +[[package]] +name = "toml_parser" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +dependencies = [ + "winnow 0.7.13", +] + +[[package]] +name = "toml_writer" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags 2.10.0", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d5572781bee8e3f994d7467084e1b1fd7a93ce66bd480f8156ba89dee55a2b" +dependencies = [ + "crossbeam-channel", + "dirs 6.0.0", + "libappindicator", + "muda", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "once_cell", + "png", + "serde", + "thiserror 2.0.17", + "windows-sys 0.60.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "uds_windows" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +dependencies = [ + "memoffset", + "tempfile", + "winapi", +] + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +dependencies = [ + "getrandom 0.3.4", + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.110", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wayland-backend" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35" +dependencies = [ + "cc", + "downcast-rs", + "rustix 1.1.2", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" +dependencies = [ + "bitflags 2.10.0", + "rustix 1.1.2", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" +dependencies = [ + "bitflags 2.10.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" +dependencies = [ + "proc-macro2", + "quick-xml 0.37.5", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34949b42822155826b41db8e5d0c1be3a2bd296c747577a43a3e6daefc296142" +dependencies = [ + "dlib", + "log", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76b1bc1e54c581da1e9f179d0b38512ba358fb1af2d634a1affe42e37172361a" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62daa38afc514d1f8f12b8693d30d5993ff77ced33ce30cd04deebc267a6d57c" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webview2-com" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ba622a989277ef3886dd5afb3e280e3dd6d974b766118950a08f8f678ad6a4" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c" +dependencies = [ + "thiserror 2.0.17", + "windows", + "windows-core 0.61.2", +] + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2 0.6.3", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "wry" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728b7d4c8ec8d81cab295e0b5b8a4c263c0d41a785fb8f8c4df284e5411140a2" +dependencies = [ + "base64 0.22.1", + "block2 0.6.2", + "cookie", + "crossbeam-channel", + "dirs 6.0.0", + "dpi", + "dunce", + "gdkx11", + "gtk", + "html5ever", + "http", + "javascriptcore-rs", + "jni", + "kuchikiki", + "libc", + "ndk", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.17", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b622b18155f7a93d1cd2dc8c01d2d6a44e08fb9ebb7b3f9e6ed101488bad6c91" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "nix", + "ordered-stream", + "serde", + "serde_repr", + "tokio", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 0.7.13", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cdb94821ca8a87ca9c298b5d1cbd80e2a8b67115d99f6e4551ac49e42b6a314" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.110", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" +dependencies = [ + "serde", + "static_assertions", + "winnow 0.7.13", + "zvariant", +] + +[[package]] +name = "zerocopy" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43fa6694ed34d6e57407afbccdeecfa268c470a7d2a5b0cf49ce9fcc345afb90" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c640b22cd9817fae95be82f0d2f90b11f7605f6c319d16705c459b27ac2cbc26" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "zvariant" +version = "5.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2be61892e4f2b1772727be11630a62664a1826b62efa43a6fe7449521cb8744c" +dependencies = [ + "endi", + "enumflags2", + "serde", + "url", + "winnow 0.7.13", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da58575a1b2b20766513b1ec59d8e2e68db2745379f961f86650655e862d2006" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.110", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6949d142f89f6916deca2232cf26a8afacf2b9fdc35ce766105e104478be599" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.110", + "winnow 0.7.13", +] diff --git a/packages/tauri-app/Cargo.toml b/packages/tauri-app/Cargo.toml new file mode 100644 index 0000000..5322fc2 --- /dev/null +++ b/packages/tauri-app/Cargo.toml @@ -0,0 +1,3 @@ +[workspace] +members = ["src-tauri"] +resolver = "2" diff --git a/packages/tauri-app/package.json b/packages/tauri-app/package.json new file mode 100644 index 0000000..1d16d5e --- /dev/null +++ b/packages/tauri-app/package.json @@ -0,0 +1,17 @@ +{ + "name": "@codenomad/tauri-app", + "version": "0.4.0", + "private": true, + "scripts": { + "dev": "npx --yes @tauri-apps/cli@^2.9.4 dev", + "dev:ui": "npm run dev --workspace @codenomad/ui", + "dev:prep": "node ./scripts/dev-prep.js", + "dev:bootstrap": "npm run dev:prep && npm run dev:ui", + "prebuild": "node ./scripts/prebuild.js", + "bundle:server": "npm run prebuild", + "build": "npx --yes @tauri-apps/cli@^2.9.4 build" + }, + "devDependencies": { + "@tauri-apps/cli": "^2.9.4" + } +} diff --git a/packages/tauri-app/scripts/dev-prep.js b/packages/tauri-app/scripts/dev-prep.js new file mode 100644 index 0000000..abbb4c6 --- /dev/null +++ b/packages/tauri-app/scripts/dev-prep.js @@ -0,0 +1,46 @@ +#!/usr/bin/env node + +const fs = require("fs") +const path = require("path") +const { execSync } = require("child_process") + +const root = path.resolve(__dirname, "..") +const workspaceRoot = path.resolve(root, "..", "..") +const uiRoot = path.resolve(root, "..", "ui") +const uiDist = path.resolve(uiRoot, "src", "renderer", "dist") +const uiLoadingDest = path.resolve(root, "src-tauri", "resources", "ui-loading") + +function ensureUiBuild() { + const loadingHtml = path.join(uiDist, "loading.html") + if (fs.existsSync(loadingHtml)) { + return + } + + console.log("[dev-prep] UI loader build missing; running workspace build…") + execSync("npm --workspace @codenomad/ui run build", { + cwd: workspaceRoot, + stdio: "inherit", + }) + + if (!fs.existsSync(loadingHtml)) { + throw new Error("[dev-prep] failed to produce loading.html after UI build") + } +} + +function copyUiLoadingAssets() { + const loadingSource = path.join(uiDist, "loading.html") + const assetsSource = path.join(uiDist, "assets") + + fs.rmSync(uiLoadingDest, { recursive: true, force: true }) + fs.mkdirSync(uiLoadingDest, { recursive: true }) + + fs.copyFileSync(loadingSource, path.join(uiLoadingDest, "loading.html")) + if (fs.existsSync(assetsSource)) { + fs.cpSync(assetsSource, path.join(uiLoadingDest, "assets"), { recursive: true }) + } + + console.log(`[dev-prep] copied loader bundle from ${uiDist}`) +} + +ensureUiBuild() +copyUiLoadingAssets() diff --git a/packages/tauri-app/scripts/prebuild.js b/packages/tauri-app/scripts/prebuild.js new file mode 100644 index 0000000..c396892 --- /dev/null +++ b/packages/tauri-app/scripts/prebuild.js @@ -0,0 +1,195 @@ +#!/usr/bin/env node +const fs = require("fs") +const path = require("path") +const { execSync } = require("child_process") + +const root = path.resolve(__dirname, "..") +const workspaceRoot = path.resolve(root, "..", "..") +const serverRoot = path.resolve(root, "..", "server") +const uiRoot = path.resolve(root, "..", "ui") +const uiDist = path.resolve(uiRoot, "src", "renderer", "dist") +const serverDest = path.resolve(root, "src-tauri", "resources", "server") +const uiLoadingDest = path.resolve(root, "src-tauri", "resources", "ui-loading") + +const sources = ["dist", "public", "node_modules", "package.json"] + +const serverInstallCommand = + "npm install --omit=dev --ignore-scripts --workspaces=false --package-lock=false --install-strategy=shallow --fund=false --audit=false" +const serverDevInstallCommand = + "npm install --workspace @neuralnomads/codenomad --include-workspace-root=false --install-strategy=nested --fund=false --audit=false" +const uiDevInstallCommand = + "npm install --workspace @codenomad/ui --include-workspace-root=false --install-strategy=nested --fund=false --audit=false" + +const envWithRootBin = { + ...process.env, + PATH: `${path.join(workspaceRoot, "node_modules/.bin")}:${process.env.PATH}`, +} + +const braceExpansionPath = path.join( + serverRoot, + "node_modules", + "@fastify", + "static", + "node_modules", + "brace-expansion", + "package.json", +) + +const viteBinPath = path.join(uiRoot, "node_modules", ".bin", "vite") + +function ensureServerBuild() { + const distPath = path.join(serverRoot, "dist") + const publicPath = path.join(serverRoot, "public") + if (fs.existsSync(distPath) && fs.existsSync(publicPath)) { + return + } + + console.log("[prebuild] server build missing; running workspace build...") + execSync("npm --workspace @neuralnomads/codenomad run build", { + cwd: workspaceRoot, + stdio: "inherit", + env: { + ...process.env, + PATH: `${path.join(workspaceRoot, "node_modules/.bin")}:${process.env.PATH}`, + }, + }) + + if (!fs.existsSync(distPath) || !fs.existsSync(publicPath)) { + throw new Error("[prebuild] server artifacts still missing after build") + } +} + +function ensureUiBuild() { + const loadingHtml = path.join(uiDist, "loading.html") + if (fs.existsSync(loadingHtml)) { + return + } + + console.log("[prebuild] ui build missing; running workspace build...") + execSync("npm --workspace @codenomad/ui run build", { + cwd: workspaceRoot, + stdio: "inherit", + }) + + if (!fs.existsSync(loadingHtml)) { + throw new Error("[prebuild] ui loading assets missing after build") + } +} + +function ensureServerDevDependencies() { + if (fs.existsSync(braceExpansionPath)) { + return + } + + console.log("[prebuild] ensuring server build dependencies (with dev)...") + execSync(serverDevInstallCommand, { + cwd: workspaceRoot, + stdio: "inherit", + env: envWithRootBin, + }) +} + +function ensureServerDependencies() { + if (fs.existsSync(braceExpansionPath)) { + return + } + + console.log("[prebuild] ensuring server production dependencies...") + execSync(serverInstallCommand, { + cwd: serverRoot, + stdio: "inherit", + }) +} + +function ensureUiDevDependencies() { + if (fs.existsSync(viteBinPath)) { + return + } + + console.log("[prebuild] ensuring ui build dependencies...") + execSync(uiDevInstallCommand, { + cwd: workspaceRoot, + stdio: "inherit", + env: envWithRootBin, + }) +} + +function ensureRollupPlatformBinary() { + const platformKey = `${process.platform}-${process.arch}` + const platformPackages = { + "linux-x64": "@rollup/rollup-linux-x64-gnu", + "linux-arm64": "@rollup/rollup-linux-arm64-gnu", + "darwin-arm64": "@rollup/rollup-darwin-arm64", + "darwin-x64": "@rollup/rollup-darwin-x64", + "win32-x64": "@rollup/rollup-win32-x64-msvc", + } + + const pkgName = platformPackages[platformKey] + if (!pkgName) { + return + } + + const platformPackagePath = path.join(workspaceRoot, "node_modules", "@rollup", pkgName.split("/").pop()) + if (fs.existsSync(platformPackagePath)) { + return + } + + let rollupVersion = "" + try { + rollupVersion = require(path.join(workspaceRoot, "node_modules", "rollup", "package.json")).version + } catch (error) { + // leave version empty; fallback install will use latest compatible + } + + const packageSpec = rollupVersion ? `${pkgName}@${rollupVersion}` : pkgName + + console.log("[prebuild] installing rollup platform binary (optional dep workaround)...") + execSync(`npm install ${packageSpec} --no-save --ignore-scripts --fund=false --audit=false`, { + cwd: workspaceRoot, + stdio: "inherit", + }) +} + +function copyServerArtifacts() { + fs.rmSync(serverDest, { recursive: true, force: true }) + fs.mkdirSync(serverDest, { recursive: true }) + + for (const name of sources) { + const from = path.join(serverRoot, name) + const to = path.join(serverDest, name) + if (!fs.existsSync(from)) { + console.warn(`[prebuild] skipped missing ${from}`) + continue + } + fs.cpSync(from, to, { recursive: true, dereference: true }) + console.log(`[prebuild] copied ${from} -> ${to}`) + } +} + +function copyUiLoadingAssets() { + const loadingSource = path.join(uiDist, "loading.html") + const assetsSource = path.join(uiDist, "assets") + + if (!fs.existsSync(loadingSource)) { + throw new Error("[prebuild] cannot find built loading.html") + } + + fs.rmSync(uiLoadingDest, { recursive: true, force: true }) + fs.mkdirSync(uiLoadingDest, { recursive: true }) + + fs.copyFileSync(loadingSource, path.join(uiLoadingDest, "loading.html")) + if (fs.existsSync(assetsSource)) { + fs.cpSync(assetsSource, path.join(uiLoadingDest, "assets"), { recursive: true }) + } + + console.log(`[prebuild] prepared UI loading assets from ${uiDist}`) +} + +ensureServerDevDependencies() +ensureUiDevDependencies() +ensureRollupPlatformBinary() +ensureServerDependencies() +ensureServerBuild() +ensureUiBuild() +copyServerArtifacts() +copyUiLoadingAssets() diff --git a/packages/tauri-app/src-tauri/Cargo.toml b/packages/tauri-app/src-tauri/Cargo.toml new file mode 100644 index 0000000..11e770a --- /dev/null +++ b/packages/tauri-app/src-tauri/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "codenomad-tauri" +version = "0.1.0" +edition = "2021" + +[build-dependencies] +tauri-build = { version = "2.5.2", features = [] } + +[dependencies] +tauri = { version = "2.5.2", features = [ "devtools"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +regex = "1" +once_cell = "1" +parking_lot = "0.12" +thiserror = "1" +anyhow = "1" +which = "4" +libc = "0.2" +tauri-plugin-dialog = "2" +dirs = "5" +tauri-plugin-opener = "2" +url = "2" diff --git a/packages/tauri-app/src-tauri/build.rs b/packages/tauri-app/src-tauri/build.rs new file mode 100644 index 0000000..d860e1e --- /dev/null +++ b/packages/tauri-app/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/packages/tauri-app/src-tauri/capabilities/main-window.json b/packages/tauri-app/src-tauri/capabilities/main-window.json new file mode 100644 index 0000000..61d6e34 --- /dev/null +++ b/packages/tauri-app/src-tauri/capabilities/main-window.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://schema.tauri.app/capabilities.json", + "identifier": "main-window-native-dialogs", + "description": "Grant the main window access to required core features and native dialog commands.", + "remote": { + "urls": ["http://127.0.0.1:*", "http://localhost:*"] + }, + "windows": ["main"], + "permissions": [ + "core:default", + "core:menu:default", + "dialog:allow-open", + "opener:allow-default-urls", + "core:webview:allow-set-webview-zoom" + ] +} diff --git a/packages/tauri-app/src-tauri/gen/schemas/acl-manifests.json b/packages/tauri-app/src-tauri/gen/schemas/acl-manifests.json new file mode 100644 index 0000000..60e4f04 --- /dev/null +++ b/packages/tauri-app/src-tauri/gen/schemas/acl-manifests.json @@ -0,0 +1 @@ +{"core":{"default_permission":{"identifier":"default","description":"Default core plugins set.","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version","allow-identifier","allow-bundle-type","allow-register-listener","allow-remove-listener"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-bundle-type":{"identifier":"allow-bundle-type","description":"Enables the bundle_type command without any pre-configured scope.","commands":{"allow":["bundle_type"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-fetch-data-store-identifiers":{"identifier":"allow-fetch-data-store-identifiers","description":"Enables the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":["fetch_data_store_identifiers"],"deny":[]}},"allow-identifier":{"identifier":"allow-identifier","description":"Enables the identifier command without any pre-configured scope.","commands":{"allow":["identifier"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-data-store":{"identifier":"allow-remove-data-store","description":"Enables the remove_data_store command without any pre-configured scope.","commands":{"allow":["remove_data_store"],"deny":[]}},"allow-remove-listener":{"identifier":"allow-remove-listener","description":"Enables the remove_listener command without any pre-configured scope.","commands":{"allow":["remove_listener"],"deny":[]}},"allow-set-app-theme":{"identifier":"allow-set-app-theme","description":"Enables the set_app_theme command without any pre-configured scope.","commands":{"allow":["set_app_theme"],"deny":[]}},"allow-set-dock-visibility":{"identifier":"allow-set-dock-visibility","description":"Enables the set_dock_visibility command without any pre-configured scope.","commands":{"allow":["set_dock_visibility"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-bundle-type":{"identifier":"deny-bundle-type","description":"Denies the bundle_type command without any pre-configured scope.","commands":{"allow":[],"deny":["bundle_type"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-fetch-data-store-identifiers":{"identifier":"deny-fetch-data-store-identifiers","description":"Denies the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":[],"deny":["fetch_data_store_identifiers"]}},"deny-identifier":{"identifier":"deny-identifier","description":"Denies the identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["identifier"]}},"deny-name":{"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]}},"deny-register-listener":{"identifier":"deny-register-listener","description":"Denies the register_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["register_listener"]}},"deny-remove-data-store":{"identifier":"deny-remove-data-store","description":"Denies the remove_data_store command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_data_store"]}},"deny-remove-listener":{"identifier":"deny-remove-listener","description":"Denies the remove_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_listener"]}},"deny-set-app-theme":{"identifier":"deny-set-app-theme","description":"Denies the set_app_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_app_theme"]}},"deny-set-dock-visibility":{"identifier":"deny-set-dock-visibility","description":"Denies the set_dock_visibility command without any pre-configured scope.","commands":{"allow":[],"deny":["set_dock_visibility"]}},"deny-tauri-version":{"identifier":"deny-tauri-version","description":"Denies the tauri_version command without any pre-configured scope.","commands":{"allow":[],"deny":["tauri_version"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"core:event":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-listen","allow-unlisten","allow-emit","allow-emit-to"]},"permissions":{"allow-emit":{"identifier":"allow-emit","description":"Enables the emit command without any pre-configured scope.","commands":{"allow":["emit"],"deny":[]}},"allow-emit-to":{"identifier":"allow-emit-to","description":"Enables the emit_to command without any pre-configured scope.","commands":{"allow":["emit_to"],"deny":[]}},"allow-listen":{"identifier":"allow-listen","description":"Enables the listen command without any pre-configured scope.","commands":{"allow":["listen"],"deny":[]}},"allow-unlisten":{"identifier":"allow-unlisten","description":"Enables the unlisten command without any pre-configured scope.","commands":{"allow":["unlisten"],"deny":[]}},"deny-emit":{"identifier":"deny-emit","description":"Denies the emit command without any pre-configured scope.","commands":{"allow":[],"deny":["emit"]}},"deny-emit-to":{"identifier":"deny-emit-to","description":"Denies the emit_to command without any pre-configured scope.","commands":{"allow":[],"deny":["emit_to"]}},"deny-listen":{"identifier":"deny-listen","description":"Denies the listen command without any pre-configured scope.","commands":{"allow":[],"deny":["listen"]}},"deny-unlisten":{"identifier":"deny-unlisten","description":"Denies the unlisten command without any pre-configured scope.","commands":{"allow":[],"deny":["unlisten"]}}},"permission_sets":{},"global_scope_schema":null},"core:image":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-from-bytes","allow-from-path","allow-rgba","allow-size"]},"permissions":{"allow-from-bytes":{"identifier":"allow-from-bytes","description":"Enables the from_bytes command without any pre-configured scope.","commands":{"allow":["from_bytes"],"deny":[]}},"allow-from-path":{"identifier":"allow-from-path","description":"Enables the from_path command without any pre-configured scope.","commands":{"allow":["from_path"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-rgba":{"identifier":"allow-rgba","description":"Enables the rgba command without any pre-configured scope.","commands":{"allow":["rgba"],"deny":[]}},"allow-size":{"identifier":"allow-size","description":"Enables the size command without any pre-configured scope.","commands":{"allow":["size"],"deny":[]}},"deny-from-bytes":{"identifier":"deny-from-bytes","description":"Denies the from_bytes command without any pre-configured scope.","commands":{"allow":[],"deny":["from_bytes"]}},"deny-from-path":{"identifier":"deny-from-path","description":"Denies the from_path command without any pre-configured scope.","commands":{"allow":[],"deny":["from_path"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-rgba":{"identifier":"deny-rgba","description":"Denies the rgba command without any pre-configured scope.","commands":{"allow":[],"deny":["rgba"]}},"deny-size":{"identifier":"deny-size","description":"Denies the size command without any pre-configured scope.","commands":{"allow":[],"deny":["size"]}}},"permission_sets":{},"global_scope_schema":null},"core:menu":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-append","allow-prepend","allow-insert","allow-remove","allow-remove-at","allow-items","allow-get","allow-popup","allow-create-default","allow-set-as-app-menu","allow-set-as-window-menu","allow-text","allow-set-text","allow-is-enabled","allow-set-enabled","allow-set-accelerator","allow-set-as-windows-menu-for-nsapp","allow-set-as-help-menu-for-nsapp","allow-is-checked","allow-set-checked","allow-set-icon"]},"permissions":{"allow-append":{"identifier":"allow-append","description":"Enables the append command without any pre-configured scope.","commands":{"allow":["append"],"deny":[]}},"allow-create-default":{"identifier":"allow-create-default","description":"Enables the create_default command without any pre-configured scope.","commands":{"allow":["create_default"],"deny":[]}},"allow-get":{"identifier":"allow-get","description":"Enables the get command without any pre-configured scope.","commands":{"allow":["get"],"deny":[]}},"allow-insert":{"identifier":"allow-insert","description":"Enables the insert command without any pre-configured scope.","commands":{"allow":["insert"],"deny":[]}},"allow-is-checked":{"identifier":"allow-is-checked","description":"Enables the is_checked command without any pre-configured scope.","commands":{"allow":["is_checked"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-items":{"identifier":"allow-items","description":"Enables the items command without any pre-configured scope.","commands":{"allow":["items"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-popup":{"identifier":"allow-popup","description":"Enables the popup command without any pre-configured scope.","commands":{"allow":["popup"],"deny":[]}},"allow-prepend":{"identifier":"allow-prepend","description":"Enables the prepend command without any pre-configured scope.","commands":{"allow":["prepend"],"deny":[]}},"allow-remove":{"identifier":"allow-remove","description":"Enables the remove command without any pre-configured scope.","commands":{"allow":["remove"],"deny":[]}},"allow-remove-at":{"identifier":"allow-remove-at","description":"Enables the remove_at command without any pre-configured scope.","commands":{"allow":["remove_at"],"deny":[]}},"allow-set-accelerator":{"identifier":"allow-set-accelerator","description":"Enables the set_accelerator command without any pre-configured scope.","commands":{"allow":["set_accelerator"],"deny":[]}},"allow-set-as-app-menu":{"identifier":"allow-set-as-app-menu","description":"Enables the set_as_app_menu command without any pre-configured scope.","commands":{"allow":["set_as_app_menu"],"deny":[]}},"allow-set-as-help-menu-for-nsapp":{"identifier":"allow-set-as-help-menu-for-nsapp","description":"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_help_menu_for_nsapp"],"deny":[]}},"allow-set-as-window-menu":{"identifier":"allow-set-as-window-menu","description":"Enables the set_as_window_menu command without any pre-configured scope.","commands":{"allow":["set_as_window_menu"],"deny":[]}},"allow-set-as-windows-menu-for-nsapp":{"identifier":"allow-set-as-windows-menu-for-nsapp","description":"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_windows_menu_for_nsapp"],"deny":[]}},"allow-set-checked":{"identifier":"allow-set-checked","description":"Enables the set_checked command without any pre-configured scope.","commands":{"allow":["set_checked"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-text":{"identifier":"allow-set-text","description":"Enables the set_text command without any pre-configured scope.","commands":{"allow":["set_text"],"deny":[]}},"allow-text":{"identifier":"allow-text","description":"Enables the text command without any pre-configured scope.","commands":{"allow":["text"],"deny":[]}},"deny-append":{"identifier":"deny-append","description":"Denies the append command without any pre-configured scope.","commands":{"allow":[],"deny":["append"]}},"deny-create-default":{"identifier":"deny-create-default","description":"Denies the create_default command without any pre-configured scope.","commands":{"allow":[],"deny":["create_default"]}},"deny-get":{"identifier":"deny-get","description":"Denies the get command without any pre-configured scope.","commands":{"allow":[],"deny":["get"]}},"deny-insert":{"identifier":"deny-insert","description":"Denies the insert command without any pre-configured scope.","commands":{"allow":[],"deny":["insert"]}},"deny-is-checked":{"identifier":"deny-is-checked","description":"Denies the is_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["is_checked"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-items":{"identifier":"deny-items","description":"Denies the items command without any pre-configured scope.","commands":{"allow":[],"deny":["items"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-popup":{"identifier":"deny-popup","description":"Denies the popup command without any pre-configured scope.","commands":{"allow":[],"deny":["popup"]}},"deny-prepend":{"identifier":"deny-prepend","description":"Denies the prepend command without any pre-configured scope.","commands":{"allow":[],"deny":["prepend"]}},"deny-remove":{"identifier":"deny-remove","description":"Denies the remove command without any pre-configured scope.","commands":{"allow":[],"deny":["remove"]}},"deny-remove-at":{"identifier":"deny-remove-at","description":"Denies the remove_at command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_at"]}},"deny-set-accelerator":{"identifier":"deny-set-accelerator","description":"Denies the set_accelerator command without any pre-configured scope.","commands":{"allow":[],"deny":["set_accelerator"]}},"deny-set-as-app-menu":{"identifier":"deny-set-as-app-menu","description":"Denies the set_as_app_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_app_menu"]}},"deny-set-as-help-menu-for-nsapp":{"identifier":"deny-set-as-help-menu-for-nsapp","description":"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_help_menu_for_nsapp"]}},"deny-set-as-window-menu":{"identifier":"deny-set-as-window-menu","description":"Denies the set_as_window_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_window_menu"]}},"deny-set-as-windows-menu-for-nsapp":{"identifier":"deny-set-as-windows-menu-for-nsapp","description":"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_windows_menu_for_nsapp"]}},"deny-set-checked":{"identifier":"deny-set-checked","description":"Denies the set_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["set_checked"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-text":{"identifier":"deny-set-text","description":"Denies the set_text command without any pre-configured scope.","commands":{"allow":[],"deny":["set_text"]}},"deny-text":{"identifier":"deny-text","description":"Denies the text command without any pre-configured scope.","commands":{"allow":[],"deny":["text"]}}},"permission_sets":{},"global_scope_schema":null},"core:path":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-resolve-directory","allow-resolve","allow-normalize","allow-join","allow-dirname","allow-extname","allow-basename","allow-is-absolute"]},"permissions":{"allow-basename":{"identifier":"allow-basename","description":"Enables the basename command without any pre-configured scope.","commands":{"allow":["basename"],"deny":[]}},"allow-dirname":{"identifier":"allow-dirname","description":"Enables the dirname command without any pre-configured scope.","commands":{"allow":["dirname"],"deny":[]}},"allow-extname":{"identifier":"allow-extname","description":"Enables the extname command without any pre-configured scope.","commands":{"allow":["extname"],"deny":[]}},"allow-is-absolute":{"identifier":"allow-is-absolute","description":"Enables the is_absolute command without any pre-configured scope.","commands":{"allow":["is_absolute"],"deny":[]}},"allow-join":{"identifier":"allow-join","description":"Enables the join command without any pre-configured scope.","commands":{"allow":["join"],"deny":[]}},"allow-normalize":{"identifier":"allow-normalize","description":"Enables the normalize command without any pre-configured scope.","commands":{"allow":["normalize"],"deny":[]}},"allow-resolve":{"identifier":"allow-resolve","description":"Enables the resolve command without any pre-configured scope.","commands":{"allow":["resolve"],"deny":[]}},"allow-resolve-directory":{"identifier":"allow-resolve-directory","description":"Enables the resolve_directory command without any pre-configured scope.","commands":{"allow":["resolve_directory"],"deny":[]}},"deny-basename":{"identifier":"deny-basename","description":"Denies the basename command without any pre-configured scope.","commands":{"allow":[],"deny":["basename"]}},"deny-dirname":{"identifier":"deny-dirname","description":"Denies the dirname command without any pre-configured scope.","commands":{"allow":[],"deny":["dirname"]}},"deny-extname":{"identifier":"deny-extname","description":"Denies the extname command without any pre-configured scope.","commands":{"allow":[],"deny":["extname"]}},"deny-is-absolute":{"identifier":"deny-is-absolute","description":"Denies the is_absolute command without any pre-configured scope.","commands":{"allow":[],"deny":["is_absolute"]}},"deny-join":{"identifier":"deny-join","description":"Denies the join command without any pre-configured scope.","commands":{"allow":[],"deny":["join"]}},"deny-normalize":{"identifier":"deny-normalize","description":"Denies the normalize command without any pre-configured scope.","commands":{"allow":[],"deny":["normalize"]}},"deny-resolve":{"identifier":"deny-resolve","description":"Denies the resolve command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve"]}},"deny-resolve-directory":{"identifier":"deny-resolve-directory","description":"Denies the resolve_directory command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve_directory"]}}},"permission_sets":{},"global_scope_schema":null},"core:resources":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-close"]},"permissions":{"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}}},"permission_sets":{},"global_scope_schema":null},"core:tray":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-get-by-id","allow-remove-by-id","allow-set-icon","allow-set-menu","allow-set-tooltip","allow-set-title","allow-set-visible","allow-set-temp-dir-path","allow-set-icon-as-template","allow-set-show-menu-on-left-click"]},"permissions":{"allow-get-by-id":{"identifier":"allow-get-by-id","description":"Enables the get_by_id command without any pre-configured scope.","commands":{"allow":["get_by_id"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-remove-by-id":{"identifier":"allow-remove-by-id","description":"Enables the remove_by_id command without any pre-configured scope.","commands":{"allow":["remove_by_id"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-icon-as-template":{"identifier":"allow-set-icon-as-template","description":"Enables the set_icon_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_as_template"],"deny":[]}},"allow-set-menu":{"identifier":"allow-set-menu","description":"Enables the set_menu command without any pre-configured scope.","commands":{"allow":["set_menu"],"deny":[]}},"allow-set-show-menu-on-left-click":{"identifier":"allow-set-show-menu-on-left-click","description":"Enables the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":["set_show_menu_on_left_click"],"deny":[]}},"allow-set-temp-dir-path":{"identifier":"allow-set-temp-dir-path","description":"Enables the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":["set_temp_dir_path"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-tooltip":{"identifier":"allow-set-tooltip","description":"Enables the set_tooltip command without any pre-configured scope.","commands":{"allow":["set_tooltip"],"deny":[]}},"allow-set-visible":{"identifier":"allow-set-visible","description":"Enables the set_visible command without any pre-configured scope.","commands":{"allow":["set_visible"],"deny":[]}},"deny-get-by-id":{"identifier":"deny-get-by-id","description":"Denies the get_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["get_by_id"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-remove-by-id":{"identifier":"deny-remove-by-id","description":"Denies the remove_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_by_id"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-icon-as-template":{"identifier":"deny-set-icon-as-template","description":"Denies the set_icon_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_as_template"]}},"deny-set-menu":{"identifier":"deny-set-menu","description":"Denies the set_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_menu"]}},"deny-set-show-menu-on-left-click":{"identifier":"deny-set-show-menu-on-left-click","description":"Denies the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":[],"deny":["set_show_menu_on_left_click"]}},"deny-set-temp-dir-path":{"identifier":"deny-set-temp-dir-path","description":"Denies the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":[],"deny":["set_temp_dir_path"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-tooltip":{"identifier":"deny-set-tooltip","description":"Denies the set_tooltip command without any pre-configured scope.","commands":{"allow":[],"deny":["set_tooltip"]}},"deny-set-visible":{"identifier":"deny-set-visible","description":"Denies the set_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible"]}}},"permission_sets":{},"global_scope_schema":null},"core:webview":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-webviews","allow-webview-position","allow-webview-size","allow-internal-toggle-devtools"]},"permissions":{"allow-clear-all-browsing-data":{"identifier":"allow-clear-all-browsing-data","description":"Enables the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":["clear_all_browsing_data"],"deny":[]}},"allow-create-webview":{"identifier":"allow-create-webview","description":"Enables the create_webview command without any pre-configured scope.","commands":{"allow":["create_webview"],"deny":[]}},"allow-create-webview-window":{"identifier":"allow-create-webview-window","description":"Enables the create_webview_window command without any pre-configured scope.","commands":{"allow":["create_webview_window"],"deny":[]}},"allow-get-all-webviews":{"identifier":"allow-get-all-webviews","description":"Enables the get_all_webviews command without any pre-configured scope.","commands":{"allow":["get_all_webviews"],"deny":[]}},"allow-internal-toggle-devtools":{"identifier":"allow-internal-toggle-devtools","description":"Enables the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":["internal_toggle_devtools"],"deny":[]}},"allow-print":{"identifier":"allow-print","description":"Enables the print command without any pre-configured scope.","commands":{"allow":["print"],"deny":[]}},"allow-reparent":{"identifier":"allow-reparent","description":"Enables the reparent command without any pre-configured scope.","commands":{"allow":["reparent"],"deny":[]}},"allow-set-webview-auto-resize":{"identifier":"allow-set-webview-auto-resize","description":"Enables the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":["set_webview_auto_resize"],"deny":[]}},"allow-set-webview-background-color":{"identifier":"allow-set-webview-background-color","description":"Enables the set_webview_background_color command without any pre-configured scope.","commands":{"allow":["set_webview_background_color"],"deny":[]}},"allow-set-webview-focus":{"identifier":"allow-set-webview-focus","description":"Enables the set_webview_focus command without any pre-configured scope.","commands":{"allow":["set_webview_focus"],"deny":[]}},"allow-set-webview-position":{"identifier":"allow-set-webview-position","description":"Enables the set_webview_position command without any pre-configured scope.","commands":{"allow":["set_webview_position"],"deny":[]}},"allow-set-webview-size":{"identifier":"allow-set-webview-size","description":"Enables the set_webview_size command without any pre-configured scope.","commands":{"allow":["set_webview_size"],"deny":[]}},"allow-set-webview-zoom":{"identifier":"allow-set-webview-zoom","description":"Enables the set_webview_zoom command without any pre-configured scope.","commands":{"allow":["set_webview_zoom"],"deny":[]}},"allow-webview-close":{"identifier":"allow-webview-close","description":"Enables the webview_close command without any pre-configured scope.","commands":{"allow":["webview_close"],"deny":[]}},"allow-webview-hide":{"identifier":"allow-webview-hide","description":"Enables the webview_hide command without any pre-configured scope.","commands":{"allow":["webview_hide"],"deny":[]}},"allow-webview-position":{"identifier":"allow-webview-position","description":"Enables the webview_position command without any pre-configured scope.","commands":{"allow":["webview_position"],"deny":[]}},"allow-webview-show":{"identifier":"allow-webview-show","description":"Enables the webview_show command without any pre-configured scope.","commands":{"allow":["webview_show"],"deny":[]}},"allow-webview-size":{"identifier":"allow-webview-size","description":"Enables the webview_size command without any pre-configured scope.","commands":{"allow":["webview_size"],"deny":[]}},"deny-clear-all-browsing-data":{"identifier":"deny-clear-all-browsing-data","description":"Denies the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":[],"deny":["clear_all_browsing_data"]}},"deny-create-webview":{"identifier":"deny-create-webview","description":"Denies the create_webview command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview"]}},"deny-create-webview-window":{"identifier":"deny-create-webview-window","description":"Denies the create_webview_window command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview_window"]}},"deny-get-all-webviews":{"identifier":"deny-get-all-webviews","description":"Denies the get_all_webviews command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_webviews"]}},"deny-internal-toggle-devtools":{"identifier":"deny-internal-toggle-devtools","description":"Denies the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_devtools"]}},"deny-print":{"identifier":"deny-print","description":"Denies the print command without any pre-configured scope.","commands":{"allow":[],"deny":["print"]}},"deny-reparent":{"identifier":"deny-reparent","description":"Denies the reparent command without any pre-configured scope.","commands":{"allow":[],"deny":["reparent"]}},"deny-set-webview-auto-resize":{"identifier":"deny-set-webview-auto-resize","description":"Denies the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_auto_resize"]}},"deny-set-webview-background-color":{"identifier":"deny-set-webview-background-color","description":"Denies the set_webview_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_background_color"]}},"deny-set-webview-focus":{"identifier":"deny-set-webview-focus","description":"Denies the set_webview_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_focus"]}},"deny-set-webview-position":{"identifier":"deny-set-webview-position","description":"Denies the set_webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_position"]}},"deny-set-webview-size":{"identifier":"deny-set-webview-size","description":"Denies the set_webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_size"]}},"deny-set-webview-zoom":{"identifier":"deny-set-webview-zoom","description":"Denies the set_webview_zoom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_zoom"]}},"deny-webview-close":{"identifier":"deny-webview-close","description":"Denies the webview_close command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_close"]}},"deny-webview-hide":{"identifier":"deny-webview-hide","description":"Denies the webview_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_hide"]}},"deny-webview-position":{"identifier":"deny-webview-position","description":"Denies the webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_position"]}},"deny-webview-show":{"identifier":"deny-webview-show","description":"Denies the webview_show command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_show"]}},"deny-webview-size":{"identifier":"deny-webview-size","description":"Denies the webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_size"]}}},"permission_sets":{},"global_scope_schema":null},"core:window":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-windows","allow-scale-factor","allow-inner-position","allow-outer-position","allow-inner-size","allow-outer-size","allow-is-fullscreen","allow-is-minimized","allow-is-maximized","allow-is-focused","allow-is-decorated","allow-is-resizable","allow-is-maximizable","allow-is-minimizable","allow-is-closable","allow-is-visible","allow-is-enabled","allow-title","allow-current-monitor","allow-primary-monitor","allow-monitor-from-point","allow-available-monitors","allow-cursor-position","allow-theme","allow-is-always-on-top","allow-internal-toggle-maximize"]},"permissions":{"allow-available-monitors":{"identifier":"allow-available-monitors","description":"Enables the available_monitors command without any pre-configured scope.","commands":{"allow":["available_monitors"],"deny":[]}},"allow-center":{"identifier":"allow-center","description":"Enables the center command without any pre-configured scope.","commands":{"allow":["center"],"deny":[]}},"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"allow-create":{"identifier":"allow-create","description":"Enables the create command without any pre-configured scope.","commands":{"allow":["create"],"deny":[]}},"allow-current-monitor":{"identifier":"allow-current-monitor","description":"Enables the current_monitor command without any pre-configured scope.","commands":{"allow":["current_monitor"],"deny":[]}},"allow-cursor-position":{"identifier":"allow-cursor-position","description":"Enables the cursor_position command without any pre-configured scope.","commands":{"allow":["cursor_position"],"deny":[]}},"allow-destroy":{"identifier":"allow-destroy","description":"Enables the destroy command without any pre-configured scope.","commands":{"allow":["destroy"],"deny":[]}},"allow-get-all-windows":{"identifier":"allow-get-all-windows","description":"Enables the get_all_windows command without any pre-configured scope.","commands":{"allow":["get_all_windows"],"deny":[]}},"allow-hide":{"identifier":"allow-hide","description":"Enables the hide command without any pre-configured scope.","commands":{"allow":["hide"],"deny":[]}},"allow-inner-position":{"identifier":"allow-inner-position","description":"Enables the inner_position command without any pre-configured scope.","commands":{"allow":["inner_position"],"deny":[]}},"allow-inner-size":{"identifier":"allow-inner-size","description":"Enables the inner_size command without any pre-configured scope.","commands":{"allow":["inner_size"],"deny":[]}},"allow-internal-toggle-maximize":{"identifier":"allow-internal-toggle-maximize","description":"Enables the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":["internal_toggle_maximize"],"deny":[]}},"allow-is-always-on-top":{"identifier":"allow-is-always-on-top","description":"Enables the is_always_on_top command without any pre-configured scope.","commands":{"allow":["is_always_on_top"],"deny":[]}},"allow-is-closable":{"identifier":"allow-is-closable","description":"Enables the is_closable command without any pre-configured scope.","commands":{"allow":["is_closable"],"deny":[]}},"allow-is-decorated":{"identifier":"allow-is-decorated","description":"Enables the is_decorated command without any pre-configured scope.","commands":{"allow":["is_decorated"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-is-focused":{"identifier":"allow-is-focused","description":"Enables the is_focused command without any pre-configured scope.","commands":{"allow":["is_focused"],"deny":[]}},"allow-is-fullscreen":{"identifier":"allow-is-fullscreen","description":"Enables the is_fullscreen command without any pre-configured scope.","commands":{"allow":["is_fullscreen"],"deny":[]}},"allow-is-maximizable":{"identifier":"allow-is-maximizable","description":"Enables the is_maximizable command without any pre-configured scope.","commands":{"allow":["is_maximizable"],"deny":[]}},"allow-is-maximized":{"identifier":"allow-is-maximized","description":"Enables the is_maximized command without any pre-configured scope.","commands":{"allow":["is_maximized"],"deny":[]}},"allow-is-minimizable":{"identifier":"allow-is-minimizable","description":"Enables the is_minimizable command without any pre-configured scope.","commands":{"allow":["is_minimizable"],"deny":[]}},"allow-is-minimized":{"identifier":"allow-is-minimized","description":"Enables the is_minimized command without any pre-configured scope.","commands":{"allow":["is_minimized"],"deny":[]}},"allow-is-resizable":{"identifier":"allow-is-resizable","description":"Enables the is_resizable command without any pre-configured scope.","commands":{"allow":["is_resizable"],"deny":[]}},"allow-is-visible":{"identifier":"allow-is-visible","description":"Enables the is_visible command without any pre-configured scope.","commands":{"allow":["is_visible"],"deny":[]}},"allow-maximize":{"identifier":"allow-maximize","description":"Enables the maximize command without any pre-configured scope.","commands":{"allow":["maximize"],"deny":[]}},"allow-minimize":{"identifier":"allow-minimize","description":"Enables the minimize command without any pre-configured scope.","commands":{"allow":["minimize"],"deny":[]}},"allow-monitor-from-point":{"identifier":"allow-monitor-from-point","description":"Enables the monitor_from_point command without any pre-configured scope.","commands":{"allow":["monitor_from_point"],"deny":[]}},"allow-outer-position":{"identifier":"allow-outer-position","description":"Enables the outer_position command without any pre-configured scope.","commands":{"allow":["outer_position"],"deny":[]}},"allow-outer-size":{"identifier":"allow-outer-size","description":"Enables the outer_size command without any pre-configured scope.","commands":{"allow":["outer_size"],"deny":[]}},"allow-primary-monitor":{"identifier":"allow-primary-monitor","description":"Enables the primary_monitor command without any pre-configured scope.","commands":{"allow":["primary_monitor"],"deny":[]}},"allow-request-user-attention":{"identifier":"allow-request-user-attention","description":"Enables the request_user_attention command without any pre-configured scope.","commands":{"allow":["request_user_attention"],"deny":[]}},"allow-scale-factor":{"identifier":"allow-scale-factor","description":"Enables the scale_factor command without any pre-configured scope.","commands":{"allow":["scale_factor"],"deny":[]}},"allow-set-always-on-bottom":{"identifier":"allow-set-always-on-bottom","description":"Enables the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":["set_always_on_bottom"],"deny":[]}},"allow-set-always-on-top":{"identifier":"allow-set-always-on-top","description":"Enables the set_always_on_top command without any pre-configured scope.","commands":{"allow":["set_always_on_top"],"deny":[]}},"allow-set-background-color":{"identifier":"allow-set-background-color","description":"Enables the set_background_color command without any pre-configured scope.","commands":{"allow":["set_background_color"],"deny":[]}},"allow-set-badge-count":{"identifier":"allow-set-badge-count","description":"Enables the set_badge_count command without any pre-configured scope.","commands":{"allow":["set_badge_count"],"deny":[]}},"allow-set-badge-label":{"identifier":"allow-set-badge-label","description":"Enables the set_badge_label command without any pre-configured scope.","commands":{"allow":["set_badge_label"],"deny":[]}},"allow-set-closable":{"identifier":"allow-set-closable","description":"Enables the set_closable command without any pre-configured scope.","commands":{"allow":["set_closable"],"deny":[]}},"allow-set-content-protected":{"identifier":"allow-set-content-protected","description":"Enables the set_content_protected command without any pre-configured scope.","commands":{"allow":["set_content_protected"],"deny":[]}},"allow-set-cursor-grab":{"identifier":"allow-set-cursor-grab","description":"Enables the set_cursor_grab command without any pre-configured scope.","commands":{"allow":["set_cursor_grab"],"deny":[]}},"allow-set-cursor-icon":{"identifier":"allow-set-cursor-icon","description":"Enables the set_cursor_icon command without any pre-configured scope.","commands":{"allow":["set_cursor_icon"],"deny":[]}},"allow-set-cursor-position":{"identifier":"allow-set-cursor-position","description":"Enables the set_cursor_position command without any pre-configured scope.","commands":{"allow":["set_cursor_position"],"deny":[]}},"allow-set-cursor-visible":{"identifier":"allow-set-cursor-visible","description":"Enables the set_cursor_visible command without any pre-configured scope.","commands":{"allow":["set_cursor_visible"],"deny":[]}},"allow-set-decorations":{"identifier":"allow-set-decorations","description":"Enables the set_decorations command without any pre-configured scope.","commands":{"allow":["set_decorations"],"deny":[]}},"allow-set-effects":{"identifier":"allow-set-effects","description":"Enables the set_effects command without any pre-configured scope.","commands":{"allow":["set_effects"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-focus":{"identifier":"allow-set-focus","description":"Enables the set_focus command without any pre-configured scope.","commands":{"allow":["set_focus"],"deny":[]}},"allow-set-focusable":{"identifier":"allow-set-focusable","description":"Enables the set_focusable command without any pre-configured scope.","commands":{"allow":["set_focusable"],"deny":[]}},"allow-set-fullscreen":{"identifier":"allow-set-fullscreen","description":"Enables the set_fullscreen command without any pre-configured scope.","commands":{"allow":["set_fullscreen"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-ignore-cursor-events":{"identifier":"allow-set-ignore-cursor-events","description":"Enables the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":["set_ignore_cursor_events"],"deny":[]}},"allow-set-max-size":{"identifier":"allow-set-max-size","description":"Enables the set_max_size command without any pre-configured scope.","commands":{"allow":["set_max_size"],"deny":[]}},"allow-set-maximizable":{"identifier":"allow-set-maximizable","description":"Enables the set_maximizable command without any pre-configured scope.","commands":{"allow":["set_maximizable"],"deny":[]}},"allow-set-min-size":{"identifier":"allow-set-min-size","description":"Enables the set_min_size command without any pre-configured scope.","commands":{"allow":["set_min_size"],"deny":[]}},"allow-set-minimizable":{"identifier":"allow-set-minimizable","description":"Enables the set_minimizable command without any pre-configured scope.","commands":{"allow":["set_minimizable"],"deny":[]}},"allow-set-overlay-icon":{"identifier":"allow-set-overlay-icon","description":"Enables the set_overlay_icon command without any pre-configured scope.","commands":{"allow":["set_overlay_icon"],"deny":[]}},"allow-set-position":{"identifier":"allow-set-position","description":"Enables the set_position command without any pre-configured scope.","commands":{"allow":["set_position"],"deny":[]}},"allow-set-progress-bar":{"identifier":"allow-set-progress-bar","description":"Enables the set_progress_bar command without any pre-configured scope.","commands":{"allow":["set_progress_bar"],"deny":[]}},"allow-set-resizable":{"identifier":"allow-set-resizable","description":"Enables the set_resizable command without any pre-configured scope.","commands":{"allow":["set_resizable"],"deny":[]}},"allow-set-shadow":{"identifier":"allow-set-shadow","description":"Enables the set_shadow command without any pre-configured scope.","commands":{"allow":["set_shadow"],"deny":[]}},"allow-set-simple-fullscreen":{"identifier":"allow-set-simple-fullscreen","description":"Enables the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":["set_simple_fullscreen"],"deny":[]}},"allow-set-size":{"identifier":"allow-set-size","description":"Enables the set_size command without any pre-configured scope.","commands":{"allow":["set_size"],"deny":[]}},"allow-set-size-constraints":{"identifier":"allow-set-size-constraints","description":"Enables the set_size_constraints command without any pre-configured scope.","commands":{"allow":["set_size_constraints"],"deny":[]}},"allow-set-skip-taskbar":{"identifier":"allow-set-skip-taskbar","description":"Enables the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":["set_skip_taskbar"],"deny":[]}},"allow-set-theme":{"identifier":"allow-set-theme","description":"Enables the set_theme command without any pre-configured scope.","commands":{"allow":["set_theme"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-title-bar-style":{"identifier":"allow-set-title-bar-style","description":"Enables the set_title_bar_style command without any pre-configured scope.","commands":{"allow":["set_title_bar_style"],"deny":[]}},"allow-set-visible-on-all-workspaces":{"identifier":"allow-set-visible-on-all-workspaces","description":"Enables the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":["set_visible_on_all_workspaces"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"allow-start-dragging":{"identifier":"allow-start-dragging","description":"Enables the start_dragging command without any pre-configured scope.","commands":{"allow":["start_dragging"],"deny":[]}},"allow-start-resize-dragging":{"identifier":"allow-start-resize-dragging","description":"Enables the start_resize_dragging command without any pre-configured scope.","commands":{"allow":["start_resize_dragging"],"deny":[]}},"allow-theme":{"identifier":"allow-theme","description":"Enables the theme command without any pre-configured scope.","commands":{"allow":["theme"],"deny":[]}},"allow-title":{"identifier":"allow-title","description":"Enables the title command without any pre-configured scope.","commands":{"allow":["title"],"deny":[]}},"allow-toggle-maximize":{"identifier":"allow-toggle-maximize","description":"Enables the toggle_maximize command without any pre-configured scope.","commands":{"allow":["toggle_maximize"],"deny":[]}},"allow-unmaximize":{"identifier":"allow-unmaximize","description":"Enables the unmaximize command without any pre-configured scope.","commands":{"allow":["unmaximize"],"deny":[]}},"allow-unminimize":{"identifier":"allow-unminimize","description":"Enables the unminimize command without any pre-configured scope.","commands":{"allow":["unminimize"],"deny":[]}},"deny-available-monitors":{"identifier":"deny-available-monitors","description":"Denies the available_monitors command without any pre-configured scope.","commands":{"allow":[],"deny":["available_monitors"]}},"deny-center":{"identifier":"deny-center","description":"Denies the center command without any pre-configured scope.","commands":{"allow":[],"deny":["center"]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}},"deny-create":{"identifier":"deny-create","description":"Denies the create command without any pre-configured scope.","commands":{"allow":[],"deny":["create"]}},"deny-current-monitor":{"identifier":"deny-current-monitor","description":"Denies the current_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["current_monitor"]}},"deny-cursor-position":{"identifier":"deny-cursor-position","description":"Denies the cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["cursor_position"]}},"deny-destroy":{"identifier":"deny-destroy","description":"Denies the destroy command without any pre-configured scope.","commands":{"allow":[],"deny":["destroy"]}},"deny-get-all-windows":{"identifier":"deny-get-all-windows","description":"Denies the get_all_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_windows"]}},"deny-hide":{"identifier":"deny-hide","description":"Denies the hide command without any pre-configured scope.","commands":{"allow":[],"deny":["hide"]}},"deny-inner-position":{"identifier":"deny-inner-position","description":"Denies the inner_position command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_position"]}},"deny-inner-size":{"identifier":"deny-inner-size","description":"Denies the inner_size command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_size"]}},"deny-internal-toggle-maximize":{"identifier":"deny-internal-toggle-maximize","description":"Denies the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_maximize"]}},"deny-is-always-on-top":{"identifier":"deny-is-always-on-top","description":"Denies the is_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["is_always_on_top"]}},"deny-is-closable":{"identifier":"deny-is-closable","description":"Denies the is_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_closable"]}},"deny-is-decorated":{"identifier":"deny-is-decorated","description":"Denies the is_decorated command without any pre-configured scope.","commands":{"allow":[],"deny":["is_decorated"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-is-focused":{"identifier":"deny-is-focused","description":"Denies the is_focused command without any pre-configured scope.","commands":{"allow":[],"deny":["is_focused"]}},"deny-is-fullscreen":{"identifier":"deny-is-fullscreen","description":"Denies the is_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["is_fullscreen"]}},"deny-is-maximizable":{"identifier":"deny-is-maximizable","description":"Denies the is_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximizable"]}},"deny-is-maximized":{"identifier":"deny-is-maximized","description":"Denies the is_maximized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximized"]}},"deny-is-minimizable":{"identifier":"deny-is-minimizable","description":"Denies the is_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimizable"]}},"deny-is-minimized":{"identifier":"deny-is-minimized","description":"Denies the is_minimized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimized"]}},"deny-is-resizable":{"identifier":"deny-is-resizable","description":"Denies the is_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_resizable"]}},"deny-is-visible":{"identifier":"deny-is-visible","description":"Denies the is_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["is_visible"]}},"deny-maximize":{"identifier":"deny-maximize","description":"Denies the maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["maximize"]}},"deny-minimize":{"identifier":"deny-minimize","description":"Denies the minimize command without any pre-configured scope.","commands":{"allow":[],"deny":["minimize"]}},"deny-monitor-from-point":{"identifier":"deny-monitor-from-point","description":"Denies the monitor_from_point command without any pre-configured scope.","commands":{"allow":[],"deny":["monitor_from_point"]}},"deny-outer-position":{"identifier":"deny-outer-position","description":"Denies the outer_position command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_position"]}},"deny-outer-size":{"identifier":"deny-outer-size","description":"Denies the outer_size command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_size"]}},"deny-primary-monitor":{"identifier":"deny-primary-monitor","description":"Denies the primary_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["primary_monitor"]}},"deny-request-user-attention":{"identifier":"deny-request-user-attention","description":"Denies the request_user_attention command without any pre-configured scope.","commands":{"allow":[],"deny":["request_user_attention"]}},"deny-scale-factor":{"identifier":"deny-scale-factor","description":"Denies the scale_factor command without any pre-configured scope.","commands":{"allow":[],"deny":["scale_factor"]}},"deny-set-always-on-bottom":{"identifier":"deny-set-always-on-bottom","description":"Denies the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_bottom"]}},"deny-set-always-on-top":{"identifier":"deny-set-always-on-top","description":"Denies the set_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_top"]}},"deny-set-background-color":{"identifier":"deny-set-background-color","description":"Denies the set_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_background_color"]}},"deny-set-badge-count":{"identifier":"deny-set-badge-count","description":"Denies the set_badge_count command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_count"]}},"deny-set-badge-label":{"identifier":"deny-set-badge-label","description":"Denies the set_badge_label command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_label"]}},"deny-set-closable":{"identifier":"deny-set-closable","description":"Denies the set_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_closable"]}},"deny-set-content-protected":{"identifier":"deny-set-content-protected","description":"Denies the set_content_protected command without any pre-configured scope.","commands":{"allow":[],"deny":["set_content_protected"]}},"deny-set-cursor-grab":{"identifier":"deny-set-cursor-grab","description":"Denies the set_cursor_grab command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_grab"]}},"deny-set-cursor-icon":{"identifier":"deny-set-cursor-icon","description":"Denies the set_cursor_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_icon"]}},"deny-set-cursor-position":{"identifier":"deny-set-cursor-position","description":"Denies the set_cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_position"]}},"deny-set-cursor-visible":{"identifier":"deny-set-cursor-visible","description":"Denies the set_cursor_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_visible"]}},"deny-set-decorations":{"identifier":"deny-set-decorations","description":"Denies the set_decorations command without any pre-configured scope.","commands":{"allow":[],"deny":["set_decorations"]}},"deny-set-effects":{"identifier":"deny-set-effects","description":"Denies the set_effects command without any pre-configured scope.","commands":{"allow":[],"deny":["set_effects"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-focus":{"identifier":"deny-set-focus","description":"Denies the set_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focus"]}},"deny-set-focusable":{"identifier":"deny-set-focusable","description":"Denies the set_focusable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focusable"]}},"deny-set-fullscreen":{"identifier":"deny-set-fullscreen","description":"Denies the set_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_fullscreen"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-ignore-cursor-events":{"identifier":"deny-set-ignore-cursor-events","description":"Denies the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":[],"deny":["set_ignore_cursor_events"]}},"deny-set-max-size":{"identifier":"deny-set-max-size","description":"Denies the set_max_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_max_size"]}},"deny-set-maximizable":{"identifier":"deny-set-maximizable","description":"Denies the set_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_maximizable"]}},"deny-set-min-size":{"identifier":"deny-set-min-size","description":"Denies the set_min_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_min_size"]}},"deny-set-minimizable":{"identifier":"deny-set-minimizable","description":"Denies the set_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_minimizable"]}},"deny-set-overlay-icon":{"identifier":"deny-set-overlay-icon","description":"Denies the set_overlay_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_overlay_icon"]}},"deny-set-position":{"identifier":"deny-set-position","description":"Denies the set_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_position"]}},"deny-set-progress-bar":{"identifier":"deny-set-progress-bar","description":"Denies the set_progress_bar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_progress_bar"]}},"deny-set-resizable":{"identifier":"deny-set-resizable","description":"Denies the set_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_resizable"]}},"deny-set-shadow":{"identifier":"deny-set-shadow","description":"Denies the set_shadow command without any pre-configured scope.","commands":{"allow":[],"deny":["set_shadow"]}},"deny-set-simple-fullscreen":{"identifier":"deny-set-simple-fullscreen","description":"Denies the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_simple_fullscreen"]}},"deny-set-size":{"identifier":"deny-set-size","description":"Denies the set_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size"]}},"deny-set-size-constraints":{"identifier":"deny-set-size-constraints","description":"Denies the set_size_constraints command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size_constraints"]}},"deny-set-skip-taskbar":{"identifier":"deny-set-skip-taskbar","description":"Denies the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_skip_taskbar"]}},"deny-set-theme":{"identifier":"deny-set-theme","description":"Denies the set_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_theme"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-title-bar-style":{"identifier":"deny-set-title-bar-style","description":"Denies the set_title_bar_style command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title_bar_style"]}},"deny-set-visible-on-all-workspaces":{"identifier":"deny-set-visible-on-all-workspaces","description":"Denies the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible_on_all_workspaces"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}},"deny-start-dragging":{"identifier":"deny-start-dragging","description":"Denies the start_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_dragging"]}},"deny-start-resize-dragging":{"identifier":"deny-start-resize-dragging","description":"Denies the start_resize_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_resize_dragging"]}},"deny-theme":{"identifier":"deny-theme","description":"Denies the theme command without any pre-configured scope.","commands":{"allow":[],"deny":["theme"]}},"deny-title":{"identifier":"deny-title","description":"Denies the title command without any pre-configured scope.","commands":{"allow":[],"deny":["title"]}},"deny-toggle-maximize":{"identifier":"deny-toggle-maximize","description":"Denies the toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["toggle_maximize"]}},"deny-unmaximize":{"identifier":"deny-unmaximize","description":"Denies the unmaximize command without any pre-configured scope.","commands":{"allow":[],"deny":["unmaximize"]}},"deny-unminimize":{"identifier":"deny-unminimize","description":"Denies the unminimize command without any pre-configured scope.","commands":{"allow":[],"deny":["unminimize"]}}},"permission_sets":{},"global_scope_schema":null},"dialog":{"default_permission":{"identifier":"default","description":"This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n","permissions":["allow-ask","allow-confirm","allow-message","allow-save","allow-open"]},"permissions":{"allow-ask":{"identifier":"allow-ask","description":"Enables the ask command without any pre-configured scope.","commands":{"allow":["ask"],"deny":[]}},"allow-confirm":{"identifier":"allow-confirm","description":"Enables the confirm command without any pre-configured scope.","commands":{"allow":["confirm"],"deny":[]}},"allow-message":{"identifier":"allow-message","description":"Enables the message command without any pre-configured scope.","commands":{"allow":["message"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-save":{"identifier":"allow-save","description":"Enables the save command without any pre-configured scope.","commands":{"allow":["save"],"deny":[]}},"deny-ask":{"identifier":"deny-ask","description":"Denies the ask command without any pre-configured scope.","commands":{"allow":[],"deny":["ask"]}},"deny-confirm":{"identifier":"deny-confirm","description":"Denies the confirm command without any pre-configured scope.","commands":{"allow":[],"deny":["confirm"]}},"deny-message":{"identifier":"deny-message","description":"Denies the message command without any pre-configured scope.","commands":{"allow":[],"deny":["message"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-save":{"identifier":"deny-save","description":"Denies the save command without any pre-configured scope.","commands":{"allow":[],"deny":["save"]}}},"permission_sets":{},"global_scope_schema":null},"opener":{"default_permission":{"identifier":"default","description":"This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer","permissions":["allow-open-url","allow-reveal-item-in-dir","allow-default-urls"]},"permissions":{"allow-default-urls":{"identifier":"allow-default-urls","description":"This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"url":"mailto:*"},{"url":"tel:*"},{"url":"http://*"},{"url":"https://*"}]}},"allow-open-path":{"identifier":"allow-open-path","description":"Enables the open_path command without any pre-configured scope.","commands":{"allow":["open_path"],"deny":[]}},"allow-open-url":{"identifier":"allow-open-url","description":"Enables the open_url command without any pre-configured scope.","commands":{"allow":["open_url"],"deny":[]}},"allow-reveal-item-in-dir":{"identifier":"allow-reveal-item-in-dir","description":"Enables the reveal_item_in_dir command without any pre-configured scope.","commands":{"allow":["reveal_item_in_dir"],"deny":[]}},"deny-open-path":{"identifier":"deny-open-path","description":"Denies the open_path command without any pre-configured scope.","commands":{"allow":[],"deny":["open_path"]}},"deny-open-url":{"identifier":"deny-open-url","description":"Denies the open_url command without any pre-configured scope.","commands":{"allow":[],"deny":["open_url"]}},"deny-reveal-item-in-dir":{"identifier":"deny-reveal-item-in-dir","description":"Denies the reveal_item_in_dir command without any pre-configured scope.","commands":{"allow":[],"deny":["reveal_item_in_dir"]}}},"permission_sets":{},"global_scope_schema":{"$schema":"http://json-schema.org/draft-07/schema#","anyOf":[{"properties":{"app":{"allOf":[{"$ref":"#/definitions/Application"}],"description":"An application to open this url with, for example: firefox."},"url":{"description":"A URL that can be opened by the webview when using the Opener APIs.\n\nWildcards can be used following the UNIX glob pattern.\n\nExamples:\n\n- \"https://*\" : allows all HTTPS origin\n\n- \"https://*.github.com/tauri-apps/tauri\": allows any subdomain of \"github.com\" with the \"tauri-apps/api\" path\n\n- \"https://myapi.service.com/users/*\": allows access to any URLs that begins with \"https://myapi.service.com/users/\"","type":"string"}},"required":["url"],"type":"object"},{"properties":{"app":{"allOf":[{"$ref":"#/definitions/Application"}],"description":"An application to open this path with, for example: xdg-open."},"path":{"description":"A path that can be opened by the webview when using the Opener APIs.\n\nThe pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.","type":"string"}},"required":["path"],"type":"object"}],"definitions":{"Application":{"anyOf":[{"description":"Open in default application.","type":"null"},{"description":"If true, allow open with any application.","type":"boolean"},{"description":"Allow specific application to open with.","type":"string"}],"description":"Opener scope application."}},"description":"Opener scope entry.","title":"OpenerScopeEntry"}}} \ No newline at end of file diff --git a/packages/tauri-app/src-tauri/gen/schemas/capabilities.json b/packages/tauri-app/src-tauri/gen/schemas/capabilities.json new file mode 100644 index 0000000..c98bf3f --- /dev/null +++ b/packages/tauri-app/src-tauri/gen/schemas/capabilities.json @@ -0,0 +1 @@ +{"main-window-native-dialogs":{"identifier":"main-window-native-dialogs","description":"Grant the main window access to required core features and native dialog commands.","remote":{"urls":["http://127.0.0.1:*","http://localhost:*"]},"local":true,"windows":["main"],"permissions":["core:default","core:menu:default","dialog:allow-open","opener:allow-default-urls","core:webview:allow-set-webview-zoom"]}} \ No newline at end of file diff --git a/packages/tauri-app/src-tauri/gen/schemas/desktop-schema.json b/packages/tauri-app/src-tauri/gen/schemas/desktop-schema.json new file mode 100644 index 0000000..344faff --- /dev/null +++ b/packages/tauri-app/src-tauri/gen/schemas/desktop-schema.json @@ -0,0 +1,2543 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CapabilityFile", + "description": "Capability formats accepted in a capability file.", + "anyOf": [ + { + "description": "A single capability.", + "allOf": [ + { + "$ref": "#/definitions/Capability" + } + ] + }, + { + "description": "A list of capabilities.", + "type": "array", + "items": { + "$ref": "#/definitions/Capability" + } + }, + { + "description": "A list of capabilities.", + "type": "object", + "required": [ + "capabilities" + ], + "properties": { + "capabilities": { + "description": "The list of capabilities.", + "type": "array", + "items": { + "$ref": "#/definitions/Capability" + } + } + } + } + ], + "definitions": { + "Capability": { + "description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows' and webviews' fine grained access to the Tauri core, application, or plugin commands. If a webview or its window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```", + "type": "object", + "required": [ + "identifier", + "permissions" + ], + "properties": { + "identifier": { + "description": "Identifier of the capability.\n\n## Example\n\n`main-user-files-write`", + "type": "string" + }, + "description": { + "description": "Description of what the capability is intended to allow on associated windows.\n\nIt should contain a description of what the grouped permissions should allow.\n\n## Example\n\nThis capability allows the `main` window access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.", + "default": "", + "type": "string" + }, + "remote": { + "description": "Configure remote URLs that can use the capability permissions.\n\nThis setting is optional and defaults to not being set, as our default use case is that the content is served from our local application.\n\n:::caution Make sure you understand the security implications of providing remote sources with local system access. :::\n\n## Example\n\n```json { \"urls\": [\"https://*.mydomain.dev\"] } ```", + "anyOf": [ + { + "$ref": "#/definitions/CapabilityRemote" + }, + { + "type": "null" + } + ] + }, + "local": { + "description": "Whether this capability is enabled for local app URLs or not. Defaults to `true`.", + "default": true, + "type": "boolean" + }, + "windows": { + "description": "List of windows that are affected by this capability. Can be a glob pattern.\n\nIf a window label matches any of the patterns in this list, the capability will be enabled on all the webviews of that window, regardless of the value of [`Self::webviews`].\n\nOn multiwebview windows, prefer specifying [`Self::webviews`] and omitting [`Self::windows`] for a fine grained access control.\n\n## Example\n\n`[\"main\"]`", + "type": "array", + "items": { + "type": "string" + } + }, + "webviews": { + "description": "List of webviews that are affected by this capability. Can be a glob pattern.\n\nThe capability will be enabled on all the webviews whose label matches any of the patterns in this list, regardless of whether the webview's window label matches a pattern in [`Self::windows`].\n\n## Example\n\n`[\"sub-webview-one\", \"sub-webview-two\"]`", + "type": "array", + "items": { + "type": "string" + } + }, + "permissions": { + "description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"core:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ] ```", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionEntry" + }, + "uniqueItems": true + }, + "platforms": { + "description": "Limit which target platforms this capability applies to.\n\nBy default all platforms are targeted.\n\n## Example\n\n`[\"macOS\",\"windows\"]`", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Target" + } + } + } + }, + "CapabilityRemote": { + "description": "Configuration for remote URLs that are associated with the capability.", + "type": "object", + "required": [ + "urls" + ], + "properties": { + "urls": { + "description": "Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).\n\n## Examples\n\n- \"https://*.mydomain.dev\": allows subdomains of mydomain.dev - \"https://mydomain.dev/api/*\": allows any subpath of mydomain.dev/api", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PermissionEntry": { + "description": "An entry for a permission value in a [`Capability`] can be either a raw permission [`Identifier`] or an object that references a permission and extends its scope.", + "anyOf": [ + { + "description": "Reference a permission or permission set by identifier.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + }, + { + "description": "Reference a permission or permission set by identifier and extends its scope.", + "type": "object", + "allOf": [ + { + "if": { + "properties": { + "identifier": { + "anyOf": [ + { + "description": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-default-urls`", + "type": "string", + "const": "opener:default", + "markdownDescription": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-default-urls`" + }, + { + "description": "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application.", + "type": "string", + "const": "opener:allow-default-urls", + "markdownDescription": "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application." + }, + { + "description": "Enables the open_path command without any pre-configured scope.", + "type": "string", + "const": "opener:allow-open-path", + "markdownDescription": "Enables the open_path command without any pre-configured scope." + }, + { + "description": "Enables the open_url command without any pre-configured scope.", + "type": "string", + "const": "opener:allow-open-url", + "markdownDescription": "Enables the open_url command without any pre-configured scope." + }, + { + "description": "Enables the reveal_item_in_dir command without any pre-configured scope.", + "type": "string", + "const": "opener:allow-reveal-item-in-dir", + "markdownDescription": "Enables the reveal_item_in_dir command without any pre-configured scope." + }, + { + "description": "Denies the open_path command without any pre-configured scope.", + "type": "string", + "const": "opener:deny-open-path", + "markdownDescription": "Denies the open_path command without any pre-configured scope." + }, + { + "description": "Denies the open_url command without any pre-configured scope.", + "type": "string", + "const": "opener:deny-open-url", + "markdownDescription": "Denies the open_url command without any pre-configured scope." + }, + { + "description": "Denies the reveal_item_in_dir command without any pre-configured scope.", + "type": "string", + "const": "opener:deny-reveal-item-in-dir", + "markdownDescription": "Denies the reveal_item_in_dir command without any pre-configured scope." + } + ] + } + } + }, + "then": { + "properties": { + "allow": { + "items": { + "title": "OpenerScopeEntry", + "description": "Opener scope entry.", + "anyOf": [ + { + "type": "object", + "required": [ + "url" + ], + "properties": { + "app": { + "description": "An application to open this url with, for example: firefox.", + "allOf": [ + { + "$ref": "#/definitions/Application" + } + ] + }, + "url": { + "description": "A URL that can be opened by the webview when using the Opener APIs.\n\nWildcards can be used following the UNIX glob pattern.\n\nExamples:\n\n- \"https://*\" : allows all HTTPS origin\n\n- \"https://*.github.com/tauri-apps/tauri\": allows any subdomain of \"github.com\" with the \"tauri-apps/api\" path\n\n- \"https://myapi.service.com/users/*\": allows access to any URLs that begins with \"https://myapi.service.com/users/\"", + "type": "string" + } + } + }, + { + "type": "object", + "required": [ + "path" + ], + "properties": { + "app": { + "description": "An application to open this path with, for example: xdg-open.", + "allOf": [ + { + "$ref": "#/definitions/Application" + } + ] + }, + "path": { + "description": "A path that can be opened by the webview when using the Opener APIs.\n\nThe pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", + "type": "string" + } + } + } + ] + } + }, + "deny": { + "items": { + "title": "OpenerScopeEntry", + "description": "Opener scope entry.", + "anyOf": [ + { + "type": "object", + "required": [ + "url" + ], + "properties": { + "app": { + "description": "An application to open this url with, for example: firefox.", + "allOf": [ + { + "$ref": "#/definitions/Application" + } + ] + }, + "url": { + "description": "A URL that can be opened by the webview when using the Opener APIs.\n\nWildcards can be used following the UNIX glob pattern.\n\nExamples:\n\n- \"https://*\" : allows all HTTPS origin\n\n- \"https://*.github.com/tauri-apps/tauri\": allows any subdomain of \"github.com\" with the \"tauri-apps/api\" path\n\n- \"https://myapi.service.com/users/*\": allows access to any URLs that begins with \"https://myapi.service.com/users/\"", + "type": "string" + } + } + }, + { + "type": "object", + "required": [ + "path" + ], + "properties": { + "app": { + "description": "An application to open this path with, for example: xdg-open.", + "allOf": [ + { + "$ref": "#/definitions/Application" + } + ] + }, + "path": { + "description": "A path that can be opened by the webview when using the Opener APIs.\n\nThe pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", + "type": "string" + } + } + } + ] + } + } + } + }, + "properties": { + "identifier": { + "description": "Identifier of the permission or permission set.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + } + } + }, + { + "properties": { + "identifier": { + "description": "Identifier of the permission or permission set.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + }, + "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + }, + "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + } + } + } + ], + "required": [ + "identifier" + ] + } + ] + }, + "Identifier": { + "description": "Permission identifier", + "oneOf": [ + { + "description": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`", + "type": "string", + "const": "core:default", + "markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`" + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`", + "type": "string", + "const": "core:app:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`" + }, + { + "description": "Enables the app_hide command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-app-hide", + "markdownDescription": "Enables the app_hide command without any pre-configured scope." + }, + { + "description": "Enables the app_show command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-app-show", + "markdownDescription": "Enables the app_show command without any pre-configured scope." + }, + { + "description": "Enables the bundle_type command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-bundle-type", + "markdownDescription": "Enables the bundle_type command without any pre-configured scope." + }, + { + "description": "Enables the default_window_icon command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-default-window-icon", + "markdownDescription": "Enables the default_window_icon command without any pre-configured scope." + }, + { + "description": "Enables the fetch_data_store_identifiers command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-fetch-data-store-identifiers", + "markdownDescription": "Enables the fetch_data_store_identifiers command without any pre-configured scope." + }, + { + "description": "Enables the identifier command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-identifier", + "markdownDescription": "Enables the identifier command without any pre-configured scope." + }, + { + "description": "Enables the name command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-name", + "markdownDescription": "Enables the name command without any pre-configured scope." + }, + { + "description": "Enables the register_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-register-listener", + "markdownDescription": "Enables the register_listener command without any pre-configured scope." + }, + { + "description": "Enables the remove_data_store command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-remove-data-store", + "markdownDescription": "Enables the remove_data_store command without any pre-configured scope." + }, + { + "description": "Enables the remove_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-remove-listener", + "markdownDescription": "Enables the remove_listener command without any pre-configured scope." + }, + { + "description": "Enables the set_app_theme command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-set-app-theme", + "markdownDescription": "Enables the set_app_theme command without any pre-configured scope." + }, + { + "description": "Enables the set_dock_visibility command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-set-dock-visibility", + "markdownDescription": "Enables the set_dock_visibility command without any pre-configured scope." + }, + { + "description": "Enables the tauri_version command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-tauri-version", + "markdownDescription": "Enables the tauri_version command without any pre-configured scope." + }, + { + "description": "Enables the version command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-version", + "markdownDescription": "Enables the version command without any pre-configured scope." + }, + { + "description": "Denies the app_hide command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-app-hide", + "markdownDescription": "Denies the app_hide command without any pre-configured scope." + }, + { + "description": "Denies the app_show command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-app-show", + "markdownDescription": "Denies the app_show command without any pre-configured scope." + }, + { + "description": "Denies the bundle_type command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-bundle-type", + "markdownDescription": "Denies the bundle_type command without any pre-configured scope." + }, + { + "description": "Denies the default_window_icon command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-default-window-icon", + "markdownDescription": "Denies the default_window_icon command without any pre-configured scope." + }, + { + "description": "Denies the fetch_data_store_identifiers command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-fetch-data-store-identifiers", + "markdownDescription": "Denies the fetch_data_store_identifiers command without any pre-configured scope." + }, + { + "description": "Denies the identifier command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-identifier", + "markdownDescription": "Denies the identifier command without any pre-configured scope." + }, + { + "description": "Denies the name command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-name", + "markdownDescription": "Denies the name command without any pre-configured scope." + }, + { + "description": "Denies the register_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-register-listener", + "markdownDescription": "Denies the register_listener command without any pre-configured scope." + }, + { + "description": "Denies the remove_data_store command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-remove-data-store", + "markdownDescription": "Denies the remove_data_store command without any pre-configured scope." + }, + { + "description": "Denies the remove_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-remove-listener", + "markdownDescription": "Denies the remove_listener command without any pre-configured scope." + }, + { + "description": "Denies the set_app_theme command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-set-app-theme", + "markdownDescription": "Denies the set_app_theme command without any pre-configured scope." + }, + { + "description": "Denies the set_dock_visibility command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-set-dock-visibility", + "markdownDescription": "Denies the set_dock_visibility command without any pre-configured scope." + }, + { + "description": "Denies the tauri_version command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-tauri-version", + "markdownDescription": "Denies the tauri_version command without any pre-configured scope." + }, + { + "description": "Denies the version command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-version", + "markdownDescription": "Denies the version command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`", + "type": "string", + "const": "core:event:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`" + }, + { + "description": "Enables the emit command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-emit", + "markdownDescription": "Enables the emit command without any pre-configured scope." + }, + { + "description": "Enables the emit_to command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-emit-to", + "markdownDescription": "Enables the emit_to command without any pre-configured scope." + }, + { + "description": "Enables the listen command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-listen", + "markdownDescription": "Enables the listen command without any pre-configured scope." + }, + { + "description": "Enables the unlisten command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-unlisten", + "markdownDescription": "Enables the unlisten command without any pre-configured scope." + }, + { + "description": "Denies the emit command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-emit", + "markdownDescription": "Denies the emit command without any pre-configured scope." + }, + { + "description": "Denies the emit_to command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-emit-to", + "markdownDescription": "Denies the emit_to command without any pre-configured scope." + }, + { + "description": "Denies the listen command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-listen", + "markdownDescription": "Denies the listen command without any pre-configured scope." + }, + { + "description": "Denies the unlisten command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-unlisten", + "markdownDescription": "Denies the unlisten command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`", + "type": "string", + "const": "core:image:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`" + }, + { + "description": "Enables the from_bytes command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-from-bytes", + "markdownDescription": "Enables the from_bytes command without any pre-configured scope." + }, + { + "description": "Enables the from_path command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-from-path", + "markdownDescription": "Enables the from_path command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the rgba command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-rgba", + "markdownDescription": "Enables the rgba command without any pre-configured scope." + }, + { + "description": "Enables the size command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-size", + "markdownDescription": "Enables the size command without any pre-configured scope." + }, + { + "description": "Denies the from_bytes command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-from-bytes", + "markdownDescription": "Denies the from_bytes command without any pre-configured scope." + }, + { + "description": "Denies the from_path command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-from-path", + "markdownDescription": "Denies the from_path command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the rgba command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-rgba", + "markdownDescription": "Denies the rgba command without any pre-configured scope." + }, + { + "description": "Denies the size command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-size", + "markdownDescription": "Denies the size command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`", + "type": "string", + "const": "core:menu:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`" + }, + { + "description": "Enables the append command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-append", + "markdownDescription": "Enables the append command without any pre-configured scope." + }, + { + "description": "Enables the create_default command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-create-default", + "markdownDescription": "Enables the create_default command without any pre-configured scope." + }, + { + "description": "Enables the get command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-get", + "markdownDescription": "Enables the get command without any pre-configured scope." + }, + { + "description": "Enables the insert command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-insert", + "markdownDescription": "Enables the insert command without any pre-configured scope." + }, + { + "description": "Enables the is_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-is-checked", + "markdownDescription": "Enables the is_checked command without any pre-configured scope." + }, + { + "description": "Enables the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-is-enabled", + "markdownDescription": "Enables the is_enabled command without any pre-configured scope." + }, + { + "description": "Enables the items command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-items", + "markdownDescription": "Enables the items command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the popup command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-popup", + "markdownDescription": "Enables the popup command without any pre-configured scope." + }, + { + "description": "Enables the prepend command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-prepend", + "markdownDescription": "Enables the prepend command without any pre-configured scope." + }, + { + "description": "Enables the remove command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-remove", + "markdownDescription": "Enables the remove command without any pre-configured scope." + }, + { + "description": "Enables the remove_at command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-remove-at", + "markdownDescription": "Enables the remove_at command without any pre-configured scope." + }, + { + "description": "Enables the set_accelerator command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-accelerator", + "markdownDescription": "Enables the set_accelerator command without any pre-configured scope." + }, + { + "description": "Enables the set_as_app_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-app-menu", + "markdownDescription": "Enables the set_as_app_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-help-menu-for-nsapp", + "markdownDescription": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Enables the set_as_window_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-window-menu", + "markdownDescription": "Enables the set_as_window_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-windows-menu-for-nsapp", + "markdownDescription": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Enables the set_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-checked", + "markdownDescription": "Enables the set_checked command without any pre-configured scope." + }, + { + "description": "Enables the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-enabled", + "markdownDescription": "Enables the set_enabled command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-text", + "markdownDescription": "Enables the set_text command without any pre-configured scope." + }, + { + "description": "Enables the text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-text", + "markdownDescription": "Enables the text command without any pre-configured scope." + }, + { + "description": "Denies the append command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-append", + "markdownDescription": "Denies the append command without any pre-configured scope." + }, + { + "description": "Denies the create_default command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-create-default", + "markdownDescription": "Denies the create_default command without any pre-configured scope." + }, + { + "description": "Denies the get command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-get", + "markdownDescription": "Denies the get command without any pre-configured scope." + }, + { + "description": "Denies the insert command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-insert", + "markdownDescription": "Denies the insert command without any pre-configured scope." + }, + { + "description": "Denies the is_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-is-checked", + "markdownDescription": "Denies the is_checked command without any pre-configured scope." + }, + { + "description": "Denies the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-is-enabled", + "markdownDescription": "Denies the is_enabled command without any pre-configured scope." + }, + { + "description": "Denies the items command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-items", + "markdownDescription": "Denies the items command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the popup command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-popup", + "markdownDescription": "Denies the popup command without any pre-configured scope." + }, + { + "description": "Denies the prepend command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-prepend", + "markdownDescription": "Denies the prepend command without any pre-configured scope." + }, + { + "description": "Denies the remove command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-remove", + "markdownDescription": "Denies the remove command without any pre-configured scope." + }, + { + "description": "Denies the remove_at command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-remove-at", + "markdownDescription": "Denies the remove_at command without any pre-configured scope." + }, + { + "description": "Denies the set_accelerator command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-accelerator", + "markdownDescription": "Denies the set_accelerator command without any pre-configured scope." + }, + { + "description": "Denies the set_as_app_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-app-menu", + "markdownDescription": "Denies the set_as_app_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-help-menu-for-nsapp", + "markdownDescription": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Denies the set_as_window_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-window-menu", + "markdownDescription": "Denies the set_as_window_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-windows-menu-for-nsapp", + "markdownDescription": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Denies the set_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-checked", + "markdownDescription": "Denies the set_checked command without any pre-configured scope." + }, + { + "description": "Denies the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-enabled", + "markdownDescription": "Denies the set_enabled command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-text", + "markdownDescription": "Denies the set_text command without any pre-configured scope." + }, + { + "description": "Denies the text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-text", + "markdownDescription": "Denies the text command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`", + "type": "string", + "const": "core:path:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`" + }, + { + "description": "Enables the basename command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-basename", + "markdownDescription": "Enables the basename command without any pre-configured scope." + }, + { + "description": "Enables the dirname command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-dirname", + "markdownDescription": "Enables the dirname command without any pre-configured scope." + }, + { + "description": "Enables the extname command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-extname", + "markdownDescription": "Enables the extname command without any pre-configured scope." + }, + { + "description": "Enables the is_absolute command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-is-absolute", + "markdownDescription": "Enables the is_absolute command without any pre-configured scope." + }, + { + "description": "Enables the join command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-join", + "markdownDescription": "Enables the join command without any pre-configured scope." + }, + { + "description": "Enables the normalize command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-normalize", + "markdownDescription": "Enables the normalize command without any pre-configured scope." + }, + { + "description": "Enables the resolve command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-resolve", + "markdownDescription": "Enables the resolve command without any pre-configured scope." + }, + { + "description": "Enables the resolve_directory command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-resolve-directory", + "markdownDescription": "Enables the resolve_directory command without any pre-configured scope." + }, + { + "description": "Denies the basename command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-basename", + "markdownDescription": "Denies the basename command without any pre-configured scope." + }, + { + "description": "Denies the dirname command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-dirname", + "markdownDescription": "Denies the dirname command without any pre-configured scope." + }, + { + "description": "Denies the extname command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-extname", + "markdownDescription": "Denies the extname command without any pre-configured scope." + }, + { + "description": "Denies the is_absolute command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-is-absolute", + "markdownDescription": "Denies the is_absolute command without any pre-configured scope." + }, + { + "description": "Denies the join command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-join", + "markdownDescription": "Denies the join command without any pre-configured scope." + }, + { + "description": "Denies the normalize command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-normalize", + "markdownDescription": "Denies the normalize command without any pre-configured scope." + }, + { + "description": "Denies the resolve command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-resolve", + "markdownDescription": "Denies the resolve command without any pre-configured scope." + }, + { + "description": "Denies the resolve_directory command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-resolve-directory", + "markdownDescription": "Denies the resolve_directory command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`", + "type": "string", + "const": "core:resources:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`" + }, + { + "description": "Enables the close command without any pre-configured scope.", + "type": "string", + "const": "core:resources:allow-close", + "markdownDescription": "Enables the close command without any pre-configured scope." + }, + { + "description": "Denies the close command without any pre-configured scope.", + "type": "string", + "const": "core:resources:deny-close", + "markdownDescription": "Denies the close command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-show-menu-on-left-click`", + "type": "string", + "const": "core:tray:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-show-menu-on-left-click`" + }, + { + "description": "Enables the get_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-get-by-id", + "markdownDescription": "Enables the get_by_id command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the remove_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-remove-by-id", + "markdownDescription": "Enables the remove_by_id command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_icon_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon-as-template", + "markdownDescription": "Enables the set_icon_as_template command without any pre-configured scope." + }, + { + "description": "Enables the set_menu command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-menu", + "markdownDescription": "Enables the set_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_show_menu_on_left_click command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-show-menu-on-left-click", + "markdownDescription": "Enables the set_show_menu_on_left_click command without any pre-configured scope." + }, + { + "description": "Enables the set_temp_dir_path command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-temp-dir-path", + "markdownDescription": "Enables the set_temp_dir_path command without any pre-configured scope." + }, + { + "description": "Enables the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-title", + "markdownDescription": "Enables the set_title command without any pre-configured scope." + }, + { + "description": "Enables the set_tooltip command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-tooltip", + "markdownDescription": "Enables the set_tooltip command without any pre-configured scope." + }, + { + "description": "Enables the set_visible command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-visible", + "markdownDescription": "Enables the set_visible command without any pre-configured scope." + }, + { + "description": "Denies the get_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-get-by-id", + "markdownDescription": "Denies the get_by_id command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the remove_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-remove-by-id", + "markdownDescription": "Denies the remove_by_id command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_icon_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon-as-template", + "markdownDescription": "Denies the set_icon_as_template command without any pre-configured scope." + }, + { + "description": "Denies the set_menu command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-menu", + "markdownDescription": "Denies the set_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_show_menu_on_left_click command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-show-menu-on-left-click", + "markdownDescription": "Denies the set_show_menu_on_left_click command without any pre-configured scope." + }, + { + "description": "Denies the set_temp_dir_path command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-temp-dir-path", + "markdownDescription": "Denies the set_temp_dir_path command without any pre-configured scope." + }, + { + "description": "Denies the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-title", + "markdownDescription": "Denies the set_title command without any pre-configured scope." + }, + { + "description": "Denies the set_tooltip command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-tooltip", + "markdownDescription": "Denies the set_tooltip command without any pre-configured scope." + }, + { + "description": "Denies the set_visible command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-visible", + "markdownDescription": "Denies the set_visible command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`", + "type": "string", + "const": "core:webview:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`" + }, + { + "description": "Enables the clear_all_browsing_data command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-clear-all-browsing-data", + "markdownDescription": "Enables the clear_all_browsing_data command without any pre-configured scope." + }, + { + "description": "Enables the create_webview command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-create-webview", + "markdownDescription": "Enables the create_webview command without any pre-configured scope." + }, + { + "description": "Enables the create_webview_window command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-create-webview-window", + "markdownDescription": "Enables the create_webview_window command without any pre-configured scope." + }, + { + "description": "Enables the get_all_webviews command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-get-all-webviews", + "markdownDescription": "Enables the get_all_webviews command without any pre-configured scope." + }, + { + "description": "Enables the internal_toggle_devtools command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-internal-toggle-devtools", + "markdownDescription": "Enables the internal_toggle_devtools command without any pre-configured scope." + }, + { + "description": "Enables the print command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-print", + "markdownDescription": "Enables the print command without any pre-configured scope." + }, + { + "description": "Enables the reparent command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-reparent", + "markdownDescription": "Enables the reparent command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_auto_resize command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-auto-resize", + "markdownDescription": "Enables the set_webview_auto_resize command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-background-color", + "markdownDescription": "Enables the set_webview_background_color command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_focus command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-focus", + "markdownDescription": "Enables the set_webview_focus command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-position", + "markdownDescription": "Enables the set_webview_position command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-size", + "markdownDescription": "Enables the set_webview_size command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_zoom command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-zoom", + "markdownDescription": "Enables the set_webview_zoom command without any pre-configured scope." + }, + { + "description": "Enables the webview_close command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-close", + "markdownDescription": "Enables the webview_close command without any pre-configured scope." + }, + { + "description": "Enables the webview_hide command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-hide", + "markdownDescription": "Enables the webview_hide command without any pre-configured scope." + }, + { + "description": "Enables the webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-position", + "markdownDescription": "Enables the webview_position command without any pre-configured scope." + }, + { + "description": "Enables the webview_show command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-show", + "markdownDescription": "Enables the webview_show command without any pre-configured scope." + }, + { + "description": "Enables the webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-size", + "markdownDescription": "Enables the webview_size command without any pre-configured scope." + }, + { + "description": "Denies the clear_all_browsing_data command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-clear-all-browsing-data", + "markdownDescription": "Denies the clear_all_browsing_data command without any pre-configured scope." + }, + { + "description": "Denies the create_webview command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-create-webview", + "markdownDescription": "Denies the create_webview command without any pre-configured scope." + }, + { + "description": "Denies the create_webview_window command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-create-webview-window", + "markdownDescription": "Denies the create_webview_window command without any pre-configured scope." + }, + { + "description": "Denies the get_all_webviews command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-get-all-webviews", + "markdownDescription": "Denies the get_all_webviews command without any pre-configured scope." + }, + { + "description": "Denies the internal_toggle_devtools command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-internal-toggle-devtools", + "markdownDescription": "Denies the internal_toggle_devtools command without any pre-configured scope." + }, + { + "description": "Denies the print command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-print", + "markdownDescription": "Denies the print command without any pre-configured scope." + }, + { + "description": "Denies the reparent command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-reparent", + "markdownDescription": "Denies the reparent command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_auto_resize command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-auto-resize", + "markdownDescription": "Denies the set_webview_auto_resize command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-background-color", + "markdownDescription": "Denies the set_webview_background_color command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_focus command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-focus", + "markdownDescription": "Denies the set_webview_focus command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-position", + "markdownDescription": "Denies the set_webview_position command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-size", + "markdownDescription": "Denies the set_webview_size command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_zoom command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-zoom", + "markdownDescription": "Denies the set_webview_zoom command without any pre-configured scope." + }, + { + "description": "Denies the webview_close command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-close", + "markdownDescription": "Denies the webview_close command without any pre-configured scope." + }, + { + "description": "Denies the webview_hide command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-hide", + "markdownDescription": "Denies the webview_hide command without any pre-configured scope." + }, + { + "description": "Denies the webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-position", + "markdownDescription": "Denies the webview_position command without any pre-configured scope." + }, + { + "description": "Denies the webview_show command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-show", + "markdownDescription": "Denies the webview_show command without any pre-configured scope." + }, + { + "description": "Denies the webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-size", + "markdownDescription": "Denies the webview_size command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-internal-toggle-maximize`", + "type": "string", + "const": "core:window:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-internal-toggle-maximize`" + }, + { + "description": "Enables the available_monitors command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-available-monitors", + "markdownDescription": "Enables the available_monitors command without any pre-configured scope." + }, + { + "description": "Enables the center command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-center", + "markdownDescription": "Enables the center command without any pre-configured scope." + }, + { + "description": "Enables the close command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-close", + "markdownDescription": "Enables the close command without any pre-configured scope." + }, + { + "description": "Enables the create command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-create", + "markdownDescription": "Enables the create command without any pre-configured scope." + }, + { + "description": "Enables the current_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-current-monitor", + "markdownDescription": "Enables the current_monitor command without any pre-configured scope." + }, + { + "description": "Enables the cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-cursor-position", + "markdownDescription": "Enables the cursor_position command without any pre-configured scope." + }, + { + "description": "Enables the destroy command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-destroy", + "markdownDescription": "Enables the destroy command without any pre-configured scope." + }, + { + "description": "Enables the get_all_windows command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-get-all-windows", + "markdownDescription": "Enables the get_all_windows command without any pre-configured scope." + }, + { + "description": "Enables the hide command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-hide", + "markdownDescription": "Enables the hide command without any pre-configured scope." + }, + { + "description": "Enables the inner_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-inner-position", + "markdownDescription": "Enables the inner_position command without any pre-configured scope." + }, + { + "description": "Enables the inner_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-inner-size", + "markdownDescription": "Enables the inner_size command without any pre-configured scope." + }, + { + "description": "Enables the internal_toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-internal-toggle-maximize", + "markdownDescription": "Enables the internal_toggle_maximize command without any pre-configured scope." + }, + { + "description": "Enables the is_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-always-on-top", + "markdownDescription": "Enables the is_always_on_top command without any pre-configured scope." + }, + { + "description": "Enables the is_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-closable", + "markdownDescription": "Enables the is_closable command without any pre-configured scope." + }, + { + "description": "Enables the is_decorated command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-decorated", + "markdownDescription": "Enables the is_decorated command without any pre-configured scope." + }, + { + "description": "Enables the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-enabled", + "markdownDescription": "Enables the is_enabled command without any pre-configured scope." + }, + { + "description": "Enables the is_focused command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-focused", + "markdownDescription": "Enables the is_focused command without any pre-configured scope." + }, + { + "description": "Enables the is_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-fullscreen", + "markdownDescription": "Enables the is_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the is_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-maximizable", + "markdownDescription": "Enables the is_maximizable command without any pre-configured scope." + }, + { + "description": "Enables the is_maximized command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-maximized", + "markdownDescription": "Enables the is_maximized command without any pre-configured scope." + }, + { + "description": "Enables the is_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-minimizable", + "markdownDescription": "Enables the is_minimizable command without any pre-configured scope." + }, + { + "description": "Enables the is_minimized command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-minimized", + "markdownDescription": "Enables the is_minimized command without any pre-configured scope." + }, + { + "description": "Enables the is_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-resizable", + "markdownDescription": "Enables the is_resizable command without any pre-configured scope." + }, + { + "description": "Enables the is_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-visible", + "markdownDescription": "Enables the is_visible command without any pre-configured scope." + }, + { + "description": "Enables the maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-maximize", + "markdownDescription": "Enables the maximize command without any pre-configured scope." + }, + { + "description": "Enables the minimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-minimize", + "markdownDescription": "Enables the minimize command without any pre-configured scope." + }, + { + "description": "Enables the monitor_from_point command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-monitor-from-point", + "markdownDescription": "Enables the monitor_from_point command without any pre-configured scope." + }, + { + "description": "Enables the outer_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-outer-position", + "markdownDescription": "Enables the outer_position command without any pre-configured scope." + }, + { + "description": "Enables the outer_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-outer-size", + "markdownDescription": "Enables the outer_size command without any pre-configured scope." + }, + { + "description": "Enables the primary_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-primary-monitor", + "markdownDescription": "Enables the primary_monitor command without any pre-configured scope." + }, + { + "description": "Enables the request_user_attention command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-request-user-attention", + "markdownDescription": "Enables the request_user_attention command without any pre-configured scope." + }, + { + "description": "Enables the scale_factor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-scale-factor", + "markdownDescription": "Enables the scale_factor command without any pre-configured scope." + }, + { + "description": "Enables the set_always_on_bottom command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-always-on-bottom", + "markdownDescription": "Enables the set_always_on_bottom command without any pre-configured scope." + }, + { + "description": "Enables the set_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-always-on-top", + "markdownDescription": "Enables the set_always_on_top command without any pre-configured scope." + }, + { + "description": "Enables the set_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-background-color", + "markdownDescription": "Enables the set_background_color command without any pre-configured scope." + }, + { + "description": "Enables the set_badge_count command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-badge-count", + "markdownDescription": "Enables the set_badge_count command without any pre-configured scope." + }, + { + "description": "Enables the set_badge_label command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-badge-label", + "markdownDescription": "Enables the set_badge_label command without any pre-configured scope." + }, + { + "description": "Enables the set_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-closable", + "markdownDescription": "Enables the set_closable command without any pre-configured scope." + }, + { + "description": "Enables the set_content_protected command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-content-protected", + "markdownDescription": "Enables the set_content_protected command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_grab command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-grab", + "markdownDescription": "Enables the set_cursor_grab command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-icon", + "markdownDescription": "Enables the set_cursor_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-position", + "markdownDescription": "Enables the set_cursor_position command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-visible", + "markdownDescription": "Enables the set_cursor_visible command without any pre-configured scope." + }, + { + "description": "Enables the set_decorations command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-decorations", + "markdownDescription": "Enables the set_decorations command without any pre-configured scope." + }, + { + "description": "Enables the set_effects command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-effects", + "markdownDescription": "Enables the set_effects command without any pre-configured scope." + }, + { + "description": "Enables the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-enabled", + "markdownDescription": "Enables the set_enabled command without any pre-configured scope." + }, + { + "description": "Enables the set_focus command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-focus", + "markdownDescription": "Enables the set_focus command without any pre-configured scope." + }, + { + "description": "Enables the set_focusable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-focusable", + "markdownDescription": "Enables the set_focusable command without any pre-configured scope." + }, + { + "description": "Enables the set_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-fullscreen", + "markdownDescription": "Enables the set_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_ignore_cursor_events command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-ignore-cursor-events", + "markdownDescription": "Enables the set_ignore_cursor_events command without any pre-configured scope." + }, + { + "description": "Enables the set_max_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-max-size", + "markdownDescription": "Enables the set_max_size command without any pre-configured scope." + }, + { + "description": "Enables the set_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-maximizable", + "markdownDescription": "Enables the set_maximizable command without any pre-configured scope." + }, + { + "description": "Enables the set_min_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-min-size", + "markdownDescription": "Enables the set_min_size command without any pre-configured scope." + }, + { + "description": "Enables the set_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-minimizable", + "markdownDescription": "Enables the set_minimizable command without any pre-configured scope." + }, + { + "description": "Enables the set_overlay_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-overlay-icon", + "markdownDescription": "Enables the set_overlay_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-position", + "markdownDescription": "Enables the set_position command without any pre-configured scope." + }, + { + "description": "Enables the set_progress_bar command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-progress-bar", + "markdownDescription": "Enables the set_progress_bar command without any pre-configured scope." + }, + { + "description": "Enables the set_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-resizable", + "markdownDescription": "Enables the set_resizable command without any pre-configured scope." + }, + { + "description": "Enables the set_shadow command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-shadow", + "markdownDescription": "Enables the set_shadow command without any pre-configured scope." + }, + { + "description": "Enables the set_simple_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-simple-fullscreen", + "markdownDescription": "Enables the set_simple_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the set_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-size", + "markdownDescription": "Enables the set_size command without any pre-configured scope." + }, + { + "description": "Enables the set_size_constraints command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-size-constraints", + "markdownDescription": "Enables the set_size_constraints command without any pre-configured scope." + }, + { + "description": "Enables the set_skip_taskbar command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-skip-taskbar", + "markdownDescription": "Enables the set_skip_taskbar command without any pre-configured scope." + }, + { + "description": "Enables the set_theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-theme", + "markdownDescription": "Enables the set_theme command without any pre-configured scope." + }, + { + "description": "Enables the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-title", + "markdownDescription": "Enables the set_title command without any pre-configured scope." + }, + { + "description": "Enables the set_title_bar_style command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-title-bar-style", + "markdownDescription": "Enables the set_title_bar_style command without any pre-configured scope." + }, + { + "description": "Enables the set_visible_on_all_workspaces command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-visible-on-all-workspaces", + "markdownDescription": "Enables the set_visible_on_all_workspaces command without any pre-configured scope." + }, + { + "description": "Enables the show command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-show", + "markdownDescription": "Enables the show command without any pre-configured scope." + }, + { + "description": "Enables the start_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-start-dragging", + "markdownDescription": "Enables the start_dragging command without any pre-configured scope." + }, + { + "description": "Enables the start_resize_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-start-resize-dragging", + "markdownDescription": "Enables the start_resize_dragging command without any pre-configured scope." + }, + { + "description": "Enables the theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-theme", + "markdownDescription": "Enables the theme command without any pre-configured scope." + }, + { + "description": "Enables the title command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-title", + "markdownDescription": "Enables the title command without any pre-configured scope." + }, + { + "description": "Enables the toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-toggle-maximize", + "markdownDescription": "Enables the toggle_maximize command without any pre-configured scope." + }, + { + "description": "Enables the unmaximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-unmaximize", + "markdownDescription": "Enables the unmaximize command without any pre-configured scope." + }, + { + "description": "Enables the unminimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-unminimize", + "markdownDescription": "Enables the unminimize command without any pre-configured scope." + }, + { + "description": "Denies the available_monitors command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-available-monitors", + "markdownDescription": "Denies the available_monitors command without any pre-configured scope." + }, + { + "description": "Denies the center command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-center", + "markdownDescription": "Denies the center command without any pre-configured scope." + }, + { + "description": "Denies the close command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-close", + "markdownDescription": "Denies the close command without any pre-configured scope." + }, + { + "description": "Denies the create command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-create", + "markdownDescription": "Denies the create command without any pre-configured scope." + }, + { + "description": "Denies the current_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-current-monitor", + "markdownDescription": "Denies the current_monitor command without any pre-configured scope." + }, + { + "description": "Denies the cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-cursor-position", + "markdownDescription": "Denies the cursor_position command without any pre-configured scope." + }, + { + "description": "Denies the destroy command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-destroy", + "markdownDescription": "Denies the destroy command without any pre-configured scope." + }, + { + "description": "Denies the get_all_windows command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-get-all-windows", + "markdownDescription": "Denies the get_all_windows command without any pre-configured scope." + }, + { + "description": "Denies the hide command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-hide", + "markdownDescription": "Denies the hide command without any pre-configured scope." + }, + { + "description": "Denies the inner_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-inner-position", + "markdownDescription": "Denies the inner_position command without any pre-configured scope." + }, + { + "description": "Denies the inner_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-inner-size", + "markdownDescription": "Denies the inner_size command without any pre-configured scope." + }, + { + "description": "Denies the internal_toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-internal-toggle-maximize", + "markdownDescription": "Denies the internal_toggle_maximize command without any pre-configured scope." + }, + { + "description": "Denies the is_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-always-on-top", + "markdownDescription": "Denies the is_always_on_top command without any pre-configured scope." + }, + { + "description": "Denies the is_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-closable", + "markdownDescription": "Denies the is_closable command without any pre-configured scope." + }, + { + "description": "Denies the is_decorated command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-decorated", + "markdownDescription": "Denies the is_decorated command without any pre-configured scope." + }, + { + "description": "Denies the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-enabled", + "markdownDescription": "Denies the is_enabled command without any pre-configured scope." + }, + { + "description": "Denies the is_focused command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-focused", + "markdownDescription": "Denies the is_focused command without any pre-configured scope." + }, + { + "description": "Denies the is_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-fullscreen", + "markdownDescription": "Denies the is_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the is_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-maximizable", + "markdownDescription": "Denies the is_maximizable command without any pre-configured scope." + }, + { + "description": "Denies the is_maximized command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-maximized", + "markdownDescription": "Denies the is_maximized command without any pre-configured scope." + }, + { + "description": "Denies the is_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-minimizable", + "markdownDescription": "Denies the is_minimizable command without any pre-configured scope." + }, + { + "description": "Denies the is_minimized command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-minimized", + "markdownDescription": "Denies the is_minimized command without any pre-configured scope." + }, + { + "description": "Denies the is_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-resizable", + "markdownDescription": "Denies the is_resizable command without any pre-configured scope." + }, + { + "description": "Denies the is_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-visible", + "markdownDescription": "Denies the is_visible command without any pre-configured scope." + }, + { + "description": "Denies the maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-maximize", + "markdownDescription": "Denies the maximize command without any pre-configured scope." + }, + { + "description": "Denies the minimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-minimize", + "markdownDescription": "Denies the minimize command without any pre-configured scope." + }, + { + "description": "Denies the monitor_from_point command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-monitor-from-point", + "markdownDescription": "Denies the monitor_from_point command without any pre-configured scope." + }, + { + "description": "Denies the outer_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-outer-position", + "markdownDescription": "Denies the outer_position command without any pre-configured scope." + }, + { + "description": "Denies the outer_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-outer-size", + "markdownDescription": "Denies the outer_size command without any pre-configured scope." + }, + { + "description": "Denies the primary_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-primary-monitor", + "markdownDescription": "Denies the primary_monitor command without any pre-configured scope." + }, + { + "description": "Denies the request_user_attention command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-request-user-attention", + "markdownDescription": "Denies the request_user_attention command without any pre-configured scope." + }, + { + "description": "Denies the scale_factor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-scale-factor", + "markdownDescription": "Denies the scale_factor command without any pre-configured scope." + }, + { + "description": "Denies the set_always_on_bottom command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-always-on-bottom", + "markdownDescription": "Denies the set_always_on_bottom command without any pre-configured scope." + }, + { + "description": "Denies the set_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-always-on-top", + "markdownDescription": "Denies the set_always_on_top command without any pre-configured scope." + }, + { + "description": "Denies the set_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-background-color", + "markdownDescription": "Denies the set_background_color command without any pre-configured scope." + }, + { + "description": "Denies the set_badge_count command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-badge-count", + "markdownDescription": "Denies the set_badge_count command without any pre-configured scope." + }, + { + "description": "Denies the set_badge_label command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-badge-label", + "markdownDescription": "Denies the set_badge_label command without any pre-configured scope." + }, + { + "description": "Denies the set_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-closable", + "markdownDescription": "Denies the set_closable command without any pre-configured scope." + }, + { + "description": "Denies the set_content_protected command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-content-protected", + "markdownDescription": "Denies the set_content_protected command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_grab command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-grab", + "markdownDescription": "Denies the set_cursor_grab command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-icon", + "markdownDescription": "Denies the set_cursor_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-position", + "markdownDescription": "Denies the set_cursor_position command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-visible", + "markdownDescription": "Denies the set_cursor_visible command without any pre-configured scope." + }, + { + "description": "Denies the set_decorations command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-decorations", + "markdownDescription": "Denies the set_decorations command without any pre-configured scope." + }, + { + "description": "Denies the set_effects command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-effects", + "markdownDescription": "Denies the set_effects command without any pre-configured scope." + }, + { + "description": "Denies the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-enabled", + "markdownDescription": "Denies the set_enabled command without any pre-configured scope." + }, + { + "description": "Denies the set_focus command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-focus", + "markdownDescription": "Denies the set_focus command without any pre-configured scope." + }, + { + "description": "Denies the set_focusable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-focusable", + "markdownDescription": "Denies the set_focusable command without any pre-configured scope." + }, + { + "description": "Denies the set_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-fullscreen", + "markdownDescription": "Denies the set_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_ignore_cursor_events command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-ignore-cursor-events", + "markdownDescription": "Denies the set_ignore_cursor_events command without any pre-configured scope." + }, + { + "description": "Denies the set_max_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-max-size", + "markdownDescription": "Denies the set_max_size command without any pre-configured scope." + }, + { + "description": "Denies the set_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-maximizable", + "markdownDescription": "Denies the set_maximizable command without any pre-configured scope." + }, + { + "description": "Denies the set_min_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-min-size", + "markdownDescription": "Denies the set_min_size command without any pre-configured scope." + }, + { + "description": "Denies the set_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-minimizable", + "markdownDescription": "Denies the set_minimizable command without any pre-configured scope." + }, + { + "description": "Denies the set_overlay_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-overlay-icon", + "markdownDescription": "Denies the set_overlay_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-position", + "markdownDescription": "Denies the set_position command without any pre-configured scope." + }, + { + "description": "Denies the set_progress_bar command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-progress-bar", + "markdownDescription": "Denies the set_progress_bar command without any pre-configured scope." + }, + { + "description": "Denies the set_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-resizable", + "markdownDescription": "Denies the set_resizable command without any pre-configured scope." + }, + { + "description": "Denies the set_shadow command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-shadow", + "markdownDescription": "Denies the set_shadow command without any pre-configured scope." + }, + { + "description": "Denies the set_simple_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-simple-fullscreen", + "markdownDescription": "Denies the set_simple_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the set_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-size", + "markdownDescription": "Denies the set_size command without any pre-configured scope." + }, + { + "description": "Denies the set_size_constraints command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-size-constraints", + "markdownDescription": "Denies the set_size_constraints command without any pre-configured scope." + }, + { + "description": "Denies the set_skip_taskbar command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-skip-taskbar", + "markdownDescription": "Denies the set_skip_taskbar command without any pre-configured scope." + }, + { + "description": "Denies the set_theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-theme", + "markdownDescription": "Denies the set_theme command without any pre-configured scope." + }, + { + "description": "Denies the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-title", + "markdownDescription": "Denies the set_title command without any pre-configured scope." + }, + { + "description": "Denies the set_title_bar_style command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-title-bar-style", + "markdownDescription": "Denies the set_title_bar_style command without any pre-configured scope." + }, + { + "description": "Denies the set_visible_on_all_workspaces command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-visible-on-all-workspaces", + "markdownDescription": "Denies the set_visible_on_all_workspaces command without any pre-configured scope." + }, + { + "description": "Denies the show command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-show", + "markdownDescription": "Denies the show command without any pre-configured scope." + }, + { + "description": "Denies the start_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-start-dragging", + "markdownDescription": "Denies the start_dragging command without any pre-configured scope." + }, + { + "description": "Denies the start_resize_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-start-resize-dragging", + "markdownDescription": "Denies the start_resize_dragging command without any pre-configured scope." + }, + { + "description": "Denies the theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-theme", + "markdownDescription": "Denies the theme command without any pre-configured scope." + }, + { + "description": "Denies the title command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-title", + "markdownDescription": "Denies the title command without any pre-configured scope." + }, + { + "description": "Denies the toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-toggle-maximize", + "markdownDescription": "Denies the toggle_maximize command without any pre-configured scope." + }, + { + "description": "Denies the unmaximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-unmaximize", + "markdownDescription": "Denies the unmaximize command without any pre-configured scope." + }, + { + "description": "Denies the unminimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-unminimize", + "markdownDescription": "Denies the unminimize command without any pre-configured scope." + }, + { + "description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-ask`\n- `allow-confirm`\n- `allow-message`\n- `allow-save`\n- `allow-open`", + "type": "string", + "const": "dialog:default", + "markdownDescription": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-ask`\n- `allow-confirm`\n- `allow-message`\n- `allow-save`\n- `allow-open`" + }, + { + "description": "Enables the ask command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-ask", + "markdownDescription": "Enables the ask command without any pre-configured scope." + }, + { + "description": "Enables the confirm command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-confirm", + "markdownDescription": "Enables the confirm command without any pre-configured scope." + }, + { + "description": "Enables the message command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-message", + "markdownDescription": "Enables the message command without any pre-configured scope." + }, + { + "description": "Enables the open command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-open", + "markdownDescription": "Enables the open command without any pre-configured scope." + }, + { + "description": "Enables the save command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-save", + "markdownDescription": "Enables the save command without any pre-configured scope." + }, + { + "description": "Denies the ask command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-ask", + "markdownDescription": "Denies the ask command without any pre-configured scope." + }, + { + "description": "Denies the confirm command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-confirm", + "markdownDescription": "Denies the confirm command without any pre-configured scope." + }, + { + "description": "Denies the message command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-message", + "markdownDescription": "Denies the message command without any pre-configured scope." + }, + { + "description": "Denies the open command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-open", + "markdownDescription": "Denies the open command without any pre-configured scope." + }, + { + "description": "Denies the save command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-save", + "markdownDescription": "Denies the save command without any pre-configured scope." + }, + { + "description": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-default-urls`", + "type": "string", + "const": "opener:default", + "markdownDescription": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-default-urls`" + }, + { + "description": "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application.", + "type": "string", + "const": "opener:allow-default-urls", + "markdownDescription": "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application." + }, + { + "description": "Enables the open_path command without any pre-configured scope.", + "type": "string", + "const": "opener:allow-open-path", + "markdownDescription": "Enables the open_path command without any pre-configured scope." + }, + { + "description": "Enables the open_url command without any pre-configured scope.", + "type": "string", + "const": "opener:allow-open-url", + "markdownDescription": "Enables the open_url command without any pre-configured scope." + }, + { + "description": "Enables the reveal_item_in_dir command without any pre-configured scope.", + "type": "string", + "const": "opener:allow-reveal-item-in-dir", + "markdownDescription": "Enables the reveal_item_in_dir command without any pre-configured scope." + }, + { + "description": "Denies the open_path command without any pre-configured scope.", + "type": "string", + "const": "opener:deny-open-path", + "markdownDescription": "Denies the open_path command without any pre-configured scope." + }, + { + "description": "Denies the open_url command without any pre-configured scope.", + "type": "string", + "const": "opener:deny-open-url", + "markdownDescription": "Denies the open_url command without any pre-configured scope." + }, + { + "description": "Denies the reveal_item_in_dir command without any pre-configured scope.", + "type": "string", + "const": "opener:deny-reveal-item-in-dir", + "markdownDescription": "Denies the reveal_item_in_dir command without any pre-configured scope." + } + ] + }, + "Value": { + "description": "All supported ACL values.", + "anyOf": [ + { + "description": "Represents a null JSON value.", + "type": "null" + }, + { + "description": "Represents a [`bool`].", + "type": "boolean" + }, + { + "description": "Represents a valid ACL [`Number`].", + "allOf": [ + { + "$ref": "#/definitions/Number" + } + ] + }, + { + "description": "Represents a [`String`].", + "type": "string" + }, + { + "description": "Represents a list of other [`Value`]s.", + "type": "array", + "items": { + "$ref": "#/definitions/Value" + } + }, + { + "description": "Represents a map of [`String`] keys to [`Value`]s.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Value" + } + } + ] + }, + "Number": { + "description": "A valid ACL number.", + "anyOf": [ + { + "description": "Represents an [`i64`].", + "type": "integer", + "format": "int64" + }, + { + "description": "Represents a [`f64`].", + "type": "number", + "format": "double" + } + ] + }, + "Target": { + "description": "Platform target.", + "oneOf": [ + { + "description": "MacOS.", + "type": "string", + "enum": [ + "macOS" + ] + }, + { + "description": "Windows.", + "type": "string", + "enum": [ + "windows" + ] + }, + { + "description": "Linux.", + "type": "string", + "enum": [ + "linux" + ] + }, + { + "description": "Android.", + "type": "string", + "enum": [ + "android" + ] + }, + { + "description": "iOS.", + "type": "string", + "enum": [ + "iOS" + ] + } + ] + }, + "Application": { + "description": "Opener scope application.", + "anyOf": [ + { + "description": "Open in default application.", + "type": "null" + }, + { + "description": "If true, allow open with any application.", + "type": "boolean" + }, + { + "description": "Allow specific application to open with.", + "type": "string" + } + ] + } + } +} \ No newline at end of file diff --git a/packages/tauri-app/src-tauri/gen/schemas/macOS-schema.json b/packages/tauri-app/src-tauri/gen/schemas/macOS-schema.json new file mode 100644 index 0000000..344faff --- /dev/null +++ b/packages/tauri-app/src-tauri/gen/schemas/macOS-schema.json @@ -0,0 +1,2543 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CapabilityFile", + "description": "Capability formats accepted in a capability file.", + "anyOf": [ + { + "description": "A single capability.", + "allOf": [ + { + "$ref": "#/definitions/Capability" + } + ] + }, + { + "description": "A list of capabilities.", + "type": "array", + "items": { + "$ref": "#/definitions/Capability" + } + }, + { + "description": "A list of capabilities.", + "type": "object", + "required": [ + "capabilities" + ], + "properties": { + "capabilities": { + "description": "The list of capabilities.", + "type": "array", + "items": { + "$ref": "#/definitions/Capability" + } + } + } + } + ], + "definitions": { + "Capability": { + "description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows' and webviews' fine grained access to the Tauri core, application, or plugin commands. If a webview or its window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```", + "type": "object", + "required": [ + "identifier", + "permissions" + ], + "properties": { + "identifier": { + "description": "Identifier of the capability.\n\n## Example\n\n`main-user-files-write`", + "type": "string" + }, + "description": { + "description": "Description of what the capability is intended to allow on associated windows.\n\nIt should contain a description of what the grouped permissions should allow.\n\n## Example\n\nThis capability allows the `main` window access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.", + "default": "", + "type": "string" + }, + "remote": { + "description": "Configure remote URLs that can use the capability permissions.\n\nThis setting is optional and defaults to not being set, as our default use case is that the content is served from our local application.\n\n:::caution Make sure you understand the security implications of providing remote sources with local system access. :::\n\n## Example\n\n```json { \"urls\": [\"https://*.mydomain.dev\"] } ```", + "anyOf": [ + { + "$ref": "#/definitions/CapabilityRemote" + }, + { + "type": "null" + } + ] + }, + "local": { + "description": "Whether this capability is enabled for local app URLs or not. Defaults to `true`.", + "default": true, + "type": "boolean" + }, + "windows": { + "description": "List of windows that are affected by this capability. Can be a glob pattern.\n\nIf a window label matches any of the patterns in this list, the capability will be enabled on all the webviews of that window, regardless of the value of [`Self::webviews`].\n\nOn multiwebview windows, prefer specifying [`Self::webviews`] and omitting [`Self::windows`] for a fine grained access control.\n\n## Example\n\n`[\"main\"]`", + "type": "array", + "items": { + "type": "string" + } + }, + "webviews": { + "description": "List of webviews that are affected by this capability. Can be a glob pattern.\n\nThe capability will be enabled on all the webviews whose label matches any of the patterns in this list, regardless of whether the webview's window label matches a pattern in [`Self::windows`].\n\n## Example\n\n`[\"sub-webview-one\", \"sub-webview-two\"]`", + "type": "array", + "items": { + "type": "string" + } + }, + "permissions": { + "description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"core:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ] ```", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionEntry" + }, + "uniqueItems": true + }, + "platforms": { + "description": "Limit which target platforms this capability applies to.\n\nBy default all platforms are targeted.\n\n## Example\n\n`[\"macOS\",\"windows\"]`", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Target" + } + } + } + }, + "CapabilityRemote": { + "description": "Configuration for remote URLs that are associated with the capability.", + "type": "object", + "required": [ + "urls" + ], + "properties": { + "urls": { + "description": "Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).\n\n## Examples\n\n- \"https://*.mydomain.dev\": allows subdomains of mydomain.dev - \"https://mydomain.dev/api/*\": allows any subpath of mydomain.dev/api", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PermissionEntry": { + "description": "An entry for a permission value in a [`Capability`] can be either a raw permission [`Identifier`] or an object that references a permission and extends its scope.", + "anyOf": [ + { + "description": "Reference a permission or permission set by identifier.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + }, + { + "description": "Reference a permission or permission set by identifier and extends its scope.", + "type": "object", + "allOf": [ + { + "if": { + "properties": { + "identifier": { + "anyOf": [ + { + "description": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-default-urls`", + "type": "string", + "const": "opener:default", + "markdownDescription": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-default-urls`" + }, + { + "description": "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application.", + "type": "string", + "const": "opener:allow-default-urls", + "markdownDescription": "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application." + }, + { + "description": "Enables the open_path command without any pre-configured scope.", + "type": "string", + "const": "opener:allow-open-path", + "markdownDescription": "Enables the open_path command without any pre-configured scope." + }, + { + "description": "Enables the open_url command without any pre-configured scope.", + "type": "string", + "const": "opener:allow-open-url", + "markdownDescription": "Enables the open_url command without any pre-configured scope." + }, + { + "description": "Enables the reveal_item_in_dir command without any pre-configured scope.", + "type": "string", + "const": "opener:allow-reveal-item-in-dir", + "markdownDescription": "Enables the reveal_item_in_dir command without any pre-configured scope." + }, + { + "description": "Denies the open_path command without any pre-configured scope.", + "type": "string", + "const": "opener:deny-open-path", + "markdownDescription": "Denies the open_path command without any pre-configured scope." + }, + { + "description": "Denies the open_url command without any pre-configured scope.", + "type": "string", + "const": "opener:deny-open-url", + "markdownDescription": "Denies the open_url command without any pre-configured scope." + }, + { + "description": "Denies the reveal_item_in_dir command without any pre-configured scope.", + "type": "string", + "const": "opener:deny-reveal-item-in-dir", + "markdownDescription": "Denies the reveal_item_in_dir command without any pre-configured scope." + } + ] + } + } + }, + "then": { + "properties": { + "allow": { + "items": { + "title": "OpenerScopeEntry", + "description": "Opener scope entry.", + "anyOf": [ + { + "type": "object", + "required": [ + "url" + ], + "properties": { + "app": { + "description": "An application to open this url with, for example: firefox.", + "allOf": [ + { + "$ref": "#/definitions/Application" + } + ] + }, + "url": { + "description": "A URL that can be opened by the webview when using the Opener APIs.\n\nWildcards can be used following the UNIX glob pattern.\n\nExamples:\n\n- \"https://*\" : allows all HTTPS origin\n\n- \"https://*.github.com/tauri-apps/tauri\": allows any subdomain of \"github.com\" with the \"tauri-apps/api\" path\n\n- \"https://myapi.service.com/users/*\": allows access to any URLs that begins with \"https://myapi.service.com/users/\"", + "type": "string" + } + } + }, + { + "type": "object", + "required": [ + "path" + ], + "properties": { + "app": { + "description": "An application to open this path with, for example: xdg-open.", + "allOf": [ + { + "$ref": "#/definitions/Application" + } + ] + }, + "path": { + "description": "A path that can be opened by the webview when using the Opener APIs.\n\nThe pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", + "type": "string" + } + } + } + ] + } + }, + "deny": { + "items": { + "title": "OpenerScopeEntry", + "description": "Opener scope entry.", + "anyOf": [ + { + "type": "object", + "required": [ + "url" + ], + "properties": { + "app": { + "description": "An application to open this url with, for example: firefox.", + "allOf": [ + { + "$ref": "#/definitions/Application" + } + ] + }, + "url": { + "description": "A URL that can be opened by the webview when using the Opener APIs.\n\nWildcards can be used following the UNIX glob pattern.\n\nExamples:\n\n- \"https://*\" : allows all HTTPS origin\n\n- \"https://*.github.com/tauri-apps/tauri\": allows any subdomain of \"github.com\" with the \"tauri-apps/api\" path\n\n- \"https://myapi.service.com/users/*\": allows access to any URLs that begins with \"https://myapi.service.com/users/\"", + "type": "string" + } + } + }, + { + "type": "object", + "required": [ + "path" + ], + "properties": { + "app": { + "description": "An application to open this path with, for example: xdg-open.", + "allOf": [ + { + "$ref": "#/definitions/Application" + } + ] + }, + "path": { + "description": "A path that can be opened by the webview when using the Opener APIs.\n\nThe pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", + "type": "string" + } + } + } + ] + } + } + } + }, + "properties": { + "identifier": { + "description": "Identifier of the permission or permission set.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + } + } + }, + { + "properties": { + "identifier": { + "description": "Identifier of the permission or permission set.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + }, + "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + }, + "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + } + } + } + ], + "required": [ + "identifier" + ] + } + ] + }, + "Identifier": { + "description": "Permission identifier", + "oneOf": [ + { + "description": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`", + "type": "string", + "const": "core:default", + "markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`" + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`", + "type": "string", + "const": "core:app:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`" + }, + { + "description": "Enables the app_hide command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-app-hide", + "markdownDescription": "Enables the app_hide command without any pre-configured scope." + }, + { + "description": "Enables the app_show command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-app-show", + "markdownDescription": "Enables the app_show command without any pre-configured scope." + }, + { + "description": "Enables the bundle_type command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-bundle-type", + "markdownDescription": "Enables the bundle_type command without any pre-configured scope." + }, + { + "description": "Enables the default_window_icon command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-default-window-icon", + "markdownDescription": "Enables the default_window_icon command without any pre-configured scope." + }, + { + "description": "Enables the fetch_data_store_identifiers command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-fetch-data-store-identifiers", + "markdownDescription": "Enables the fetch_data_store_identifiers command without any pre-configured scope." + }, + { + "description": "Enables the identifier command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-identifier", + "markdownDescription": "Enables the identifier command without any pre-configured scope." + }, + { + "description": "Enables the name command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-name", + "markdownDescription": "Enables the name command without any pre-configured scope." + }, + { + "description": "Enables the register_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-register-listener", + "markdownDescription": "Enables the register_listener command without any pre-configured scope." + }, + { + "description": "Enables the remove_data_store command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-remove-data-store", + "markdownDescription": "Enables the remove_data_store command without any pre-configured scope." + }, + { + "description": "Enables the remove_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-remove-listener", + "markdownDescription": "Enables the remove_listener command without any pre-configured scope." + }, + { + "description": "Enables the set_app_theme command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-set-app-theme", + "markdownDescription": "Enables the set_app_theme command without any pre-configured scope." + }, + { + "description": "Enables the set_dock_visibility command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-set-dock-visibility", + "markdownDescription": "Enables the set_dock_visibility command without any pre-configured scope." + }, + { + "description": "Enables the tauri_version command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-tauri-version", + "markdownDescription": "Enables the tauri_version command without any pre-configured scope." + }, + { + "description": "Enables the version command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-version", + "markdownDescription": "Enables the version command without any pre-configured scope." + }, + { + "description": "Denies the app_hide command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-app-hide", + "markdownDescription": "Denies the app_hide command without any pre-configured scope." + }, + { + "description": "Denies the app_show command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-app-show", + "markdownDescription": "Denies the app_show command without any pre-configured scope." + }, + { + "description": "Denies the bundle_type command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-bundle-type", + "markdownDescription": "Denies the bundle_type command without any pre-configured scope." + }, + { + "description": "Denies the default_window_icon command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-default-window-icon", + "markdownDescription": "Denies the default_window_icon command without any pre-configured scope." + }, + { + "description": "Denies the fetch_data_store_identifiers command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-fetch-data-store-identifiers", + "markdownDescription": "Denies the fetch_data_store_identifiers command without any pre-configured scope." + }, + { + "description": "Denies the identifier command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-identifier", + "markdownDescription": "Denies the identifier command without any pre-configured scope." + }, + { + "description": "Denies the name command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-name", + "markdownDescription": "Denies the name command without any pre-configured scope." + }, + { + "description": "Denies the register_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-register-listener", + "markdownDescription": "Denies the register_listener command without any pre-configured scope." + }, + { + "description": "Denies the remove_data_store command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-remove-data-store", + "markdownDescription": "Denies the remove_data_store command without any pre-configured scope." + }, + { + "description": "Denies the remove_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-remove-listener", + "markdownDescription": "Denies the remove_listener command without any pre-configured scope." + }, + { + "description": "Denies the set_app_theme command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-set-app-theme", + "markdownDescription": "Denies the set_app_theme command without any pre-configured scope." + }, + { + "description": "Denies the set_dock_visibility command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-set-dock-visibility", + "markdownDescription": "Denies the set_dock_visibility command without any pre-configured scope." + }, + { + "description": "Denies the tauri_version command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-tauri-version", + "markdownDescription": "Denies the tauri_version command without any pre-configured scope." + }, + { + "description": "Denies the version command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-version", + "markdownDescription": "Denies the version command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`", + "type": "string", + "const": "core:event:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`" + }, + { + "description": "Enables the emit command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-emit", + "markdownDescription": "Enables the emit command without any pre-configured scope." + }, + { + "description": "Enables the emit_to command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-emit-to", + "markdownDescription": "Enables the emit_to command without any pre-configured scope." + }, + { + "description": "Enables the listen command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-listen", + "markdownDescription": "Enables the listen command without any pre-configured scope." + }, + { + "description": "Enables the unlisten command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-unlisten", + "markdownDescription": "Enables the unlisten command without any pre-configured scope." + }, + { + "description": "Denies the emit command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-emit", + "markdownDescription": "Denies the emit command without any pre-configured scope." + }, + { + "description": "Denies the emit_to command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-emit-to", + "markdownDescription": "Denies the emit_to command without any pre-configured scope." + }, + { + "description": "Denies the listen command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-listen", + "markdownDescription": "Denies the listen command without any pre-configured scope." + }, + { + "description": "Denies the unlisten command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-unlisten", + "markdownDescription": "Denies the unlisten command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`", + "type": "string", + "const": "core:image:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`" + }, + { + "description": "Enables the from_bytes command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-from-bytes", + "markdownDescription": "Enables the from_bytes command without any pre-configured scope." + }, + { + "description": "Enables the from_path command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-from-path", + "markdownDescription": "Enables the from_path command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the rgba command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-rgba", + "markdownDescription": "Enables the rgba command without any pre-configured scope." + }, + { + "description": "Enables the size command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-size", + "markdownDescription": "Enables the size command without any pre-configured scope." + }, + { + "description": "Denies the from_bytes command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-from-bytes", + "markdownDescription": "Denies the from_bytes command without any pre-configured scope." + }, + { + "description": "Denies the from_path command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-from-path", + "markdownDescription": "Denies the from_path command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the rgba command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-rgba", + "markdownDescription": "Denies the rgba command without any pre-configured scope." + }, + { + "description": "Denies the size command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-size", + "markdownDescription": "Denies the size command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`", + "type": "string", + "const": "core:menu:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`" + }, + { + "description": "Enables the append command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-append", + "markdownDescription": "Enables the append command without any pre-configured scope." + }, + { + "description": "Enables the create_default command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-create-default", + "markdownDescription": "Enables the create_default command without any pre-configured scope." + }, + { + "description": "Enables the get command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-get", + "markdownDescription": "Enables the get command without any pre-configured scope." + }, + { + "description": "Enables the insert command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-insert", + "markdownDescription": "Enables the insert command without any pre-configured scope." + }, + { + "description": "Enables the is_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-is-checked", + "markdownDescription": "Enables the is_checked command without any pre-configured scope." + }, + { + "description": "Enables the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-is-enabled", + "markdownDescription": "Enables the is_enabled command without any pre-configured scope." + }, + { + "description": "Enables the items command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-items", + "markdownDescription": "Enables the items command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the popup command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-popup", + "markdownDescription": "Enables the popup command without any pre-configured scope." + }, + { + "description": "Enables the prepend command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-prepend", + "markdownDescription": "Enables the prepend command without any pre-configured scope." + }, + { + "description": "Enables the remove command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-remove", + "markdownDescription": "Enables the remove command without any pre-configured scope." + }, + { + "description": "Enables the remove_at command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-remove-at", + "markdownDescription": "Enables the remove_at command without any pre-configured scope." + }, + { + "description": "Enables the set_accelerator command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-accelerator", + "markdownDescription": "Enables the set_accelerator command without any pre-configured scope." + }, + { + "description": "Enables the set_as_app_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-app-menu", + "markdownDescription": "Enables the set_as_app_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-help-menu-for-nsapp", + "markdownDescription": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Enables the set_as_window_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-window-menu", + "markdownDescription": "Enables the set_as_window_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-windows-menu-for-nsapp", + "markdownDescription": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Enables the set_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-checked", + "markdownDescription": "Enables the set_checked command without any pre-configured scope." + }, + { + "description": "Enables the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-enabled", + "markdownDescription": "Enables the set_enabled command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-text", + "markdownDescription": "Enables the set_text command without any pre-configured scope." + }, + { + "description": "Enables the text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-text", + "markdownDescription": "Enables the text command without any pre-configured scope." + }, + { + "description": "Denies the append command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-append", + "markdownDescription": "Denies the append command without any pre-configured scope." + }, + { + "description": "Denies the create_default command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-create-default", + "markdownDescription": "Denies the create_default command without any pre-configured scope." + }, + { + "description": "Denies the get command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-get", + "markdownDescription": "Denies the get command without any pre-configured scope." + }, + { + "description": "Denies the insert command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-insert", + "markdownDescription": "Denies the insert command without any pre-configured scope." + }, + { + "description": "Denies the is_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-is-checked", + "markdownDescription": "Denies the is_checked command without any pre-configured scope." + }, + { + "description": "Denies the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-is-enabled", + "markdownDescription": "Denies the is_enabled command without any pre-configured scope." + }, + { + "description": "Denies the items command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-items", + "markdownDescription": "Denies the items command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the popup command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-popup", + "markdownDescription": "Denies the popup command without any pre-configured scope." + }, + { + "description": "Denies the prepend command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-prepend", + "markdownDescription": "Denies the prepend command without any pre-configured scope." + }, + { + "description": "Denies the remove command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-remove", + "markdownDescription": "Denies the remove command without any pre-configured scope." + }, + { + "description": "Denies the remove_at command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-remove-at", + "markdownDescription": "Denies the remove_at command without any pre-configured scope." + }, + { + "description": "Denies the set_accelerator command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-accelerator", + "markdownDescription": "Denies the set_accelerator command without any pre-configured scope." + }, + { + "description": "Denies the set_as_app_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-app-menu", + "markdownDescription": "Denies the set_as_app_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-help-menu-for-nsapp", + "markdownDescription": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Denies the set_as_window_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-window-menu", + "markdownDescription": "Denies the set_as_window_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-windows-menu-for-nsapp", + "markdownDescription": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Denies the set_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-checked", + "markdownDescription": "Denies the set_checked command without any pre-configured scope." + }, + { + "description": "Denies the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-enabled", + "markdownDescription": "Denies the set_enabled command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-text", + "markdownDescription": "Denies the set_text command without any pre-configured scope." + }, + { + "description": "Denies the text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-text", + "markdownDescription": "Denies the text command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`", + "type": "string", + "const": "core:path:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`" + }, + { + "description": "Enables the basename command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-basename", + "markdownDescription": "Enables the basename command without any pre-configured scope." + }, + { + "description": "Enables the dirname command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-dirname", + "markdownDescription": "Enables the dirname command without any pre-configured scope." + }, + { + "description": "Enables the extname command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-extname", + "markdownDescription": "Enables the extname command without any pre-configured scope." + }, + { + "description": "Enables the is_absolute command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-is-absolute", + "markdownDescription": "Enables the is_absolute command without any pre-configured scope." + }, + { + "description": "Enables the join command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-join", + "markdownDescription": "Enables the join command without any pre-configured scope." + }, + { + "description": "Enables the normalize command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-normalize", + "markdownDescription": "Enables the normalize command without any pre-configured scope." + }, + { + "description": "Enables the resolve command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-resolve", + "markdownDescription": "Enables the resolve command without any pre-configured scope." + }, + { + "description": "Enables the resolve_directory command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-resolve-directory", + "markdownDescription": "Enables the resolve_directory command without any pre-configured scope." + }, + { + "description": "Denies the basename command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-basename", + "markdownDescription": "Denies the basename command without any pre-configured scope." + }, + { + "description": "Denies the dirname command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-dirname", + "markdownDescription": "Denies the dirname command without any pre-configured scope." + }, + { + "description": "Denies the extname command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-extname", + "markdownDescription": "Denies the extname command without any pre-configured scope." + }, + { + "description": "Denies the is_absolute command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-is-absolute", + "markdownDescription": "Denies the is_absolute command without any pre-configured scope." + }, + { + "description": "Denies the join command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-join", + "markdownDescription": "Denies the join command without any pre-configured scope." + }, + { + "description": "Denies the normalize command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-normalize", + "markdownDescription": "Denies the normalize command without any pre-configured scope." + }, + { + "description": "Denies the resolve command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-resolve", + "markdownDescription": "Denies the resolve command without any pre-configured scope." + }, + { + "description": "Denies the resolve_directory command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-resolve-directory", + "markdownDescription": "Denies the resolve_directory command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`", + "type": "string", + "const": "core:resources:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`" + }, + { + "description": "Enables the close command without any pre-configured scope.", + "type": "string", + "const": "core:resources:allow-close", + "markdownDescription": "Enables the close command without any pre-configured scope." + }, + { + "description": "Denies the close command without any pre-configured scope.", + "type": "string", + "const": "core:resources:deny-close", + "markdownDescription": "Denies the close command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-show-menu-on-left-click`", + "type": "string", + "const": "core:tray:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-show-menu-on-left-click`" + }, + { + "description": "Enables the get_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-get-by-id", + "markdownDescription": "Enables the get_by_id command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the remove_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-remove-by-id", + "markdownDescription": "Enables the remove_by_id command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_icon_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon-as-template", + "markdownDescription": "Enables the set_icon_as_template command without any pre-configured scope." + }, + { + "description": "Enables the set_menu command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-menu", + "markdownDescription": "Enables the set_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_show_menu_on_left_click command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-show-menu-on-left-click", + "markdownDescription": "Enables the set_show_menu_on_left_click command without any pre-configured scope." + }, + { + "description": "Enables the set_temp_dir_path command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-temp-dir-path", + "markdownDescription": "Enables the set_temp_dir_path command without any pre-configured scope." + }, + { + "description": "Enables the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-title", + "markdownDescription": "Enables the set_title command without any pre-configured scope." + }, + { + "description": "Enables the set_tooltip command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-tooltip", + "markdownDescription": "Enables the set_tooltip command without any pre-configured scope." + }, + { + "description": "Enables the set_visible command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-visible", + "markdownDescription": "Enables the set_visible command without any pre-configured scope." + }, + { + "description": "Denies the get_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-get-by-id", + "markdownDescription": "Denies the get_by_id command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the remove_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-remove-by-id", + "markdownDescription": "Denies the remove_by_id command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_icon_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon-as-template", + "markdownDescription": "Denies the set_icon_as_template command without any pre-configured scope." + }, + { + "description": "Denies the set_menu command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-menu", + "markdownDescription": "Denies the set_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_show_menu_on_left_click command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-show-menu-on-left-click", + "markdownDescription": "Denies the set_show_menu_on_left_click command without any pre-configured scope." + }, + { + "description": "Denies the set_temp_dir_path command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-temp-dir-path", + "markdownDescription": "Denies the set_temp_dir_path command without any pre-configured scope." + }, + { + "description": "Denies the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-title", + "markdownDescription": "Denies the set_title command without any pre-configured scope." + }, + { + "description": "Denies the set_tooltip command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-tooltip", + "markdownDescription": "Denies the set_tooltip command without any pre-configured scope." + }, + { + "description": "Denies the set_visible command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-visible", + "markdownDescription": "Denies the set_visible command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`", + "type": "string", + "const": "core:webview:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`" + }, + { + "description": "Enables the clear_all_browsing_data command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-clear-all-browsing-data", + "markdownDescription": "Enables the clear_all_browsing_data command without any pre-configured scope." + }, + { + "description": "Enables the create_webview command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-create-webview", + "markdownDescription": "Enables the create_webview command without any pre-configured scope." + }, + { + "description": "Enables the create_webview_window command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-create-webview-window", + "markdownDescription": "Enables the create_webview_window command without any pre-configured scope." + }, + { + "description": "Enables the get_all_webviews command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-get-all-webviews", + "markdownDescription": "Enables the get_all_webviews command without any pre-configured scope." + }, + { + "description": "Enables the internal_toggle_devtools command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-internal-toggle-devtools", + "markdownDescription": "Enables the internal_toggle_devtools command without any pre-configured scope." + }, + { + "description": "Enables the print command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-print", + "markdownDescription": "Enables the print command without any pre-configured scope." + }, + { + "description": "Enables the reparent command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-reparent", + "markdownDescription": "Enables the reparent command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_auto_resize command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-auto-resize", + "markdownDescription": "Enables the set_webview_auto_resize command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-background-color", + "markdownDescription": "Enables the set_webview_background_color command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_focus command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-focus", + "markdownDescription": "Enables the set_webview_focus command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-position", + "markdownDescription": "Enables the set_webview_position command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-size", + "markdownDescription": "Enables the set_webview_size command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_zoom command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-zoom", + "markdownDescription": "Enables the set_webview_zoom command without any pre-configured scope." + }, + { + "description": "Enables the webview_close command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-close", + "markdownDescription": "Enables the webview_close command without any pre-configured scope." + }, + { + "description": "Enables the webview_hide command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-hide", + "markdownDescription": "Enables the webview_hide command without any pre-configured scope." + }, + { + "description": "Enables the webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-position", + "markdownDescription": "Enables the webview_position command without any pre-configured scope." + }, + { + "description": "Enables the webview_show command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-show", + "markdownDescription": "Enables the webview_show command without any pre-configured scope." + }, + { + "description": "Enables the webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-size", + "markdownDescription": "Enables the webview_size command without any pre-configured scope." + }, + { + "description": "Denies the clear_all_browsing_data command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-clear-all-browsing-data", + "markdownDescription": "Denies the clear_all_browsing_data command without any pre-configured scope." + }, + { + "description": "Denies the create_webview command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-create-webview", + "markdownDescription": "Denies the create_webview command without any pre-configured scope." + }, + { + "description": "Denies the create_webview_window command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-create-webview-window", + "markdownDescription": "Denies the create_webview_window command without any pre-configured scope." + }, + { + "description": "Denies the get_all_webviews command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-get-all-webviews", + "markdownDescription": "Denies the get_all_webviews command without any pre-configured scope." + }, + { + "description": "Denies the internal_toggle_devtools command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-internal-toggle-devtools", + "markdownDescription": "Denies the internal_toggle_devtools command without any pre-configured scope." + }, + { + "description": "Denies the print command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-print", + "markdownDescription": "Denies the print command without any pre-configured scope." + }, + { + "description": "Denies the reparent command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-reparent", + "markdownDescription": "Denies the reparent command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_auto_resize command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-auto-resize", + "markdownDescription": "Denies the set_webview_auto_resize command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-background-color", + "markdownDescription": "Denies the set_webview_background_color command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_focus command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-focus", + "markdownDescription": "Denies the set_webview_focus command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-position", + "markdownDescription": "Denies the set_webview_position command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-size", + "markdownDescription": "Denies the set_webview_size command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_zoom command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-zoom", + "markdownDescription": "Denies the set_webview_zoom command without any pre-configured scope." + }, + { + "description": "Denies the webview_close command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-close", + "markdownDescription": "Denies the webview_close command without any pre-configured scope." + }, + { + "description": "Denies the webview_hide command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-hide", + "markdownDescription": "Denies the webview_hide command without any pre-configured scope." + }, + { + "description": "Denies the webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-position", + "markdownDescription": "Denies the webview_position command without any pre-configured scope." + }, + { + "description": "Denies the webview_show command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-show", + "markdownDescription": "Denies the webview_show command without any pre-configured scope." + }, + { + "description": "Denies the webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-size", + "markdownDescription": "Denies the webview_size command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-internal-toggle-maximize`", + "type": "string", + "const": "core:window:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-internal-toggle-maximize`" + }, + { + "description": "Enables the available_monitors command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-available-monitors", + "markdownDescription": "Enables the available_monitors command without any pre-configured scope." + }, + { + "description": "Enables the center command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-center", + "markdownDescription": "Enables the center command without any pre-configured scope." + }, + { + "description": "Enables the close command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-close", + "markdownDescription": "Enables the close command without any pre-configured scope." + }, + { + "description": "Enables the create command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-create", + "markdownDescription": "Enables the create command without any pre-configured scope." + }, + { + "description": "Enables the current_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-current-monitor", + "markdownDescription": "Enables the current_monitor command without any pre-configured scope." + }, + { + "description": "Enables the cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-cursor-position", + "markdownDescription": "Enables the cursor_position command without any pre-configured scope." + }, + { + "description": "Enables the destroy command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-destroy", + "markdownDescription": "Enables the destroy command without any pre-configured scope." + }, + { + "description": "Enables the get_all_windows command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-get-all-windows", + "markdownDescription": "Enables the get_all_windows command without any pre-configured scope." + }, + { + "description": "Enables the hide command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-hide", + "markdownDescription": "Enables the hide command without any pre-configured scope." + }, + { + "description": "Enables the inner_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-inner-position", + "markdownDescription": "Enables the inner_position command without any pre-configured scope." + }, + { + "description": "Enables the inner_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-inner-size", + "markdownDescription": "Enables the inner_size command without any pre-configured scope." + }, + { + "description": "Enables the internal_toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-internal-toggle-maximize", + "markdownDescription": "Enables the internal_toggle_maximize command without any pre-configured scope." + }, + { + "description": "Enables the is_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-always-on-top", + "markdownDescription": "Enables the is_always_on_top command without any pre-configured scope." + }, + { + "description": "Enables the is_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-closable", + "markdownDescription": "Enables the is_closable command without any pre-configured scope." + }, + { + "description": "Enables the is_decorated command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-decorated", + "markdownDescription": "Enables the is_decorated command without any pre-configured scope." + }, + { + "description": "Enables the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-enabled", + "markdownDescription": "Enables the is_enabled command without any pre-configured scope." + }, + { + "description": "Enables the is_focused command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-focused", + "markdownDescription": "Enables the is_focused command without any pre-configured scope." + }, + { + "description": "Enables the is_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-fullscreen", + "markdownDescription": "Enables the is_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the is_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-maximizable", + "markdownDescription": "Enables the is_maximizable command without any pre-configured scope." + }, + { + "description": "Enables the is_maximized command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-maximized", + "markdownDescription": "Enables the is_maximized command without any pre-configured scope." + }, + { + "description": "Enables the is_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-minimizable", + "markdownDescription": "Enables the is_minimizable command without any pre-configured scope." + }, + { + "description": "Enables the is_minimized command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-minimized", + "markdownDescription": "Enables the is_minimized command without any pre-configured scope." + }, + { + "description": "Enables the is_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-resizable", + "markdownDescription": "Enables the is_resizable command without any pre-configured scope." + }, + { + "description": "Enables the is_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-visible", + "markdownDescription": "Enables the is_visible command without any pre-configured scope." + }, + { + "description": "Enables the maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-maximize", + "markdownDescription": "Enables the maximize command without any pre-configured scope." + }, + { + "description": "Enables the minimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-minimize", + "markdownDescription": "Enables the minimize command without any pre-configured scope." + }, + { + "description": "Enables the monitor_from_point command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-monitor-from-point", + "markdownDescription": "Enables the monitor_from_point command without any pre-configured scope." + }, + { + "description": "Enables the outer_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-outer-position", + "markdownDescription": "Enables the outer_position command without any pre-configured scope." + }, + { + "description": "Enables the outer_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-outer-size", + "markdownDescription": "Enables the outer_size command without any pre-configured scope." + }, + { + "description": "Enables the primary_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-primary-monitor", + "markdownDescription": "Enables the primary_monitor command without any pre-configured scope." + }, + { + "description": "Enables the request_user_attention command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-request-user-attention", + "markdownDescription": "Enables the request_user_attention command without any pre-configured scope." + }, + { + "description": "Enables the scale_factor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-scale-factor", + "markdownDescription": "Enables the scale_factor command without any pre-configured scope." + }, + { + "description": "Enables the set_always_on_bottom command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-always-on-bottom", + "markdownDescription": "Enables the set_always_on_bottom command without any pre-configured scope." + }, + { + "description": "Enables the set_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-always-on-top", + "markdownDescription": "Enables the set_always_on_top command without any pre-configured scope." + }, + { + "description": "Enables the set_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-background-color", + "markdownDescription": "Enables the set_background_color command without any pre-configured scope." + }, + { + "description": "Enables the set_badge_count command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-badge-count", + "markdownDescription": "Enables the set_badge_count command without any pre-configured scope." + }, + { + "description": "Enables the set_badge_label command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-badge-label", + "markdownDescription": "Enables the set_badge_label command without any pre-configured scope." + }, + { + "description": "Enables the set_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-closable", + "markdownDescription": "Enables the set_closable command without any pre-configured scope." + }, + { + "description": "Enables the set_content_protected command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-content-protected", + "markdownDescription": "Enables the set_content_protected command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_grab command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-grab", + "markdownDescription": "Enables the set_cursor_grab command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-icon", + "markdownDescription": "Enables the set_cursor_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-position", + "markdownDescription": "Enables the set_cursor_position command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-visible", + "markdownDescription": "Enables the set_cursor_visible command without any pre-configured scope." + }, + { + "description": "Enables the set_decorations command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-decorations", + "markdownDescription": "Enables the set_decorations command without any pre-configured scope." + }, + { + "description": "Enables the set_effects command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-effects", + "markdownDescription": "Enables the set_effects command without any pre-configured scope." + }, + { + "description": "Enables the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-enabled", + "markdownDescription": "Enables the set_enabled command without any pre-configured scope." + }, + { + "description": "Enables the set_focus command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-focus", + "markdownDescription": "Enables the set_focus command without any pre-configured scope." + }, + { + "description": "Enables the set_focusable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-focusable", + "markdownDescription": "Enables the set_focusable command without any pre-configured scope." + }, + { + "description": "Enables the set_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-fullscreen", + "markdownDescription": "Enables the set_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_ignore_cursor_events command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-ignore-cursor-events", + "markdownDescription": "Enables the set_ignore_cursor_events command without any pre-configured scope." + }, + { + "description": "Enables the set_max_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-max-size", + "markdownDescription": "Enables the set_max_size command without any pre-configured scope." + }, + { + "description": "Enables the set_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-maximizable", + "markdownDescription": "Enables the set_maximizable command without any pre-configured scope." + }, + { + "description": "Enables the set_min_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-min-size", + "markdownDescription": "Enables the set_min_size command without any pre-configured scope." + }, + { + "description": "Enables the set_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-minimizable", + "markdownDescription": "Enables the set_minimizable command without any pre-configured scope." + }, + { + "description": "Enables the set_overlay_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-overlay-icon", + "markdownDescription": "Enables the set_overlay_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-position", + "markdownDescription": "Enables the set_position command without any pre-configured scope." + }, + { + "description": "Enables the set_progress_bar command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-progress-bar", + "markdownDescription": "Enables the set_progress_bar command without any pre-configured scope." + }, + { + "description": "Enables the set_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-resizable", + "markdownDescription": "Enables the set_resizable command without any pre-configured scope." + }, + { + "description": "Enables the set_shadow command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-shadow", + "markdownDescription": "Enables the set_shadow command without any pre-configured scope." + }, + { + "description": "Enables the set_simple_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-simple-fullscreen", + "markdownDescription": "Enables the set_simple_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the set_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-size", + "markdownDescription": "Enables the set_size command without any pre-configured scope." + }, + { + "description": "Enables the set_size_constraints command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-size-constraints", + "markdownDescription": "Enables the set_size_constraints command without any pre-configured scope." + }, + { + "description": "Enables the set_skip_taskbar command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-skip-taskbar", + "markdownDescription": "Enables the set_skip_taskbar command without any pre-configured scope." + }, + { + "description": "Enables the set_theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-theme", + "markdownDescription": "Enables the set_theme command without any pre-configured scope." + }, + { + "description": "Enables the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-title", + "markdownDescription": "Enables the set_title command without any pre-configured scope." + }, + { + "description": "Enables the set_title_bar_style command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-title-bar-style", + "markdownDescription": "Enables the set_title_bar_style command without any pre-configured scope." + }, + { + "description": "Enables the set_visible_on_all_workspaces command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-visible-on-all-workspaces", + "markdownDescription": "Enables the set_visible_on_all_workspaces command without any pre-configured scope." + }, + { + "description": "Enables the show command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-show", + "markdownDescription": "Enables the show command without any pre-configured scope." + }, + { + "description": "Enables the start_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-start-dragging", + "markdownDescription": "Enables the start_dragging command without any pre-configured scope." + }, + { + "description": "Enables the start_resize_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-start-resize-dragging", + "markdownDescription": "Enables the start_resize_dragging command without any pre-configured scope." + }, + { + "description": "Enables the theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-theme", + "markdownDescription": "Enables the theme command without any pre-configured scope." + }, + { + "description": "Enables the title command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-title", + "markdownDescription": "Enables the title command without any pre-configured scope." + }, + { + "description": "Enables the toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-toggle-maximize", + "markdownDescription": "Enables the toggle_maximize command without any pre-configured scope." + }, + { + "description": "Enables the unmaximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-unmaximize", + "markdownDescription": "Enables the unmaximize command without any pre-configured scope." + }, + { + "description": "Enables the unminimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-unminimize", + "markdownDescription": "Enables the unminimize command without any pre-configured scope." + }, + { + "description": "Denies the available_monitors command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-available-monitors", + "markdownDescription": "Denies the available_monitors command without any pre-configured scope." + }, + { + "description": "Denies the center command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-center", + "markdownDescription": "Denies the center command without any pre-configured scope." + }, + { + "description": "Denies the close command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-close", + "markdownDescription": "Denies the close command without any pre-configured scope." + }, + { + "description": "Denies the create command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-create", + "markdownDescription": "Denies the create command without any pre-configured scope." + }, + { + "description": "Denies the current_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-current-monitor", + "markdownDescription": "Denies the current_monitor command without any pre-configured scope." + }, + { + "description": "Denies the cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-cursor-position", + "markdownDescription": "Denies the cursor_position command without any pre-configured scope." + }, + { + "description": "Denies the destroy command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-destroy", + "markdownDescription": "Denies the destroy command without any pre-configured scope." + }, + { + "description": "Denies the get_all_windows command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-get-all-windows", + "markdownDescription": "Denies the get_all_windows command without any pre-configured scope." + }, + { + "description": "Denies the hide command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-hide", + "markdownDescription": "Denies the hide command without any pre-configured scope." + }, + { + "description": "Denies the inner_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-inner-position", + "markdownDescription": "Denies the inner_position command without any pre-configured scope." + }, + { + "description": "Denies the inner_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-inner-size", + "markdownDescription": "Denies the inner_size command without any pre-configured scope." + }, + { + "description": "Denies the internal_toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-internal-toggle-maximize", + "markdownDescription": "Denies the internal_toggle_maximize command without any pre-configured scope." + }, + { + "description": "Denies the is_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-always-on-top", + "markdownDescription": "Denies the is_always_on_top command without any pre-configured scope." + }, + { + "description": "Denies the is_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-closable", + "markdownDescription": "Denies the is_closable command without any pre-configured scope." + }, + { + "description": "Denies the is_decorated command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-decorated", + "markdownDescription": "Denies the is_decorated command without any pre-configured scope." + }, + { + "description": "Denies the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-enabled", + "markdownDescription": "Denies the is_enabled command without any pre-configured scope." + }, + { + "description": "Denies the is_focused command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-focused", + "markdownDescription": "Denies the is_focused command without any pre-configured scope." + }, + { + "description": "Denies the is_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-fullscreen", + "markdownDescription": "Denies the is_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the is_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-maximizable", + "markdownDescription": "Denies the is_maximizable command without any pre-configured scope." + }, + { + "description": "Denies the is_maximized command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-maximized", + "markdownDescription": "Denies the is_maximized command without any pre-configured scope." + }, + { + "description": "Denies the is_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-minimizable", + "markdownDescription": "Denies the is_minimizable command without any pre-configured scope." + }, + { + "description": "Denies the is_minimized command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-minimized", + "markdownDescription": "Denies the is_minimized command without any pre-configured scope." + }, + { + "description": "Denies the is_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-resizable", + "markdownDescription": "Denies the is_resizable command without any pre-configured scope." + }, + { + "description": "Denies the is_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-visible", + "markdownDescription": "Denies the is_visible command without any pre-configured scope." + }, + { + "description": "Denies the maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-maximize", + "markdownDescription": "Denies the maximize command without any pre-configured scope." + }, + { + "description": "Denies the minimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-minimize", + "markdownDescription": "Denies the minimize command without any pre-configured scope." + }, + { + "description": "Denies the monitor_from_point command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-monitor-from-point", + "markdownDescription": "Denies the monitor_from_point command without any pre-configured scope." + }, + { + "description": "Denies the outer_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-outer-position", + "markdownDescription": "Denies the outer_position command without any pre-configured scope." + }, + { + "description": "Denies the outer_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-outer-size", + "markdownDescription": "Denies the outer_size command without any pre-configured scope." + }, + { + "description": "Denies the primary_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-primary-monitor", + "markdownDescription": "Denies the primary_monitor command without any pre-configured scope." + }, + { + "description": "Denies the request_user_attention command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-request-user-attention", + "markdownDescription": "Denies the request_user_attention command without any pre-configured scope." + }, + { + "description": "Denies the scale_factor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-scale-factor", + "markdownDescription": "Denies the scale_factor command without any pre-configured scope." + }, + { + "description": "Denies the set_always_on_bottom command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-always-on-bottom", + "markdownDescription": "Denies the set_always_on_bottom command without any pre-configured scope." + }, + { + "description": "Denies the set_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-always-on-top", + "markdownDescription": "Denies the set_always_on_top command without any pre-configured scope." + }, + { + "description": "Denies the set_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-background-color", + "markdownDescription": "Denies the set_background_color command without any pre-configured scope." + }, + { + "description": "Denies the set_badge_count command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-badge-count", + "markdownDescription": "Denies the set_badge_count command without any pre-configured scope." + }, + { + "description": "Denies the set_badge_label command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-badge-label", + "markdownDescription": "Denies the set_badge_label command without any pre-configured scope." + }, + { + "description": "Denies the set_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-closable", + "markdownDescription": "Denies the set_closable command without any pre-configured scope." + }, + { + "description": "Denies the set_content_protected command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-content-protected", + "markdownDescription": "Denies the set_content_protected command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_grab command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-grab", + "markdownDescription": "Denies the set_cursor_grab command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-icon", + "markdownDescription": "Denies the set_cursor_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-position", + "markdownDescription": "Denies the set_cursor_position command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-visible", + "markdownDescription": "Denies the set_cursor_visible command without any pre-configured scope." + }, + { + "description": "Denies the set_decorations command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-decorations", + "markdownDescription": "Denies the set_decorations command without any pre-configured scope." + }, + { + "description": "Denies the set_effects command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-effects", + "markdownDescription": "Denies the set_effects command without any pre-configured scope." + }, + { + "description": "Denies the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-enabled", + "markdownDescription": "Denies the set_enabled command without any pre-configured scope." + }, + { + "description": "Denies the set_focus command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-focus", + "markdownDescription": "Denies the set_focus command without any pre-configured scope." + }, + { + "description": "Denies the set_focusable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-focusable", + "markdownDescription": "Denies the set_focusable command without any pre-configured scope." + }, + { + "description": "Denies the set_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-fullscreen", + "markdownDescription": "Denies the set_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_ignore_cursor_events command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-ignore-cursor-events", + "markdownDescription": "Denies the set_ignore_cursor_events command without any pre-configured scope." + }, + { + "description": "Denies the set_max_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-max-size", + "markdownDescription": "Denies the set_max_size command without any pre-configured scope." + }, + { + "description": "Denies the set_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-maximizable", + "markdownDescription": "Denies the set_maximizable command without any pre-configured scope." + }, + { + "description": "Denies the set_min_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-min-size", + "markdownDescription": "Denies the set_min_size command without any pre-configured scope." + }, + { + "description": "Denies the set_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-minimizable", + "markdownDescription": "Denies the set_minimizable command without any pre-configured scope." + }, + { + "description": "Denies the set_overlay_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-overlay-icon", + "markdownDescription": "Denies the set_overlay_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-position", + "markdownDescription": "Denies the set_position command without any pre-configured scope." + }, + { + "description": "Denies the set_progress_bar command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-progress-bar", + "markdownDescription": "Denies the set_progress_bar command without any pre-configured scope." + }, + { + "description": "Denies the set_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-resizable", + "markdownDescription": "Denies the set_resizable command without any pre-configured scope." + }, + { + "description": "Denies the set_shadow command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-shadow", + "markdownDescription": "Denies the set_shadow command without any pre-configured scope." + }, + { + "description": "Denies the set_simple_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-simple-fullscreen", + "markdownDescription": "Denies the set_simple_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the set_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-size", + "markdownDescription": "Denies the set_size command without any pre-configured scope." + }, + { + "description": "Denies the set_size_constraints command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-size-constraints", + "markdownDescription": "Denies the set_size_constraints command without any pre-configured scope." + }, + { + "description": "Denies the set_skip_taskbar command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-skip-taskbar", + "markdownDescription": "Denies the set_skip_taskbar command without any pre-configured scope." + }, + { + "description": "Denies the set_theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-theme", + "markdownDescription": "Denies the set_theme command without any pre-configured scope." + }, + { + "description": "Denies the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-title", + "markdownDescription": "Denies the set_title command without any pre-configured scope." + }, + { + "description": "Denies the set_title_bar_style command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-title-bar-style", + "markdownDescription": "Denies the set_title_bar_style command without any pre-configured scope." + }, + { + "description": "Denies the set_visible_on_all_workspaces command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-visible-on-all-workspaces", + "markdownDescription": "Denies the set_visible_on_all_workspaces command without any pre-configured scope." + }, + { + "description": "Denies the show command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-show", + "markdownDescription": "Denies the show command without any pre-configured scope." + }, + { + "description": "Denies the start_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-start-dragging", + "markdownDescription": "Denies the start_dragging command without any pre-configured scope." + }, + { + "description": "Denies the start_resize_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-start-resize-dragging", + "markdownDescription": "Denies the start_resize_dragging command without any pre-configured scope." + }, + { + "description": "Denies the theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-theme", + "markdownDescription": "Denies the theme command without any pre-configured scope." + }, + { + "description": "Denies the title command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-title", + "markdownDescription": "Denies the title command without any pre-configured scope." + }, + { + "description": "Denies the toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-toggle-maximize", + "markdownDescription": "Denies the toggle_maximize command without any pre-configured scope." + }, + { + "description": "Denies the unmaximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-unmaximize", + "markdownDescription": "Denies the unmaximize command without any pre-configured scope." + }, + { + "description": "Denies the unminimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-unminimize", + "markdownDescription": "Denies the unminimize command without any pre-configured scope." + }, + { + "description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-ask`\n- `allow-confirm`\n- `allow-message`\n- `allow-save`\n- `allow-open`", + "type": "string", + "const": "dialog:default", + "markdownDescription": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-ask`\n- `allow-confirm`\n- `allow-message`\n- `allow-save`\n- `allow-open`" + }, + { + "description": "Enables the ask command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-ask", + "markdownDescription": "Enables the ask command without any pre-configured scope." + }, + { + "description": "Enables the confirm command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-confirm", + "markdownDescription": "Enables the confirm command without any pre-configured scope." + }, + { + "description": "Enables the message command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-message", + "markdownDescription": "Enables the message command without any pre-configured scope." + }, + { + "description": "Enables the open command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-open", + "markdownDescription": "Enables the open command without any pre-configured scope." + }, + { + "description": "Enables the save command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-save", + "markdownDescription": "Enables the save command without any pre-configured scope." + }, + { + "description": "Denies the ask command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-ask", + "markdownDescription": "Denies the ask command without any pre-configured scope." + }, + { + "description": "Denies the confirm command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-confirm", + "markdownDescription": "Denies the confirm command without any pre-configured scope." + }, + { + "description": "Denies the message command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-message", + "markdownDescription": "Denies the message command without any pre-configured scope." + }, + { + "description": "Denies the open command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-open", + "markdownDescription": "Denies the open command without any pre-configured scope." + }, + { + "description": "Denies the save command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-save", + "markdownDescription": "Denies the save command without any pre-configured scope." + }, + { + "description": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-default-urls`", + "type": "string", + "const": "opener:default", + "markdownDescription": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-default-urls`" + }, + { + "description": "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application.", + "type": "string", + "const": "opener:allow-default-urls", + "markdownDescription": "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application." + }, + { + "description": "Enables the open_path command without any pre-configured scope.", + "type": "string", + "const": "opener:allow-open-path", + "markdownDescription": "Enables the open_path command without any pre-configured scope." + }, + { + "description": "Enables the open_url command without any pre-configured scope.", + "type": "string", + "const": "opener:allow-open-url", + "markdownDescription": "Enables the open_url command without any pre-configured scope." + }, + { + "description": "Enables the reveal_item_in_dir command without any pre-configured scope.", + "type": "string", + "const": "opener:allow-reveal-item-in-dir", + "markdownDescription": "Enables the reveal_item_in_dir command without any pre-configured scope." + }, + { + "description": "Denies the open_path command without any pre-configured scope.", + "type": "string", + "const": "opener:deny-open-path", + "markdownDescription": "Denies the open_path command without any pre-configured scope." + }, + { + "description": "Denies the open_url command without any pre-configured scope.", + "type": "string", + "const": "opener:deny-open-url", + "markdownDescription": "Denies the open_url command without any pre-configured scope." + }, + { + "description": "Denies the reveal_item_in_dir command without any pre-configured scope.", + "type": "string", + "const": "opener:deny-reveal-item-in-dir", + "markdownDescription": "Denies the reveal_item_in_dir command without any pre-configured scope." + } + ] + }, + "Value": { + "description": "All supported ACL values.", + "anyOf": [ + { + "description": "Represents a null JSON value.", + "type": "null" + }, + { + "description": "Represents a [`bool`].", + "type": "boolean" + }, + { + "description": "Represents a valid ACL [`Number`].", + "allOf": [ + { + "$ref": "#/definitions/Number" + } + ] + }, + { + "description": "Represents a [`String`].", + "type": "string" + }, + { + "description": "Represents a list of other [`Value`]s.", + "type": "array", + "items": { + "$ref": "#/definitions/Value" + } + }, + { + "description": "Represents a map of [`String`] keys to [`Value`]s.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Value" + } + } + ] + }, + "Number": { + "description": "A valid ACL number.", + "anyOf": [ + { + "description": "Represents an [`i64`].", + "type": "integer", + "format": "int64" + }, + { + "description": "Represents a [`f64`].", + "type": "number", + "format": "double" + } + ] + }, + "Target": { + "description": "Platform target.", + "oneOf": [ + { + "description": "MacOS.", + "type": "string", + "enum": [ + "macOS" + ] + }, + { + "description": "Windows.", + "type": "string", + "enum": [ + "windows" + ] + }, + { + "description": "Linux.", + "type": "string", + "enum": [ + "linux" + ] + }, + { + "description": "Android.", + "type": "string", + "enum": [ + "android" + ] + }, + { + "description": "iOS.", + "type": "string", + "enum": [ + "iOS" + ] + } + ] + }, + "Application": { + "description": "Opener scope application.", + "anyOf": [ + { + "description": "Open in default application.", + "type": "null" + }, + { + "description": "If true, allow open with any application.", + "type": "boolean" + }, + { + "description": "Allow specific application to open with.", + "type": "string" + } + ] + } + } +} \ No newline at end of file diff --git a/packages/tauri-app/src-tauri/icon.icns b/packages/tauri-app/src-tauri/icon.icns new file mode 100644 index 0000000..4431bb4 Binary files /dev/null and b/packages/tauri-app/src-tauri/icon.icns differ diff --git a/packages/tauri-app/src-tauri/icon.ico b/packages/tauri-app/src-tauri/icon.ico new file mode 100644 index 0000000..7d50243 Binary files /dev/null and b/packages/tauri-app/src-tauri/icon.ico differ diff --git a/packages/tauri-app/src-tauri/icon.png b/packages/tauri-app/src-tauri/icon.png new file mode 100644 index 0000000..4c08915 Binary files /dev/null and b/packages/tauri-app/src-tauri/icon.png differ diff --git a/packages/tauri-app/src-tauri/src/cli_manager.rs b/packages/tauri-app/src-tauri/src/cli_manager.rs new file mode 100644 index 0000000..6b43574 --- /dev/null +++ b/packages/tauri-app/src-tauri/src/cli_manager.rs @@ -0,0 +1,712 @@ +use dirs::home_dir; +use parking_lot::Mutex; +use regex::Regex; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use std::collections::VecDeque; +use std::env; +use std::ffi::OsStr; +use std::fs; +use std::io::{BufRead, BufReader}; +use std::path::PathBuf; +use std::process::{Child, Command, Stdio}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::thread; +use std::time::{Duration, Instant}; +use tauri::{AppHandle, Emitter, Manager, Url}; + +fn log_line(message: &str) { + println!("[tauri-cli] {message}"); +} + +fn workspace_root() -> Option { + std::env::current_dir().ok().and_then(|mut dir| { + for _ in 0..3 { + if let Some(parent) = dir.parent() { + dir = parent.to_path_buf(); + } + } + Some(dir) + }) +} + +fn navigate_main(app: &AppHandle, url: &str) { + if let Some(win) = app.webview_windows().get("main") { + log_line(&format!("navigating main to {url}")); + if let Ok(parsed) = Url::parse(url) { + let _ = win.navigate(parsed); + } else { + log_line("failed to parse URL for navigation"); + } + } else { + log_line("main window not found for navigation"); + } +} + +const DEFAULT_CONFIG_PATH: &str = "~/.config/codenomad/config.json"; + +#[derive(Debug, Deserialize)] +struct PreferencesConfig { + #[serde(rename = "listeningMode")] + listening_mode: Option, +} + +#[derive(Debug, Deserialize)] +struct AppConfig { + preferences: Option, +} + +fn resolve_config_path() -> PathBuf { + let raw = env::var("CLI_CONFIG") + .ok() + .filter(|value| !value.trim().is_empty()) + .unwrap_or_else(|| DEFAULT_CONFIG_PATH.to_string()); + expand_home(&raw) +} + +fn expand_home(path: &str) -> PathBuf { + if path.starts_with("~/") { + if let Some(home) = home_dir().or_else(|| env::var("HOME").ok().map(PathBuf::from)) { + return home.join(path.trim_start_matches("~/")); + } + } + PathBuf::from(path) +} + +fn resolve_listening_mode() -> String { + let path = resolve_config_path(); + if let Ok(content) = fs::read_to_string(path) { + if let Ok(config) = serde_json::from_str::(&content) { + if let Some(mode) = config + .preferences + .as_ref() + .and_then(|prefs| prefs.listening_mode.as_ref()) + { + if mode == "local" { + return "local".to_string(); + } + if mode == "all" { + return "all".to_string(); + } + } + } + } + "local".to_string() +} + +fn resolve_listening_host() -> String { + let mode = resolve_listening_mode(); + if mode == "local" { + "127.0.0.1".to_string() + } else { + "0.0.0.0".to_string() + } +} + +#[derive(Debug, Clone, Serialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum CliState { + Starting, + Ready, + Error, + Stopped, +} + +#[derive(Debug, Clone, Serialize)] +pub struct CliStatus { + pub state: CliState, + pub pid: Option, + pub port: Option, + pub url: Option, + pub error: Option, +} + +impl Default for CliStatus { + fn default() -> Self { + Self { + state: CliState::Stopped, + pid: None, + port: None, + url: None, + error: None, + } + } +} + +#[derive(Debug, Clone)] +pub struct CliProcessManager { + status: Arc>, + child: Arc>>, + ready: Arc, +} + +impl CliProcessManager { + pub fn new() -> Self { + Self { + status: Arc::new(Mutex::new(CliStatus::default())), + child: Arc::new(Mutex::new(None)), + ready: Arc::new(AtomicBool::new(false)), + } + } + + pub fn start(&self, app: AppHandle, dev: bool) -> anyhow::Result<()> { + log_line(&format!("start requested (dev={dev})")); + self.stop()?; + self.ready.store(false, Ordering::SeqCst); + { + let mut status = self.status.lock(); + status.state = CliState::Starting; + status.port = None; + status.url = None; + status.error = None; + status.pid = None; + } + Self::emit_status(&app, &self.status.lock()); + + let status_arc = self.status.clone(); + let child_arc = self.child.clone(); + let ready_flag = self.ready.clone(); + thread::spawn(move || { + if let Err(err) = Self::spawn_cli(app.clone(), status_arc.clone(), child_arc, ready_flag, dev) { + log_line(&format!("cli spawn failed: {err}")); + let mut locked = status_arc.lock(); + locked.state = CliState::Error; + locked.error = Some(err.to_string()); + let snapshot = locked.clone(); + drop(locked); + let _ = app.emit("cli:error", json!({"message": err.to_string()})); + let _ = app.emit("cli:status", snapshot); + } + }); + + Ok(()) + } + + pub fn stop(&self) -> anyhow::Result<()> { + let mut child_opt = self.child.lock(); + if let Some(mut child) = child_opt.take() { + #[cfg(unix)] + unsafe { + libc::kill(child.id() as i32, libc::SIGTERM); + } + #[cfg(windows)] + { + let _ = child.kill(); + } + + let start = Instant::now(); + loop { + match child.try_wait() { + Ok(Some(_)) => break, + Ok(None) => { + if start.elapsed() > Duration::from_secs(4) { + #[cfg(unix)] + unsafe { + libc::kill(child.id() as i32, libc::SIGKILL); + } + #[cfg(windows)] + { + let _ = child.kill(); + } + break; + } + thread::sleep(Duration::from_millis(50)); + } + Err(_) => break, + } + } + } + + let mut status = self.status.lock(); + status.state = CliState::Stopped; + status.pid = None; + status.port = None; + status.url = None; + status.error = None; + + Ok(()) + } + + pub fn status(&self) -> CliStatus { + self.status.lock().clone() + } + + fn spawn_cli( + app: AppHandle, + status: Arc>, + child_holder: Arc>>, + ready: Arc, + dev: bool, + ) -> anyhow::Result<()> { + log_line("resolving CLI entry"); + let resolution = CliEntry::resolve(&app, dev)?; + let host = resolve_listening_host(); + log_line(&format!( + "resolved CLI entry runner={:?} entry={} host={}", + resolution.runner, resolution.entry, host + )); + let args = resolution.build_args(dev, &host); + log_line(&format!("CLI args: {:?}", args)); + if dev { + log_line("development mode: will prefer tsx + source if present"); + } + + let cwd = workspace_root(); + if let Some(ref c) = cwd { + log_line(&format!("using cwd={}", c.display())); + } + + let command_info = if supports_user_shell() { + log_line("spawning via user shell"); + ShellCommandType::UserShell(build_shell_command_string(&resolution, &args)?) + } else { + log_line("spawning directly with node"); + ShellCommandType::Direct(DirectCommand { + program: resolution.node_binary.clone(), + args: resolution.runner_args(&args), + }) + }; + + if !supports_user_shell() { + if which::which(&resolution.node_binary).is_err() { + return Err(anyhow::anyhow!("Node binary not found. Make sure Node.js is installed.")); + } + } + + let child = match &command_info { + ShellCommandType::UserShell(cmd) => { + log_line(&format!("spawn command: {} {:?}", cmd.shell, cmd.args)); + let mut c = Command::new(&cmd.shell); + c.args(&cmd.args) + .env("ELECTRON_RUN_AS_NODE", "1") + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + if let Some(ref cwd) = cwd { + c.current_dir(cwd); + } + c.spawn()? + } + ShellCommandType::Direct(cmd) => { + log_line(&format!("spawn command: {} {:?}", cmd.program, cmd.args)); + let mut c = Command::new(&cmd.program); + c.args(&cmd.args) + .env("ELECTRON_RUN_AS_NODE", "1") + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + if let Some(ref cwd) = cwd { + c.current_dir(cwd); + } + c.spawn()? + } + }; + + let pid = child.id(); + log_line(&format!("spawned pid={pid}")); + { + let mut locked = status.lock(); + locked.pid = Some(pid); + } + Self::emit_status(&app, &status.lock()); + + { + let mut holder = child_holder.lock(); + *holder = Some(child); + } + + let child_clone = child_holder.clone(); + let status_clone = status.clone(); + let app_clone = app.clone(); + let ready_clone = ready.clone(); + + thread::spawn(move || { + let stdout = child_clone + .lock() + .as_mut() + .and_then(|c| c.stdout.take()) + .map(BufReader::new); + let stderr = child_clone + .lock() + .as_mut() + .and_then(|c| c.stderr.take()) + .map(BufReader::new); + + if let Some(reader) = stdout { + Self::process_stream(reader, "stdout", &app_clone, &status_clone, &ready_clone); + } + if let Some(reader) = stderr { + Self::process_stream(reader, "stderr", &app_clone, &status_clone, &ready_clone); + } + }); + + let app_clone = app.clone(); + let status_clone = status.clone(); + let ready_clone = ready.clone(); + let child_holder_clone = child_holder.clone(); + thread::spawn(move || { + let timeout = Duration::from_secs(60); + thread::sleep(timeout); + if ready_clone.load(Ordering::SeqCst) { + return; + } + let mut locked = status_clone.lock(); + locked.state = CliState::Error; + locked.error = Some("CLI did not start in time".to_string()); + log_line("timeout waiting for CLI readiness"); + if let Some(child) = child_holder_clone.lock().as_mut() { + let _ = child.kill(); + } + let _ = app_clone.emit("cli:error", json!({"message": "CLI did not start in time"})); + Self::emit_status(&app_clone, &locked); + }); + + let status_clone = status.clone(); + let app_clone = app.clone(); + thread::spawn(move || { + let code = { + let mut guard = child_holder.lock(); + if let Some(child) = guard.as_mut() { + child.wait().ok() + } else { + None + } + }; + + let mut locked = status_clone.lock(); + let failed = locked.state != CliState::Ready; + let err_msg = if failed { + Some(match code { + Some(status) => format!("CLI exited early: {status}"), + None => "CLI exited early".to_string(), + }) + } else { + None + }; + + if failed { + locked.state = CliState::Error; + if locked.error.is_none() { + locked.error = err_msg.clone(); + } + log_line(&format!("cli process exited before ready: {:?}", locked.error)); + let _ = app_clone.emit("cli:error", json!({"message": locked.error.clone().unwrap_or_default()})); + } else { + locked.state = CliState::Stopped; + log_line("cli process stopped cleanly"); + } + + Self::emit_status(&app_clone, &locked); + }); + + Ok(()) + } + + fn process_stream( + mut reader: R, + stream: &str, + app: &AppHandle, + status: &Arc>, + ready: &Arc, + ) { + let mut buffer = String::new(); + let port_regex = Regex::new(r"CodeNomad Server is ready at http://[^:]+:(\d+)").ok(); + let http_regex = Regex::new(r":(\d{2,5})(?!.*:\d)").ok(); + + loop { + buffer.clear(); + match reader.read_line(&mut buffer) { + Ok(0) => break, + Ok(_) => { + let line = buffer.trim_end(); + if !line.is_empty() { + log_line(&format!("[cli][{}] {}", stream, line)); + + if ready.load(Ordering::SeqCst) { + continue; + } + + if let Some(port) = port_regex + .as_ref() + .and_then(|re| re.captures(line).and_then(|c| c.get(1))) + .and_then(|m| m.as_str().parse::().ok()) + { + Self::mark_ready(app, status, ready, port); + continue; + } + + if line.to_lowercase().contains("http server listening") { + if let Some(port) = http_regex + .as_ref() + .and_then(|re| re.captures(line).and_then(|c| c.get(1))) + .and_then(|m| m.as_str().parse::().ok()) + { + Self::mark_ready(app, status, ready, port); + continue; + } + + if let Ok(value) = serde_json::from_str::(line) { + if let Some(port) = value.get("port").and_then(|p| p.as_u64()) { + Self::mark_ready(app, status, ready, port as u16); + continue; + } + } + } + } + } + Err(_) => break, + } + } + } + + fn mark_ready(app: &AppHandle, status: &Arc>, ready: &Arc, port: u16) { + ready.store(true, Ordering::SeqCst); + let mut locked = status.lock(); + let url = format!("http://127.0.0.1:{port}"); + locked.port = Some(port); + locked.url = Some(url.clone()); + locked.state = CliState::Ready; + locked.error = None; + log_line(&format!("cli ready on {url}")); + navigate_main(app, &url); + let _ = app.emit("cli:ready", locked.clone()); + Self::emit_status(app, &locked); + } + + fn emit_status(app: &AppHandle, status: &CliStatus) { + let _ = app.emit("cli:status", status.clone()); + } +} + +fn supports_user_shell() -> bool { + cfg!(unix) +} + +#[derive(Debug)] +struct ShellCommand { + shell: String, + args: Vec, +} + +#[derive(Debug)] +struct DirectCommand { + program: String, + args: Vec, +} + +#[derive(Debug)] +enum ShellCommandType { + UserShell(ShellCommand), + Direct(DirectCommand), +} + +#[derive(Debug)] +struct CliEntry { + entry: String, + runner: Runner, + runner_path: Option, + node_binary: String, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Runner { + Node, + Tsx, +} + +impl CliEntry { + fn resolve(app: &AppHandle, dev: bool) -> anyhow::Result { + let node_binary = std::env::var("NODE_BINARY").unwrap_or_else(|_| "node".to_string()); + + if dev { + if let Some(tsx_path) = resolve_tsx(app) { + if let Some(entry) = resolve_dev_entry(app) { + return Ok(Self { + entry, + runner: Runner::Tsx, + runner_path: Some(tsx_path), + node_binary, + }); + } + } + } + + if let Some(entry) = resolve_dist_entry(app) { + return Ok(Self { + entry, + runner: Runner::Node, + runner_path: None, + node_binary, + }); + } + + Err(anyhow::anyhow!( + "Unable to locate CodeNomad CLI build (dist/bin.js). Please build @neuralnomads/codenomad." + )) + } + + fn build_args(&self, dev: bool, host: &str) -> Vec { + let mut args = vec![ + "serve".to_string(), + "--host".to_string(), + host.to_string(), + "--port".to_string(), + "0".to_string(), + ]; + if dev { + args.push("--ui-dev-server".to_string()); + args.push("http://localhost:3000".to_string()); + args.push("--log-level".to_string()); + args.push("debug".to_string()); + } + args + } + + fn runner_args(&self, cli_args: &[String]) -> Vec { + let mut args = VecDeque::new(); + if self.runner == Runner::Tsx { + if let Some(path) = &self.runner_path { + args.push_back(path.clone()); + } + } + args.push_back(self.entry.clone()); + for arg in cli_args { + args.push_back(arg.clone()); + } + args.into_iter().collect() + } +} + +fn resolve_tsx(_app: &AppHandle) -> Option { + let candidates = vec![ + std::env::current_dir() + .ok() + .map(|p| p.join("node_modules/tsx/dist/cli.js")), + std::env::current_exe() + .ok() + .and_then(|ex| ex.parent().map(|p| p.join("../node_modules/tsx/dist/cli.js"))), + ]; + + first_existing(candidates) +} + +fn resolve_dev_entry(_app: &AppHandle) -> Option { + let candidates = vec![ + std::env::current_dir() + .ok() + .map(|p| p.join("packages/server/src/index.ts")), + std::env::current_dir() + .ok() + .map(|p| p.join("../server/src/index.ts")), + ]; + + first_existing(candidates) +} + +fn resolve_dist_entry(_app: &AppHandle) -> Option { + let base = workspace_root(); + let mut candidates: Vec> = vec![ + base.as_ref().map(|p| p.join("packages/server/dist/bin.js")), + base.as_ref().map(|p| p.join("packages/server/dist/index.js")), + base.as_ref().map(|p| p.join("server/dist/bin.js")), + base.as_ref().map(|p| p.join("server/dist/index.js")), + ]; + + if let Ok(exe) = std::env::current_exe() { + if let Some(dir) = exe.parent() { + let resources = dir.join("../Resources"); + candidates.push(Some(resources.join("server/dist/bin.js"))); + candidates.push(Some(resources.join("server/dist/index.js"))); + candidates.push(Some(resources.join("server/dist/server/bin.js"))); + candidates.push(Some(resources.join("server/dist/server/index.js"))); + candidates.push(Some(resources.join("resources/server/dist/bin.js"))); + candidates.push(Some(resources.join("resources/server/dist/index.js"))); + candidates.push(Some(resources.join("resources/server/dist/server/bin.js"))); + candidates.push(Some(resources.join("resources/server/dist/server/index.js"))); + + let linux_resource_roots = [dir.join("../lib/CodeNomad"), dir.join("../lib/codenomad")]; + for root in linux_resource_roots { + candidates.push(Some(root.join("server/dist/bin.js"))); + candidates.push(Some(root.join("server/dist/index.js"))); + candidates.push(Some(root.join("server/dist/server/bin.js"))); + candidates.push(Some(root.join("server/dist/server/index.js"))); + candidates.push(Some(root.join("resources/server/dist/bin.js"))); + candidates.push(Some(root.join("resources/server/dist/index.js"))); + candidates.push(Some(root.join("resources/server/dist/server/bin.js"))); + candidates.push(Some(root.join("resources/server/dist/server/index.js"))); + } + } + } + + first_existing(candidates) +} + +fn build_shell_command_string(entry: &CliEntry, cli_args: &[String]) -> anyhow::Result { + + let shell = default_shell(); + let mut quoted: Vec = Vec::new(); + quoted.push(shell_escape(&entry.node_binary)); + for arg in entry.runner_args(cli_args) { + quoted.push(shell_escape(&arg)); + } + let command = format!("ELECTRON_RUN_AS_NODE=1 exec {}", quoted.join(" ")); + let args = build_shell_args(&shell, &command); + log_line(&format!("user shell command: {} {:?}", shell, args)); + Ok(ShellCommand { shell, args }) +} + +fn default_shell() -> String { + if let Ok(shell) = std::env::var("SHELL") { + if !shell.trim().is_empty() { + return shell; + } + } + if cfg!(target_os = "macos") { + "/bin/zsh".to_string() + } else { + "/bin/bash".to_string() + } +} + +fn shell_escape(input: &str) -> String { + if input.is_empty() { + "''".to_string() + } else if !input + .chars() + .any(|c| matches!(c, ' ' | '"' | '\'' | '$' | '`' | '!' )) + { + input.to_string() + } else { + let escaped = input.replace('\'', "'\\''"); + format!("'{}'", escaped) + } +} + +fn build_shell_args(shell: &str, command: &str) -> Vec { + let shell_name = std::path::Path::new(shell) + .file_name() + .and_then(OsStr::to_str) + .unwrap_or("") + .to_lowercase(); + + if shell_name.contains("zsh") { + vec!["-l".into(), "-i".into(), "-c".into(), command.into()] + } else { + vec!["-l".into(), "-c".into(), command.into()] + } +} + +fn first_existing(paths: Vec>) -> Option { + paths + .into_iter() + .flatten() + .find(|p| p.exists()) + .map(|p| normalize_path(p)) +} + +fn normalize_path(path: PathBuf) -> String { + if let Ok(clean) = path.canonicalize() { + clean.to_string_lossy().to_string() + } else { + path.to_string_lossy().to_string() + } +} diff --git a/packages/tauri-app/src-tauri/src/main.rs b/packages/tauri-app/src-tauri/src/main.rs new file mode 100644 index 0000000..e92cce9 --- /dev/null +++ b/packages/tauri-app/src-tauri/src/main.rs @@ -0,0 +1,267 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +mod cli_manager; + +use cli_manager::{CliProcessManager, CliStatus}; +use serde_json::json; +use tauri::menu::{MenuBuilder, MenuItem, SubmenuBuilder}; +use tauri::plugin::{Builder as PluginBuilder, TauriPlugin}; +use tauri::webview::Webview; +use tauri::{AppHandle, Emitter, Manager, Runtime, Wry}; +use tauri_plugin_opener::OpenerExt; +use url::Url; + +#[derive(Clone)] +pub struct AppState { + pub manager: CliProcessManager, +} + +#[tauri::command] +fn cli_get_status(state: tauri::State) -> CliStatus { + state.manager.status() +} + +#[tauri::command] +fn cli_restart(app: AppHandle, state: tauri::State) -> Result { + let dev_mode = is_dev_mode(); + state.manager.stop().map_err(|e| e.to_string())?; + state + .manager + .start(app, dev_mode) + .map_err(|e| e.to_string())?; + Ok(state.manager.status()) +} + +fn is_dev_mode() -> bool { + cfg!(debug_assertions) || std::env::var("TAURI_DEV").is_ok() +} + +fn should_allow_internal(url: &Url) -> bool { + match url.scheme() { + "tauri" | "asset" | "file" => true, + "http" | "https" => matches!(url.host_str(), Some("127.0.0.1" | "localhost")), + _ => false, + } +} + +fn intercept_navigation(webview: &Webview, url: &Url) -> bool { + if should_allow_internal(url) { + return true; + } + + if let Err(err) = webview + .app_handle() + .opener() + .open_url(url.as_str(), None::<&str>) + { + eprintln!("[tauri] failed to open external link {}: {}", url, err); + } + false +} + +fn main() { + let navigation_guard: TauriPlugin = PluginBuilder::new("external-link-guard") + .on_navigation(|webview, url| intercept_navigation(webview, url)) + .build(); + + tauri::Builder::default() + .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_opener::init()) + .plugin(navigation_guard) + .manage(AppState { + manager: CliProcessManager::new(), + }) + .setup(|app| { + build_menu(&app.handle())?; + let dev_mode = is_dev_mode(); + let app_handle = app.handle().clone(); + let manager = app.state::().manager.clone(); + std::thread::spawn(move || { + if let Err(err) = manager.start(app_handle.clone(), dev_mode) { + let _ = app_handle.emit("cli:error", json!({"message": err.to_string()})); + } + }); + Ok(()) + }) + .invoke_handler(tauri::generate_handler![cli_get_status, cli_restart]) + .on_menu_event(|app_handle, event| { + match event.id().0.as_str() { + // File menu + "new_instance" => { + if let Some(window) = app_handle.get_webview_window("main") { + let _ = window.emit("menu:newInstance", ()); + } + } + "close" => { + if let Some(window) = app_handle.get_webview_window("main") { + let _ = window.close(); + } + } + "quit" => { + app_handle.exit(0); + } + + // View menu + "reload" => { + if let Some(window) = app_handle.get_webview_window("main") { + let _ = window.eval("window.location.reload()"); + } + } + "force_reload" => { + if let Some(window) = app_handle.get_webview_window("main") { + let _ = window.eval("window.location.reload(true)"); + } + } + "toggle_devtools" => { + if let Some(window) = app_handle.get_webview_window("main") { + window.open_devtools(); + } + } + + "toggle_fullscreen" => { + if let Some(window) = app_handle.get_webview_window("main") { + let _ = window.set_fullscreen(!window.is_fullscreen().unwrap_or(false)); + } + } + + // Window menu + "minimize" => { + if let Some(window) = app_handle.get_webview_window("main") { + let _ = window.minimize(); + } + } + "zoom" => { + if let Some(window) = app_handle.get_webview_window("main") { + let _ = window.maximize(); + } + } + + // App menu (macOS) + "about" => { + // TODO: Implement about dialog + println!("About menu item clicked"); + } + "hide" => { + if let Some(window) = app_handle.get_webview_window("main") { + let _ = window.hide(); + } + } + "hide_others" => { + // TODO: Hide other app windows + println!("Hide Others menu item clicked"); + } + "show_all" => { + // TODO: Show all app windows + println!("Show All menu item clicked"); + } + + _ => { + println!("Unhandled menu event: {}", event.id().0); + } + } + }) + .build(tauri::generate_context!()) + .expect("error while building tauri application") + .run(|app_handle, event| match event { + tauri::RunEvent::ExitRequested { .. } => { + let app = app_handle.clone(); + std::thread::spawn(move || { + if let Some(state) = app.try_state::() { + let _ = state.manager.stop(); + } + app.exit(0); + }); + } + tauri::RunEvent::WindowEvent { + event: tauri::WindowEvent::Destroyed, + .. + } => { + if app_handle.webview_windows().len() <= 1 { + let app = app_handle.clone(); + std::thread::spawn(move || { + if let Some(state) = app.try_state::() { + let _ = state.manager.stop(); + } + app.exit(0); + }); + } + } + _ => {} + }); +} + +fn build_menu(app: &AppHandle) -> tauri::Result<()> { + let is_mac = cfg!(target_os = "macos"); + + // Create submenus + let mut submenus = Vec::new(); + + // App menu (macOS only) + if is_mac { + let app_menu = SubmenuBuilder::new(app, "CodeNomad") + .text("about", "About CodeNomad") + .separator() + .text("hide", "Hide CodeNomad") + .text("hide_others", "Hide Others") + .text("show_all", "Show All") + .separator() + .text("quit", "Quit CodeNomad") + .build()?; + submenus.push(app_menu); + } + + // File menu - create New Instance with accelerator + let new_instance_item = MenuItem::with_id( + app, + "new_instance", + "New Instance", + true, + Some("CmdOrCtrl+N") + )?; + + let file_menu = SubmenuBuilder::new(app, "File") + .item(&new_instance_item) + .separator() + .text(if is_mac { "close" } else { "quit" }, if is_mac { "Close" } else { "Quit" }) + .build()?; + submenus.push(file_menu); + + // Edit menu with predefined items for standard functionality + let edit_menu = SubmenuBuilder::new(app, "Edit") + .undo() + .redo() + .separator() + .cut() + .copy() + .paste() + .separator() + .select_all() + .build()?; + submenus.push(edit_menu); + + // View menu + let view_menu = SubmenuBuilder::new(app, "View") + .text("reload", "Reload") + .text("force_reload", "Force Reload") + .text("toggle_devtools", "Toggle Developer Tools") + .separator() + + .separator() + .text("toggle_fullscreen", "Toggle Full Screen") + .build()?; + submenus.push(view_menu); + + // Window menu + let window_menu = SubmenuBuilder::new(app, "Window") + .text("minimize", "Minimize") + .text("zoom", "Zoom") + .build()?; + submenus.push(window_menu); + + // Build the main menu with all submenus + let submenu_refs: Vec<&dyn tauri::menu::IsMenuItem<_>> = submenus.iter().map(|s| s as &dyn tauri::menu::IsMenuItem<_>).collect(); + let menu = MenuBuilder::new(app).items(&submenu_refs).build()?; + + app.set_menu(menu)?; + Ok(()) +} diff --git a/packages/tauri-app/src-tauri/tauri.conf.json b/packages/tauri-app/src-tauri/tauri.conf.json new file mode 100644 index 0000000..e847379 --- /dev/null +++ b/packages/tauri-app/src-tauri/tauri.conf.json @@ -0,0 +1,50 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "CodeNomad", + "version": "0.1.0", + "identifier": "ai.opencode.client", + "build": { + "beforeDevCommand": "npm run dev:bootstrap", + "beforeBuildCommand": "npm run bundle:server", + "frontendDist": "resources/ui-loading" + }, + + + + "app": { + "withGlobalTauri": true, + "windows": [ + { + "label": "main", + "title": "CodeNomad", + "url": "loading.html", + "width": 1400, + "height": 900, + "minWidth": 800, + "minHeight": 600, + "center": true, + "resizable": true, + "fullscreen": false, + "decorations": true, + "theme": "Dark", + "backgroundColor": "#1a1a1a", + "zoomHotkeysEnabled": true + } + ], + "security": { + "assetProtocol": { + "scope": ["**"] + }, + "capabilities": ["main-window-native-dialogs"] + } + }, + "bundle": { + "active": true, + "resources": [ + "resources/server", + "resources/ui-loading" + ], + "icon": ["icon.icns", "icon.ico", "icon.png"], + "targets": ["app", "appimage", "deb", "rpm", "nsis"] + } +} diff --git a/packages/ui/.gitignore b/packages/ui/.gitignore new file mode 100644 index 0000000..3ff38cc --- /dev/null +++ b/packages/ui/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +.vite/ diff --git a/packages/ui/README.md b/packages/ui/README.md new file mode 100644 index 0000000..d59a42d --- /dev/null +++ b/packages/ui/README.md @@ -0,0 +1,54 @@ +# CodeNomad UI + +This package contains the frontend user interface for CodeNomad, built with [SolidJS](https://www.solidjs.com/) and [Tailwind CSS](https://tailwindcss.com/). + +## Overview + +The UI is designed to be a high-performance, low-latency cockpit for managing OpenCode sessions. It connects to the CodeNomad server (either running locally via CLI or embedded in the Electron app). + +## Features + +- **SolidJS**: Fine-grained reactivity for high performance. +- **Tailwind CSS**: Utility-first styling for rapid development. +- **Vite**: Fast build tool and dev server. + +## Development + +To run the UI in standalone mode (connected to a running server): + +```bash +npm run dev +``` + +This starts the Vite dev server at `http://localhost:3000`. + +## Building + +To build the production assets: + +``` +npm run build +``` + +The output will be generated in the `dist` directory, which is then consumed by the Server or Electron app. + +## Debug Logging + +The UI now routes all logging through a lightweight wrapper around [`debug`](https://github.com/debug-js/debug). The logger exposes four namespaces that can be toggled at runtime: + +- `sse` – Server-sent event transport and handlers +- `api` – HTTP/API calls and workspace lifecycle +- `session` – Session/model state, prompt handling, tool calls +- `actions` – User-driven interactions in UI components + +You can enable or disable namespaces from DevTools (in dev or production builds) via the global `window.codenomadLogger` helpers: + +```js +window.codenomadLogger?.listLoggerNamespaces() // => [{ name: "sse", enabled: false }, ...] +window.codenomadLogger?.enableLogger("sse") // turn on SSE logs +window.codenomadLogger?.disableLogger("sse") // turn them off again +window.codenomadLogger?.enableAllLoggers() // optional helper +``` + +Enabled namespaces are persisted in `localStorage` under `opencode:logger:namespaces`, so your preference survives reloads. + diff --git a/packages/ui/package.json b/packages/ui/package.json new file mode 100644 index 0000000..098a9c2 --- /dev/null +++ b/packages/ui/package.json @@ -0,0 +1,37 @@ +{ + "name": "@codenomad/ui", + "version": "0.4.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "typecheck": "tsc --noEmit -p tsconfig.json" + }, + "dependencies": { + "@git-diff-view/solid": "^0.0.8", + "@kobalte/core": "0.13.11", + "@opencode-ai/sdk": "^1.0.138", + "@solidjs/router": "^0.13.0", + "@suid/icons-material": "^0.9.0", + "@suid/material": "^0.19.0", + "@suid/system": "^0.14.0", + "debug": "^4.4.3", + "github-markdown-css": "^5.8.1", + "lucide-solid": "^0.300.0", + "marked": "^12.0.0", + "qrcode": "^1.5.3", + "shiki": "^3.13.0", + "solid-js": "^1.8.0", + "solid-toast": "^0.5.0" + }, + "devDependencies": { + "autoprefixer": "10.4.21", + "postcss": "8.5.6", + "tailwindcss": "3", + "typescript": "^5.3.0", + "vite": "^5.0.0", + "vite-plugin-solid": "^2.10.0" + } +} diff --git a/packages/ui/postcss.config.js b/packages/ui/postcss.config.js new file mode 100644 index 0000000..d90d3d0 --- /dev/null +++ b/packages/ui/postcss.config.js @@ -0,0 +1,11 @@ +import { fileURLToPath } from "url" +import { dirname, resolve } from "path" + +const __dirname = dirname(fileURLToPath(import.meta.url)) + +export default { + plugins: { + tailwindcss: { config: resolve(__dirname, "tailwind.config.js") }, + autoprefixer: {}, + }, +} diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx new file mode 100644 index 0000000..78488de --- /dev/null +++ b/packages/ui/src/App.tsx @@ -0,0 +1,466 @@ +import { Component, For, Show, createMemo, createEffect, createSignal, onMount, onCleanup } from "solid-js" +import { Dialog } from "@kobalte/core/dialog" +import { Toaster } from "solid-toast" +import AlertDialog from "./components/alert-dialog" +import FolderSelectionView from "./components/folder-selection-view" +import { showConfirmDialog } from "./stores/alerts" +import InstanceTabs from "./components/instance-tabs" +import InstanceDisconnectedModal from "./components/instance-disconnected-modal" +import InstanceShell from "./components/instance/instance-shell2" +import { RemoteAccessOverlay } from "./components/remote-access-overlay" +import { InstanceMetadataProvider } from "./lib/contexts/instance-metadata-context" +import { initMarkdown } from "./lib/markdown" + +import { useTheme } from "./lib/theme" +import { useCommands } from "./lib/hooks/use-commands" +import { useAppLifecycle } from "./lib/hooks/use-app-lifecycle" +import { getLogger } from "./lib/logger" +import { initReleaseNotifications } from "./stores/releases" +import { runtimeEnv } from "./lib/runtime-env" +import { + hasInstances, + isSelectingFolder, + setIsSelectingFolder, + showFolderSelection, + setShowFolderSelection, +} from "./stores/ui" +import { useConfig } from "./stores/preferences" +import { + createInstance, + instances, + activeInstanceId, + setActiveInstanceId, + stopInstance, + getActiveInstance, + disconnectedInstance, + acknowledgeDisconnectedInstance, +} from "./stores/instances" +import { + getSessions, + activeSessionId, + setActiveParentSession, + clearActiveParentSession, + createSession, + fetchSessions, + updateSessionAgent, + updateSessionModel, +} from "./stores/sessions" + +const log = getLogger("actions") + +const App: Component = () => { + const { isDark } = useTheme() + const { + preferences, + recordWorkspaceLaunch, + toggleShowThinkingBlocks, + toggleShowTimelineTools, + toggleAutoCleanupBlankSessions, + toggleUsageMetrics, + setDiffViewMode, + setToolOutputExpansion, + setDiagnosticsExpansion, + setThinkingBlocksExpansion, + } = useConfig() + const [escapeInDebounce, setEscapeInDebounce] = createSignal(false) + interface LaunchErrorState { + message: string + binaryPath: string + missingBinary: boolean + } + const [launchError, setLaunchError] = createSignal(null) + const [isAdvancedSettingsOpen, setIsAdvancedSettingsOpen] = createSignal(false) + const [remoteAccessOpen, setRemoteAccessOpen] = createSignal(false) + const [instanceTabBarHeight, setInstanceTabBarHeight] = createSignal(0) + + const updateInstanceTabBarHeight = () => { + if (typeof document === "undefined") return + const element = document.querySelector(".tab-bar-instance") + setInstanceTabBarHeight(element?.offsetHeight ?? 0) + } + + createEffect(() => { + void initMarkdown(isDark()).catch((error) => log.error("Failed to initialize markdown", error)) + }) + + createEffect(() => { + initReleaseNotifications() + }) + + createEffect(() => { + instances() + hasInstances() + requestAnimationFrame(() => updateInstanceTabBarHeight()) + }) + + onMount(() => { + updateInstanceTabBarHeight() + const handleResize = () => updateInstanceTabBarHeight() + window.addEventListener("resize", handleResize) + onCleanup(() => window.removeEventListener("resize", handleResize)) + }) + + const activeInstance = createMemo(() => getActiveInstance()) + const activeSessionIdForInstance = createMemo(() => { + const instance = activeInstance() + if (!instance) return null + return activeSessionId().get(instance.id) || null + }) + + const launchErrorPath = () => { + const value = launchError()?.binaryPath + if (!value) return "opencode" + return value.trim() || "opencode" + } + + const launchErrorMessage = () => launchError()?.message ?? "" + + const formatLaunchErrorMessage = (error: unknown): string => { + if (!error) { + return "Failed to launch workspace" + } + const raw = typeof error === "string" ? error : error instanceof Error ? error.message : String(error) + try { + const parsed = JSON.parse(raw) + if (parsed && typeof parsed.error === "string") { + return parsed.error + } + } catch { + // ignore JSON parse errors + } + return raw + } + + const isMissingBinaryMessage = (message: string): boolean => { + const normalized = message.toLowerCase() + return ( + normalized.includes("opencode binary not found") || + normalized.includes("binary not found") || + normalized.includes("no such file or directory") || + normalized.includes("binary is not executable") || + normalized.includes("enoent") + ) + } + + const clearLaunchError = () => setLaunchError(null) + + async function handleSelectFolder(folderPath: string, binaryPath?: string) { + if (!folderPath) { + return + } + setIsSelectingFolder(true) + const selectedBinary = binaryPath || preferences().lastUsedBinary || "opencode" + try { + recordWorkspaceLaunch(folderPath, selectedBinary) + clearLaunchError() + const instanceId = await createInstance(folderPath, selectedBinary) + setShowFolderSelection(false) + setIsAdvancedSettingsOpen(false) + + log.info("Created instance", { + instanceId, + port: instances().get(instanceId)?.port, + }) + } catch (error) { + const message = formatLaunchErrorMessage(error) + const missingBinary = isMissingBinaryMessage(message) + setLaunchError({ + message, + binaryPath: selectedBinary, + missingBinary, + }) + log.error("Failed to create instance", error) + } finally { + setIsSelectingFolder(false) + } + } + + function handleLaunchErrorClose() { + clearLaunchError() + } + + function handleLaunchErrorAdvanced() { + clearLaunchError() + setIsAdvancedSettingsOpen(true) + } + + function handleNewInstanceRequest() { + if (hasInstances()) { + setShowFolderSelection(true) + } + } + + async function handleDisconnectedInstanceClose() { + try { + await acknowledgeDisconnectedInstance() + } catch (error) { + log.error("Failed to finalize disconnected instance", error) + } + } + + async function handleCloseInstance(instanceId: string) { + const confirmed = await showConfirmDialog( + "Stop OpenCode instance? This will stop the server.", + { + title: "Stop instance", + variant: "warning", + confirmLabel: "Stop", + cancelLabel: "Keep running", + }, + ) + + if (!confirmed) return + + await stopInstance(instanceId) + } + + async function handleNewSession(instanceId: string) { + try { + const session = await createSession(instanceId) + setActiveParentSession(instanceId, session.id) + } catch (error) { + log.error("Failed to create session", error) + } + } + + async function handleCloseSession(instanceId: string, sessionId: string) { + const sessions = getSessions(instanceId) + const session = sessions.find((s) => s.id === sessionId) + + if (!session) { + return + } + + const parentSessionId = session.parentId ?? session.id + const parentSession = sessions.find((s) => s.id === parentSessionId) + + if (!parentSession || parentSession.parentId !== null) { + return + } + + clearActiveParentSession(instanceId) + + try { + await fetchSessions(instanceId) + } catch (error) { + log.error("Failed to refresh sessions after closing", error) + } + } + + const handleSidebarAgentChange = async (instanceId: string, sessionId: string, agent: string) => { + if (!instanceId || !sessionId || sessionId === "info") return + await updateSessionAgent(instanceId, sessionId, agent) + } + + const handleSidebarModelChange = async ( + instanceId: string, + sessionId: string, + model: { providerId: string; modelId: string }, + ) => { + if (!instanceId || !sessionId || sessionId === "info") return + await updateSessionModel(instanceId, sessionId, model) + } + + const { commands: paletteCommands, executeCommand } = useCommands({ + preferences, + toggleAutoCleanupBlankSessions, + toggleShowThinkingBlocks, + toggleShowTimelineTools, + toggleUsageMetrics, + setDiffViewMode, + setToolOutputExpansion, + setDiagnosticsExpansion, + setThinkingBlocksExpansion, + handleNewInstanceRequest, + handleCloseInstance, + handleNewSession, + handleCloseSession, + getActiveInstance: activeInstance, + getActiveSessionIdForInstance: activeSessionIdForInstance, + }) + + useAppLifecycle({ + setEscapeInDebounce, + handleNewInstanceRequest, + handleCloseInstance, + handleNewSession, + handleCloseSession, + showFolderSelection, + setShowFolderSelection, + getActiveInstance: activeInstance, + getActiveSessionIdForInstance: activeSessionIdForInstance, + }) + + // Listen for Tauri menu events + onMount(() => { + if (runtimeEnv.host === "tauri") { + const tauriBridge = (window as { __TAURI__?: { event?: { listen: (event: string, handler: (event: { payload: unknown }) => void) => Promise<() => void> } } }).__TAURI__ + if (tauriBridge?.event) { + let unlistenMenu: (() => void) | null = null + + tauriBridge.event.listen("menu:newInstance", () => { + handleNewInstanceRequest() + }).then((unlisten) => { + unlistenMenu = unlisten + }).catch((error) => { + log.error("Failed to listen for menu:newInstance event", error) + }) + + onCleanup(() => { + unlistenMenu?.() + }) + } + } + }) + + return ( + <> + + + + + +
+ +
+ Unable to launch OpenCode + + We couldn't start the selected OpenCode binary. Review the error output below or choose a different + binary from Advanced Settings. + +
+ +
+

Binary path

+

{launchErrorPath()}

+
+ + +
+

Error output

+
{launchErrorMessage()}
+
+
+ +
+ + + + +
+
+
+
+
+
+ + setRemoteAccessOpen(true)} + /> + + + {(instance) => { + const isActiveInstance = () => activeInstanceId() === instance.id + const isVisible = () => isActiveInstance() && !showFolderSelection() + return ( +
+ + handleCloseSession(instance.id, sessionId)} + onNewSession={() => handleNewSession(instance.id)} + handleSidebarAgentChange={(sessionId, agent) => handleSidebarAgentChange(instance.id, sessionId, agent)} + handleSidebarModelChange={(sessionId, model) => handleSidebarModelChange(instance.id, sessionId, model)} + onExecuteCommand={executeCommand} + tabBarOffset={instanceTabBarHeight()} + /> + + +
+ ) + + }} +
+ + + } + > + setIsAdvancedSettingsOpen(true)} + onAdvancedSettingsClose={() => setIsAdvancedSettingsOpen(false)} + onOpenRemoteAccess={() => setRemoteAccessOpen(true)} + /> +
+ + +
+
+ + setIsAdvancedSettingsOpen(true)} + onAdvancedSettingsClose={() => setIsAdvancedSettingsOpen(false)} + /> +
+
+
+ + setRemoteAccessOpen(false)} /> + + + + +
+ + ) +} + + +export default App diff --git a/packages/ui/src/components/advanced-settings-modal.tsx b/packages/ui/src/components/advanced-settings-modal.tsx new file mode 100644 index 0000000..06a60bb --- /dev/null +++ b/packages/ui/src/components/advanced-settings-modal.tsx @@ -0,0 +1,60 @@ +import { Component } from "solid-js" +import { Dialog } from "@kobalte/core/dialog" +import OpenCodeBinarySelector from "./opencode-binary-selector" +import EnvironmentVariablesEditor from "./environment-variables-editor" + +interface AdvancedSettingsModalProps { + open: boolean + onClose: () => void + selectedBinary: string + onBinaryChange: (binary: string) => void + isLoading?: boolean +} + +const AdvancedSettingsModal: Component = (props) => { + return ( + !open && props.onClose()}> + + +
+ +
+ Advanced Settings +
+ +
+ + +
+
+

Environment Variables

+

Applied whenever a new OpenCode instance starts

+
+
+ +
+
+
+ +
+ +
+
+
+
+
+ ) +} + +export default AdvancedSettingsModal diff --git a/packages/ui/src/components/agent-selector.tsx b/packages/ui/src/components/agent-selector.tsx new file mode 100644 index 0000000..2511266 --- /dev/null +++ b/packages/ui/src/components/agent-selector.tsx @@ -0,0 +1,124 @@ +import { Select } from "@kobalte/core/select" +import { For, Show, createEffect, createMemo } from "solid-js" +import { agents, fetchAgents, sessions } from "../stores/sessions" +import { ChevronDown } from "lucide-solid" +import type { Agent } from "../types/session" +import { getLogger } from "../lib/logger" +const log = getLogger("session") + + +interface AgentSelectorProps { + instanceId: string + sessionId: string + currentAgent: string + onAgentChange: (agent: string) => Promise +} + +export default function AgentSelector(props: AgentSelectorProps) { + const instanceAgents = () => agents().get(props.instanceId) || [] + + const session = createMemo(() => { + const instanceSessions = sessions().get(props.instanceId) + return instanceSessions?.get(props.sessionId) + }) + + const isChildSession = createMemo(() => { + return session()?.parentId !== null && session()?.parentId !== undefined + }) + + const availableAgents = createMemo(() => { + const allAgents = instanceAgents() + if (isChildSession()) { + return allAgents + } + + const filtered = allAgents.filter((agent) => agent.mode !== "subagent") + + const currentAgent = allAgents.find((a) => a.name === props.currentAgent) + if (currentAgent && !filtered.find((a) => a.name === props.currentAgent)) { + return [currentAgent, ...filtered] + } + + return filtered + }) + + createEffect(() => { + const list = availableAgents() + if (list.length === 0) return + if (!list.some((agent) => agent.name === props.currentAgent)) { + void props.onAgentChange(list[0].name) + } + }) + + createEffect(() => { + if (instanceAgents().length === 0) { + fetchAgents(props.instanceId).catch((error) => log.error("Failed to fetch agents", error)) + } + }) + + + const handleChange = async (value: Agent | null) => { + if (value && value.name !== props.currentAgent) { + await props.onAgentChange(value.name) + } + } + + return ( + + ) +} diff --git a/packages/ui/src/components/alert-dialog.tsx b/packages/ui/src/components/alert-dialog.tsx new file mode 100644 index 0000000..7dc7b73 --- /dev/null +++ b/packages/ui/src/components/alert-dialog.tsx @@ -0,0 +1,132 @@ +import { Dialog } from "@kobalte/core/dialog" +import { Component, Show, createEffect } from "solid-js" +import { alertDialogState, dismissAlertDialog } from "../stores/alerts" +import type { AlertVariant, AlertDialogState } from "../stores/alerts" + +const variantAccent: Record = { + info: { + badgeBg: "var(--badge-neutral-bg)", + badgeBorder: "var(--border-base)", + badgeText: "var(--accent-primary)", + symbol: "i", + fallbackTitle: "Heads up", + }, + warning: { + badgeBg: "rgba(255, 152, 0, 0.14)", + badgeBorder: "var(--status-warning)", + badgeText: "var(--status-warning)", + symbol: "!", + fallbackTitle: "Please review", + }, + error: { + badgeBg: "var(--danger-soft-bg)", + badgeBorder: "var(--status-error)", + badgeText: "var(--status-error)", + symbol: "!", + fallbackTitle: "Something went wrong", + }, +} + +function dismiss(confirmed: boolean, payload?: AlertDialogState | null) { + const current = payload ?? alertDialogState() + if (current?.type === "confirm") { + if (confirmed) { + current.onConfirm?.() + } else { + current.onCancel?.() + } + current.resolve?.(confirmed) + } else if (confirmed) { + current?.onConfirm?.() + } + dismissAlertDialog() +} + +const AlertDialog: Component = () => { + let primaryButtonRef: HTMLButtonElement | undefined + + createEffect(() => { + if (alertDialogState()) { + queueMicrotask(() => { + primaryButtonRef?.focus() + }) + } + }) + + return ( + + {(payload) => { + const variant = payload.variant ?? "info" + const accent = variantAccent[variant] + const title = payload.title || accent.fallbackTitle + const isConfirm = payload.type === "confirm" + const confirmLabel = payload.confirmLabel || (isConfirm ? "Confirm" : "OK") + const cancelLabel = payload.cancelLabel || "Cancel" + + return ( + { + if (!open) { + dismiss(false, payload) + } + }} + > + + +
+ +
+
+ {accent.symbol} +
+
+ {title} + + {payload.message} + {payload.detail &&

{payload.detail}

} +
+
+
+ +
+ {isConfirm && ( + + )} + +
+
+
+
+
+ ) + }} +
+ ) +} + +export default AlertDialog diff --git a/packages/ui/src/components/attachment-chip.tsx b/packages/ui/src/components/attachment-chip.tsx new file mode 100644 index 0000000..49d7581 --- /dev/null +++ b/packages/ui/src/components/attachment-chip.tsx @@ -0,0 +1,27 @@ +import { Component } from "solid-js" +import type { Attachment } from "../types/attachment" + +interface AttachmentChipProps { + attachment: Attachment + onRemove: () => void +} + +const AttachmentChip: Component = (props) => { + return ( +
+ {props.attachment.display} + +
+ ) +} + +export default AttachmentChip diff --git a/packages/ui/src/components/code-block-inline.tsx b/packages/ui/src/components/code-block-inline.tsx new file mode 100644 index 0000000..150a9b1 --- /dev/null +++ b/packages/ui/src/components/code-block-inline.tsx @@ -0,0 +1,107 @@ +import { createSignal, onMount, Show, createEffect } from "solid-js" +import type { Highlighter } from "shiki/bundle/full" +import { useTheme } from "../lib/theme" +import { getSharedHighlighter, escapeHtml } from "../lib/markdown" + +const inlineLoadedLanguages = new Set() + +type LoadLanguageArg = Parameters[0] +type CodeToHtmlOptions = Parameters[1] + +interface CodeBlockInlineProps { + code: string + language?: string +} + +export function CodeBlockInline(props: CodeBlockInlineProps) { + const { isDark } = useTheme() + const [html, setHtml] = createSignal("") + const [copied, setCopied] = createSignal(false) + const [ready, setReady] = createSignal(false) + let highlighter: Highlighter | null = null + + onMount(async () => { + highlighter = await getSharedHighlighter() + setReady(true) + await updateHighlight() + }) + + createEffect(() => { + if (ready()) { + isDark() + props.code + props.language + void updateHighlight() + } + }) + + const updateHighlight = async () => { + if (!highlighter) return + + if (!props.language) { + setHtml(`
${escapeHtml(props.code)}
`) + return + } + + try { + const language = props.language as LoadLanguageArg + if (!inlineLoadedLanguages.has(props.language)) { + await highlighter.loadLanguage(language) + inlineLoadedLanguages.add(props.language) + } + + const highlighted = highlighter.codeToHtml(props.code, { + lang: props.language as CodeToHtmlOptions["lang"], + theme: isDark() ? "github-dark" : "github-light", + }) + setHtml(highlighted) + } catch { + setHtml(`
${escapeHtml(props.code)}
`) + } + } + + const copyCode = async () => { + await navigator.clipboard.writeText(props.code) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } + + return ( + + {props.code} + + } + > +
+
+ + {props.language} + + +
+
+
+ + ) +} diff --git a/packages/ui/src/components/command-palette.tsx b/packages/ui/src/components/command-palette.tsx new file mode 100644 index 0000000..3d394f9 --- /dev/null +++ b/packages/ui/src/components/command-palette.tsx @@ -0,0 +1,287 @@ +import { Component, createSignal, For, Show, createEffect, createMemo } from "solid-js" +import { Dialog } from "@kobalte/core/dialog" +import type { Command } from "../lib/commands" +import Kbd from "./kbd" + +interface CommandPaletteProps { + open: boolean + onClose: () => void + commands: Command[] + onExecute: (command: Command) => void +} + +function buildShortcutString(shortcut: Command["shortcut"]): string { + if (!shortcut) return "" + + const parts: string[] = [] + + if (shortcut.meta || shortcut.ctrl) parts.push("cmd") + if (shortcut.shift) parts.push("shift") + if (shortcut.alt) parts.push("alt") + parts.push(shortcut.key) + + return parts.join("+") +} + +const CommandPalette: Component = (props) => { + const [query, setQuery] = createSignal("") + const [selectedCommandId, setSelectedCommandId] = createSignal(null) + const [isPointerSelecting, setIsPointerSelecting] = createSignal(false) + let inputRef: HTMLInputElement | undefined + let listRef: HTMLDivElement | undefined + + const categoryOrder = ["Custom Commands", "Instance", "Session", "Agent & Model", "Input & Focus", "System", "Other"] as const + + type CommandGroup = { category: string; commands: Command[]; startIndex: number } + type ProcessedCommands = { groups: CommandGroup[]; ordered: Command[] } + + const processedCommands = createMemo(() => { + const source = props.commands ?? [] + const q = query().trim().toLowerCase() + + const filtered = q + ? source.filter((cmd) => { + const label = typeof cmd.label === "function" ? cmd.label() : cmd.label + const labelMatch = label.toLowerCase().includes(q) + const descMatch = cmd.description.toLowerCase().includes(q) + const keywordMatch = cmd.keywords?.some((k) => k.toLowerCase().includes(q)) + const categoryMatch = cmd.category?.toLowerCase().includes(q) + return labelMatch || descMatch || keywordMatch || categoryMatch + }) + : source + + const groupsMap = new Map() + for (const cmd of filtered) { + const category = cmd.category || "Other" + const list = groupsMap.get(category) + if (list) { + list.push(cmd) + } else { + groupsMap.set(category, [cmd]) + } + } + + const groups: CommandGroup[] = [] + const ordered: Command[] = [] + const processedCategories = new Set() + + const addGroup = (category: string) => { + const cmds = groupsMap.get(category) + if (!cmds || cmds.length === 0 || processedCategories.has(category)) return + groups.push({ category, commands: cmds, startIndex: ordered.length }) + ordered.push(...cmds) + processedCategories.add(category) + } + + for (const category of categoryOrder) { + addGroup(category) + } + + for (const [category] of groupsMap) { + addGroup(category) + } + + return { groups, ordered } + }) + + const groupedCommandList = () => processedCommands().groups + const orderedCommands = () => processedCommands().ordered + const selectedIndex = createMemo(() => { + const ordered = orderedCommands() + if (ordered.length === 0) return -1 + const id = selectedCommandId() + if (!id) return 0 + const index = ordered.findIndex((cmd) => cmd.id === id) + return index >= 0 ? index : 0 + }) + + createEffect(() => { + if (props.open) { + setQuery("") + setSelectedCommandId(null) + setIsPointerSelecting(false) + setTimeout(() => inputRef?.focus(), 100) + } + }) + + createEffect(() => { + const ordered = orderedCommands() + if (ordered.length === 0) { + if (selectedCommandId() !== null) { + setSelectedCommandId(null) + } + return + } + + const currentId = selectedCommandId() + if (!currentId || !ordered.some((cmd) => cmd.id === currentId)) { + setSelectedCommandId(ordered[0].id) + } + }) + + + createEffect(() => { + const index = selectedIndex() + if (!listRef || index < 0) return + + const selectedButton = listRef.querySelector(`[data-command-index="${index}"]`) as HTMLElement + if (selectedButton) { + selectedButton.scrollIntoView({ block: "nearest", behavior: "smooth" }) + } + }) + + function handleKeyDown(e: KeyboardEvent) { + const ordered = orderedCommands() + + if (e.key === "Escape") { + e.preventDefault() + e.stopPropagation() + props.onClose() + return + } + + if (ordered.length === 0) { + if (e.key === "ArrowDown" || e.key === "ArrowUp" || e.key === "Enter") { + e.preventDefault() + e.stopPropagation() + } + return + } + + if (e.key === "ArrowDown") { + e.preventDefault() + e.stopPropagation() + setIsPointerSelecting(false) + const current = selectedIndex() + const nextIndex = Math.min((current < 0 ? 0 : current) + 1, ordered.length - 1) + setSelectedCommandId(ordered[nextIndex]?.id ?? null) + } else if (e.key === "ArrowUp") { + e.preventDefault() + e.stopPropagation() + setIsPointerSelecting(false) + const current = selectedIndex() + const nextIndex = current <= 0 ? ordered.length - 1 : current - 1 + setSelectedCommandId(ordered[nextIndex]?.id ?? null) + } else if (e.key === "Enter") { + e.preventDefault() + e.stopPropagation() + const index = selectedIndex() + if (index < 0 || index >= ordered.length) return + const command = ordered[index] + if (!command) return + props.onExecute(command) + props.onClose() + } + } + + function handleCommandClick(command: Command) { + props.onExecute(command) + props.onClose() + } + + function handlePointerLeave() { + setIsPointerSelecting(false) + } + + return ( + + !open && props.onClose()}> + + +
+ + Command Palette + Search and execute commands + + + + + +
+
+
+ ) +} + +export default CommandPalette diff --git a/packages/ui/src/components/diff-viewer.tsx b/packages/ui/src/components/diff-viewer.tsx new file mode 100644 index 0000000..b2fef6a --- /dev/null +++ b/packages/ui/src/components/diff-viewer.tsx @@ -0,0 +1,137 @@ +import { createMemo, Show, createEffect, onCleanup } from "solid-js" +import { DiffView, DiffModeEnum } from "@git-diff-view/solid" +import { disableCache } from "@git-diff-view/core" +import type { DiffHighlighterLang } from "@git-diff-view/core" +import { ErrorBoundary } from "solid-js" +import { getLanguageFromPath } from "../lib/markdown" +import { normalizeDiffText } from "../lib/diff-utils" +import { setCacheEntry } from "../lib/global-cache" +import type { CacheEntryParams } from "../lib/global-cache" +import type { DiffViewMode } from "../stores/preferences" +import { getLogger } from "../lib/logger" +const log = getLogger("session") + + +disableCache() + +interface ToolCallDiffViewerProps { + diffText: string + filePath?: string + theme: "light" | "dark" + mode: DiffViewMode + onRendered?: () => void + cachedHtml?: string + cacheEntryParams?: CacheEntryParams +} + +type DiffData = { + oldFile?: { fileName?: string | null; fileLang?: string | null; content?: string | null } + newFile?: { fileName?: string | null; fileLang?: string | null; content?: string | null } + hunks: string[] +} + +type CaptureContext = { + theme: ToolCallDiffViewerProps["theme"] + mode: DiffViewMode + diffText: string + cacheEntryParams?: CacheEntryParams +} + +export function ToolCallDiffViewer(props: ToolCallDiffViewerProps) { + const diffData = createMemo(() => { + const normalized = normalizeDiffText(props.diffText) + if (!normalized) { + return null + } + + const language = getLanguageFromPath(props.filePath) || "text" + const fileName = props.filePath || "diff" + + return { + oldFile: { + fileName, + fileLang: (language || "text") as DiffHighlighterLang | null, + }, + newFile: { + fileName, + fileLang: (language || "text") as DiffHighlighterLang | null, + }, + hunks: [normalized], + } + }) + + let diffContainerRef: HTMLDivElement | undefined + let lastCapturedKey: string | undefined + + const contextKey = createMemo(() => { + const data = diffData() + if (!data) return "" + return `${props.theme}|${props.mode}|${props.diffText}` + }) + + createEffect(() => { + const cachedHtml = props.cachedHtml + if (cachedHtml) { + // When we are given cached HTML, we rely on the caller's cache + // and simply notify once rendered. + props.onRendered?.() + return + } + + const key = contextKey() + if (!key) return + if (!diffContainerRef) return + if (lastCapturedKey === key) return + + requestAnimationFrame(() => { + if (!diffContainerRef) return + const markup = diffContainerRef.innerHTML + if (!markup) return + lastCapturedKey = key + if (props.cacheEntryParams) { + setCacheEntry(props.cacheEntryParams, { + text: props.diffText, + html: markup, + theme: props.theme, + mode: props.mode, + }) + } + props.onRendered?.() + }) + }) + + + return ( +
+ + {props.diffText}} + > + {(data) => ( + { + log.warn("Failed to render diff view", error) + return
{props.diffText}
+ }}> + +
+ )} +
+
+ } + > +
+ +
+ ) +} \ No newline at end of file diff --git a/packages/ui/src/components/directory-browser-dialog.tsx b/packages/ui/src/components/directory-browser-dialog.tsx new file mode 100644 index 0000000..c3622a6 --- /dev/null +++ b/packages/ui/src/components/directory-browser-dialog.tsx @@ -0,0 +1,375 @@ +import { Component, Show, For, createSignal, createMemo, createEffect, onCleanup } from "solid-js" +import { ArrowUpLeft, Folder as FolderIcon, Loader2, X } from "lucide-solid" +import type { FileSystemEntry, FileSystemListingMetadata } from "../../../server/src/api-types" +import { WINDOWS_DRIVES_ROOT } from "../../../server/src/api-types" +import { serverApi } from "../lib/api-client" + +function normalizePathKey(input?: string | null) { + if (!input || input === "." || input === "./") { + return "." + } + if (input === WINDOWS_DRIVES_ROOT) { + return WINDOWS_DRIVES_ROOT + } + let normalized = input.replace(/\\/g, "/") + if (/^[a-zA-Z]:/.test(normalized)) { + const [drive, rest = ""] = normalized.split(":") + const suffix = rest.startsWith("/") ? rest : rest ? `/${rest}` : "/" + return `${drive.toUpperCase()}:${suffix.replace(/\/+/g, "/")}` + } + if (normalized.startsWith("//")) { + return `//${normalized.slice(2).replace(/\/+/g, "/")}` + } + if (normalized.startsWith("/")) { + return `/${normalized.slice(1).replace(/\/+/g, "/")}` + } + normalized = normalized.replace(/^\.\/+/, "").replace(/\/+/g, "/") + return normalized === "" ? "." : normalized +} + + +function isAbsolutePathLike(input: string) { + return input.startsWith("/") || /^[a-zA-Z]:/.test(input) || input.startsWith("\\\\") +} + +interface DirectoryBrowserDialogProps { + open: boolean + title: string + description?: string + onSelect: (absolutePath: string) => void + onClose: () => void +} + +function resolveAbsolutePath(root: string, relativePath: string) { + if (!root) { + return relativePath + } + if (!relativePath || relativePath === "." || relativePath === "./") { + return root + } + if (isAbsolutePathLike(relativePath)) { + return relativePath + } + const separator = root.includes("\\") ? "\\" : "/" + const trimmedRoot = root.endsWith(separator) ? root : `${root}${separator}` + const normalized = relativePath.replace(/[\\/]+/g, separator).replace(/^[\\/]+/, "") + return `${trimmedRoot}${normalized}` +} + +type FolderRow = + | { type: "up"; path: string } + | { type: "folder"; entry: FileSystemEntry } + +const DirectoryBrowserDialog: Component = (props) => { + const [rootPath, setRootPath] = createSignal("") + const [loading, setLoading] = createSignal(false) + const [error, setError] = createSignal(null) + const [directoryChildren, setDirectoryChildren] = createSignal>(new Map()) + const [loadingPaths, setLoadingPaths] = createSignal>(new Set()) + const [currentPathKey, setCurrentPathKey] = createSignal(null) + const [currentMetadata, setCurrentMetadata] = createSignal(null) + + const metadataCache = new Map() + const inFlightRequests = new Map>() + + function resetState() { + setDirectoryChildren(new Map()) + setLoadingPaths(new Set()) + setCurrentPathKey(null) + setCurrentMetadata(null) + metadataCache.clear() + inFlightRequests.clear() + setError(null) + } + + createEffect(() => { + if (!props.open) { + return + } + resetState() + void initialize() + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") { + event.preventDefault() + props.onClose() + } + } + + window.addEventListener("keydown", handleKeyDown) + onCleanup(() => { + window.removeEventListener("keydown", handleKeyDown) + }) + }) + + async function initialize() { + setLoading(true) + try { + const metadata = await loadDirectory() + applyMetadata(metadata) + } catch (err) { + const message = err instanceof Error ? err.message : "Unable to load filesystem" + setError(message) + } finally { + setLoading(false) + } + } + + function applyMetadata(metadata: FileSystemListingMetadata) { + const key = normalizePathKey(metadata.currentPath) + setCurrentPathKey(key) + setCurrentMetadata(metadata) + setRootPath(metadata.rootPath) + } + + async function loadDirectory(targetPath?: string): Promise { + const key = targetPath ? normalizePathKey(targetPath) : undefined + if (key) { + const cached = metadataCache.get(key) + if (cached) { + return cached + } + const pending = inFlightRequests.get(key) + if (pending) { + return pending + } + } + + const request = (async () => { + if (key) { + setLoadingPaths((prev) => { + const next = new Set(prev) + next.add(key) + return next + }) + } + + const response = await serverApi.listFileSystem(targetPath, { includeFiles: false }) + const canonicalKey = normalizePathKey(response.metadata.currentPath) + const directories = response.entries + .filter((entry) => entry.type === "directory") + .sort((a, b) => a.name.localeCompare(b.name)) + + setDirectoryChildren((prev) => { + const next = new Map(prev) + next.set(canonicalKey, directories) + return next + }) + + metadataCache.set(canonicalKey, response.metadata) + + setLoadingPaths((prev) => { + const next = new Set(prev) + if (key) { + next.delete(key) + } + next.delete(canonicalKey) + return next + }) + + return response.metadata + })() + .catch((err) => { + if (key) { + setLoadingPaths((prev) => { + const next = new Set(prev) + next.delete(key) + return next + }) + } + throw err + }) + .finally(() => { + if (key) { + inFlightRequests.delete(key) + } + }) + + if (key) { + inFlightRequests.set(key, request) + } + + return request + } + + async function navigateTo(path?: string) { + setError(null) + try { + const metadata = await loadDirectory(path) + applyMetadata(metadata) + } catch (err) { + const message = err instanceof Error ? err.message : "Unable to load filesystem" + setError(message) + } + } + + const folderRows = createMemo(() => { + const rows: FolderRow[] = [] + const metadata = currentMetadata() + if (metadata?.parentPath) { + rows.push({ type: "up", path: metadata.parentPath }) + } + const key = currentPathKey() + if (!key) { + return rows + } + const children = directoryChildren().get(key) ?? [] + for (const entry of children) { + rows.push({ type: "folder", entry }) + } + return rows + }) + + function handleNavigateTo(path: string) { + void navigateTo(path) + } + + function handleNavigateUp() { + const parent = currentMetadata()?.parentPath + if (parent) { + void navigateTo(parent) + } + } + + const currentAbsolutePath = createMemo(() => { + const metadata = currentMetadata() + if (!metadata) { + return "" + } + if (metadata.pathKind === "drives") { + return "" + } + if (metadata.pathKind === "relative") { + return resolveAbsolutePath(metadata.rootPath, metadata.currentPath) + } + return metadata.displayPath + }) + + const canSelectCurrent = createMemo(() => Boolean(currentAbsolutePath())) + + function handleEntrySelect(entry: FileSystemEntry) { + const absolutePath = entry.absolutePath + ? entry.absolutePath + : isAbsolutePathLike(entry.path) + ? entry.path + : resolveAbsolutePath(rootPath(), entry.path) + props.onSelect(absolutePath) + } + + function isPathLoading(path: string) { + return loadingPaths().has(normalizePathKey(path)) + } + + function handleOverlayClick(event: MouseEvent) { + if (event.target === event.currentTarget) { + props.onClose() + } + } + + return ( + +
+ +
+
+
+ ) +} + +export default DirectoryBrowserDialog diff --git a/packages/ui/src/components/empty-state.tsx b/packages/ui/src/components/empty-state.tsx new file mode 100644 index 0000000..c875eb9 --- /dev/null +++ b/packages/ui/src/components/empty-state.tsx @@ -0,0 +1,51 @@ +import { Component } from "solid-js" +import { Loader2 } from "lucide-solid" + +const codeNomadIcon = new URL("../images/CodeNomad-Icon.png", import.meta.url).href + +interface EmptyStateProps { + onSelectFolder: () => void + isLoading?: boolean +} + +const EmptyState: Component = (props) => { + return ( +
+
+
+ CodeNomad logo +
+ +

CodeNomad

+

Select a folder to start coding with AI

+ + + + +

+ Keyboard shortcut: {navigator.platform.includes("Mac") ? "Cmd" : "Ctrl"}+N +

+ +
+

Examples: ~/projects/my-app

+

You can have multiple instances of the same folder

+
+
+
+ ) +} + +export default EmptyState diff --git a/packages/ui/src/components/environment-variables-editor.tsx b/packages/ui/src/components/environment-variables-editor.tsx new file mode 100644 index 0000000..c4e07b1 --- /dev/null +++ b/packages/ui/src/components/environment-variables-editor.tsx @@ -0,0 +1,148 @@ +import { Component, createSignal, For, Show } from "solid-js" +import { Plus, Trash2, Key, Globe } from "lucide-solid" +import { useConfig } from "../stores/preferences" + +interface EnvironmentVariablesEditorProps { + disabled?: boolean +} + +const EnvironmentVariablesEditor: Component = (props) => { + const { + preferences, + addEnvironmentVariable, + removeEnvironmentVariable, + updateEnvironmentVariables, + } = useConfig() + const [envVars, setEnvVars] = createSignal>(preferences().environmentVariables || {}) + const [newKey, setNewKey] = createSignal("") + const [newValue, setNewValue] = createSignal("") + + const entries = () => Object.entries(envVars()) + + function handleAddVariable() { + const key = newKey().trim() + const value = newValue().trim() + + if (!key) return + + addEnvironmentVariable(key, value) + setEnvVars({ ...envVars(), [key]: value }) + setNewKey("") + setNewValue("") + } + + function handleRemoveVariable(key: string) { + removeEnvironmentVariable(key) + const { [key]: removed, ...rest } = envVars() + setEnvVars(rest) + } + + function handleUpdateVariable(key: string, value: string) { + const updated = { ...envVars(), [key]: value } + setEnvVars(updated) + updateEnvironmentVariables(updated) + } + + function handleKeyPress(e: KeyboardEvent) { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault() + handleAddVariable() + } + } + + return ( +
+
+ + Environment Variables + + ({entries().length} variable{entries().length !== 1 ? "s" : ""}) + +
+ + {/* Existing variables */} + 0}> +
+ + {([key, value]) => ( +
+
+ + + handleUpdateVariable(key, e.currentTarget.value)} + class="flex-1 px-2.5 py-1.5 text-sm bg-surface-base border border-base rounded text-primary focus-ring-accent disabled:opacity-50 disabled:cursor-not-allowed" + placeholder="Variable value" + /> +
+ +
+ )} +
+
+
+ + {/* Add new variable */} +
+
+ + setNewKey(e.currentTarget.value)} + onKeyPress={handleKeyPress} + disabled={props.disabled} + class="flex-1 px-2.5 py-1.5 text-sm bg-surface-base border border-base rounded text-primary focus-ring-accent disabled:opacity-50 disabled:cursor-not-allowed" + placeholder="Variable name" + /> + setNewValue(e.currentTarget.value)} + onKeyPress={handleKeyPress} + disabled={props.disabled} + class="flex-1 px-2.5 py-1.5 text-sm bg-surface-base border border-base rounded text-primary focus-ring-accent disabled:opacity-50 disabled:cursor-not-allowed" + placeholder="Variable value" + /> +
+ +
+ + +
+ No environment variables configured. Add variables above to customize the OpenCode environment. +
+
+ +
+ These variables will be available in the OpenCode environment when starting instances. +
+
+ ) +} + +export default EnvironmentVariablesEditor diff --git a/packages/ui/src/components/filesystem-browser-dialog.tsx b/packages/ui/src/components/filesystem-browser-dialog.tsx new file mode 100644 index 0000000..b1c5d7a --- /dev/null +++ b/packages/ui/src/components/filesystem-browser-dialog.tsx @@ -0,0 +1,451 @@ +import { Component, Show, For, createSignal, createMemo, createEffect, onCleanup } from "solid-js" +import { Folder as FolderIcon, File as FileIcon, Loader2, Search, X, ArrowUpLeft } from "lucide-solid" +import type { FileSystemEntry, FileSystemListingMetadata } from "../../../server/src/api-types" +import { serverApi } from "../lib/api-client" +import { getLogger } from "../lib/logger" +const log = getLogger("actions") + + +const MAX_RESULTS = 200 + +function normalizeEntryPath(path: string | undefined): string { + if (!path || path === "." || path === "./") { + return "." + } + let cleaned = path.replace(/\\/g, "/") + if (cleaned.startsWith("./")) { + cleaned = cleaned.replace(/^\.\/+/, "") + } + if (cleaned.startsWith("/")) { + cleaned = cleaned.replace(/^\/+/, "") + } + cleaned = cleaned.replace(/\/+/g, "/") + return cleaned === "" ? "." : cleaned +} + +function resolveAbsolutePath(root: string, relativePath: string): string { + if (!root) { + return relativePath + } + if (!relativePath || relativePath === "." || relativePath === "./") { + return root + } + const separator = root.includes("\\") ? "\\" : "/" + const trimmedRoot = root.endsWith(separator) ? root : `${root}${separator}` + const normalized = relativePath.replace(/[\\/]+/g, separator).replace(/^[\\/]+/, "") + return `${trimmedRoot}${normalized}` +} + + +interface FileSystemBrowserDialogProps { + open: boolean + mode: "directories" | "files" + title: string + description?: string + onSelect: (absolutePath: string) => void + onClose: () => void +} + +type FolderRow = { type: "up"; path: string } | { type: "entry"; entry: FileSystemEntry } + +const FileSystemBrowserDialog: Component = (props) => { + const [rootPath, setRootPath] = createSignal("") + const [entries, setEntries] = createSignal([]) + const [currentMetadata, setCurrentMetadata] = createSignal(null) + const [loadingPath, setLoadingPath] = createSignal(null) + const [error, setError] = createSignal(null) + const [searchQuery, setSearchQuery] = createSignal("") + const [selectedIndex, setSelectedIndex] = createSignal(0) + + let searchInputRef: HTMLInputElement | undefined + + const directoryCache = new Map() + const metadataCache = new Map() + const inFlightLoads = new Map>() + + function resetDialogState() { + directoryCache.clear() + metadataCache.clear() + inFlightLoads.clear() + setEntries([]) + setCurrentMetadata(null) + setLoadingPath(null) + } + + async function fetchDirectory(path: string, makeCurrent = false): Promise { + const normalized = normalizeEntryPath(path) + + if (directoryCache.has(normalized) && metadataCache.has(normalized)) { + if (makeCurrent) { + setCurrentMetadata(metadataCache.get(normalized) ?? null) + setEntries(directoryCache.get(normalized) ?? []) + } + return metadataCache.get(normalized) as FileSystemListingMetadata + } + + if (inFlightLoads.has(normalized)) { + const metadata = await inFlightLoads.get(normalized)! + if (makeCurrent) { + setCurrentMetadata(metadata) + setEntries(directoryCache.get(normalized) ?? []) + } + return metadata + } + + const loadPromise = (async () => { + setLoadingPath(normalized) + const response = await serverApi.listFileSystem(normalized === "." ? "." : normalized, { + includeFiles: props.mode === "files", + }) + directoryCache.set(normalized, response.entries) + metadataCache.set(normalized, response.metadata) + if (!rootPath()) { + setRootPath(response.metadata.rootPath) + } + if (loadingPath() === normalized) { + setLoadingPath(null) + } + return response.metadata + })().catch((err) => { + if (loadingPath() === normalized) { + setLoadingPath(null) + } + throw err + }) + + inFlightLoads.set(normalized, loadPromise) + try { + const metadata = await loadPromise + if (makeCurrent) { + const key = normalizeEntryPath(metadata.currentPath) + setCurrentMetadata(metadata) + setEntries(directoryCache.get(key) ?? directoryCache.get(normalized) ?? []) + } + return metadata + } finally { + inFlightLoads.delete(normalized) + } + } + + async function refreshEntries() { + setError(null) + resetDialogState() + try { + const metadata = await fetchDirectory(".", true) + setRootPath(metadata.rootPath) + setEntries(directoryCache.get(normalizeEntryPath(metadata.currentPath)) ?? []) + } catch (err) { + const message = err instanceof Error ? err.message : "Unable to load filesystem" + setError(message) + } + } + + function describeLoadingPath() { + const path = loadingPath() + if (!path) { + return "filesystem" + } + if (path === ".") { + return rootPath() || "workspace root" + } + return resolveAbsolutePath(rootPath(), path) + } + + function currentAbsolutePath(): string { + const metadata = currentMetadata() + if (!metadata) { + return rootPath() + } + if (metadata.pathKind === "relative") { + return resolveAbsolutePath(rootPath(), metadata.currentPath) + } + return metadata.displayPath + } + + function handleOverlayClick(event: MouseEvent) { + if (event.target === event.currentTarget) { + props.onClose() + } + } + + function handleEntrySelect(entry: FileSystemEntry) { + const absolute = resolveAbsolutePath(rootPath(), entry.path) + props.onSelect(absolute) + } + + function handleNavigateTo(path: string) { + void fetchDirectory(path, true).catch((err) => { + log.error("Failed to open directory", err) + setError(err instanceof Error ? err.message : "Unable to open directory") + }) + } + + function handleNavigateUp() { + const parent = currentMetadata()?.parentPath + if (!parent) { + return + } + handleNavigateTo(parent) + } + + const filteredEntries = createMemo(() => { + const query = searchQuery().trim().toLowerCase() + const subset = entries().filter((entry) => (props.mode === "directories" ? entry.type === "directory" : true)) + if (!query) { + return subset + } + return subset.filter((entry) => { + const absolute = resolveAbsolutePath(rootPath(), entry.path) + return absolute.toLowerCase().includes(query) || entry.name.toLowerCase().includes(query) + }) + }) + + const visibleEntries = createMemo(() => filteredEntries().slice(0, MAX_RESULTS)) + + const folderRows = createMemo(() => { + const rows: FolderRow[] = [] + const metadata = currentMetadata() + if (metadata?.parentPath) { + rows.push({ type: "up", path: metadata.parentPath }) + } + for (const entry of visibleEntries()) { + rows.push({ type: "entry", entry }) + } + return rows + }) + + createEffect(() => { + const list = visibleEntries() + if (list.length === 0) { + setSelectedIndex(0) + return + } + if (selectedIndex() >= list.length) { + setSelectedIndex(list.length - 1) + } + }) + + createEffect(() => { + if (!props.open) { + return + } + setSearchQuery("") + setSelectedIndex(0) + void refreshEntries() + setTimeout(() => searchInputRef?.focus(), 50) + + const handleKeyDown = (event: KeyboardEvent) => { + if (!props.open) return + const results = visibleEntries() + if (event.key === "Escape") { + event.preventDefault() + props.onClose() + return + } + if (results.length === 0) { + return + } + if (event.key === "ArrowDown") { + event.preventDefault() + setSelectedIndex((prev) => Math.min(prev + 1, results.length - 1)) + } else if (event.key === "ArrowUp") { + event.preventDefault() + setSelectedIndex((prev) => Math.max(prev - 1, 0)) + } else if (event.key === "Enter") { + event.preventDefault() + const entry = results[selectedIndex()] + if (entry) { + handleEntrySelect(entry) + } + } + } + + window.addEventListener("keydown", handleKeyDown) + onCleanup(() => { + window.removeEventListener("keydown", handleKeyDown) + resetDialogState() + setRootPath("") + setError(null) + }) + }) + + return ( + +
+ +
+
+ ) +} + +export default FileSystemBrowserDialog + diff --git a/packages/ui/src/components/folder-selection-view.tsx b/packages/ui/src/components/folder-selection-view.tsx new file mode 100644 index 0000000..c495457 --- /dev/null +++ b/packages/ui/src/components/folder-selection-view.tsx @@ -0,0 +1,426 @@ +import { Component, createSignal, Show, For, onMount, onCleanup, createEffect } from "solid-js" +import { Folder, Clock, Trash2, FolderPlus, Settings, ChevronRight, MonitorUp } from "lucide-solid" +import { useConfig } from "../stores/preferences" +import AdvancedSettingsModal from "./advanced-settings-modal" +import DirectoryBrowserDialog from "./directory-browser-dialog" +import Kbd from "./kbd" +import { openNativeFolderDialog, supportsNativeDialogs } from "../lib/native/native-functions" + +const codeNomadLogo = new URL("../images/CodeNomad-Icon.png", import.meta.url).href + + +interface FolderSelectionViewProps { + onSelectFolder: (folder: string, binaryPath?: string) => void + isLoading?: boolean + advancedSettingsOpen?: boolean + onAdvancedSettingsOpen?: () => void + onAdvancedSettingsClose?: () => void + onOpenRemoteAccess?: () => void +} + +const FolderSelectionView: Component = (props) => { + const { recentFolders, removeRecentFolder, preferences } = useConfig() + const [selectedIndex, setSelectedIndex] = createSignal(0) + const [focusMode, setFocusMode] = createSignal<"recent" | "new" | null>("recent") + const [selectedBinary, setSelectedBinary] = createSignal(preferences().lastUsedBinary || "opencode") + const [isFolderBrowserOpen, setIsFolderBrowserOpen] = createSignal(false) + const nativeDialogsAvailable = supportsNativeDialogs() + let recentListRef: HTMLDivElement | undefined + + const folders = () => recentFolders() + const isLoading = () => Boolean(props.isLoading) + + // Update selected binary when preferences change + createEffect(() => { + const lastUsed = preferences().lastUsedBinary + if (!lastUsed) return + setSelectedBinary((current) => (current === lastUsed ? current : lastUsed)) + }) + + + function scrollToIndex(index: number) { + const container = recentListRef + if (!container) return + const element = container.querySelector(`[data-folder-index="${index}"]`) as HTMLElement | null + if (!element) return + + const containerRect = container.getBoundingClientRect() + const elementRect = element.getBoundingClientRect() + + if (elementRect.top < containerRect.top) { + container.scrollTop -= containerRect.top - elementRect.top + } else if (elementRect.bottom > containerRect.bottom) { + container.scrollTop += elementRect.bottom - containerRect.bottom + } + } + + + function handleKeyDown(e: KeyboardEvent) { + const normalizedKey = e.key.toLowerCase() + const isBrowseShortcut = (e.metaKey || e.ctrlKey) && !e.shiftKey && normalizedKey === "n" + const blockedKeys = [ + "ArrowDown", + "ArrowUp", + "PageDown", + "PageUp", + "Home", + "End", + "Enter", + "Backspace", + "Delete", + ] + + if (isLoading()) { + if (isBrowseShortcut || blockedKeys.includes(e.key)) { + e.preventDefault() + } + return + } + + const folderList = folders() + + if (isBrowseShortcut) { + e.preventDefault() + void handleBrowse() + return + } + + if (folderList.length === 0) return + + if (e.key === "ArrowDown") { + e.preventDefault() + const newIndex = Math.min(selectedIndex() + 1, folderList.length - 1) + setSelectedIndex(newIndex) + setFocusMode("recent") + scrollToIndex(newIndex) + } else if (e.key === "ArrowUp") { + e.preventDefault() + const newIndex = Math.max(selectedIndex() - 1, 0) + setSelectedIndex(newIndex) + setFocusMode("recent") + scrollToIndex(newIndex) + } else if (e.key === "PageDown") { + e.preventDefault() + const pageSize = 5 + const newIndex = Math.min(selectedIndex() + pageSize, folderList.length - 1) + setSelectedIndex(newIndex) + setFocusMode("recent") + scrollToIndex(newIndex) + } else if (e.key === "PageUp") { + e.preventDefault() + const pageSize = 5 + const newIndex = Math.max(selectedIndex() - pageSize, 0) + setSelectedIndex(newIndex) + setFocusMode("recent") + scrollToIndex(newIndex) + } else if (e.key === "Home") { + e.preventDefault() + setSelectedIndex(0) + setFocusMode("recent") + scrollToIndex(0) + } else if (e.key === "End") { + e.preventDefault() + const newIndex = folderList.length - 1 + setSelectedIndex(newIndex) + setFocusMode("recent") + scrollToIndex(newIndex) + } else if (e.key === "Enter") { + e.preventDefault() + handleEnterKey() + } else if (e.key === "Backspace" || e.key === "Delete") { + e.preventDefault() + if (folderList.length > 0 && focusMode() === "recent") { + const folder = folderList[selectedIndex()] + if (folder) { + handleRemove(folder.path) + } + } + } + } + + + function handleEnterKey() { + if (isLoading()) return + const folderList = folders() + const index = selectedIndex() + + const folder = folderList[index] + if (folder) { + handleFolderSelect(folder.path) + } + } + + + onMount(() => { + window.addEventListener("keydown", handleKeyDown) + onCleanup(() => { + window.removeEventListener("keydown", handleKeyDown) + }) + }) + + function formatRelativeTime(timestamp: number): string { + const seconds = Math.floor((Date.now() - timestamp) / 1000) + const minutes = Math.floor(seconds / 60) + const hours = Math.floor(minutes / 60) + const days = Math.floor(hours / 24) + + if (days > 0) return `${days}d ago` + if (hours > 0) return `${hours}h ago` + if (minutes > 0) return `${minutes}m ago` + return "just now" + } + + function handleFolderSelect(path: string) { + if (isLoading()) return + props.onSelectFolder(path, selectedBinary()) + } + + async function handleBrowse() { + if (isLoading()) return + setFocusMode("new") + if (nativeDialogsAvailable) { + const fallbackPath = folders()[0]?.path + const selected = await openNativeFolderDialog({ + title: "Select Workspace", + defaultPath: fallbackPath, + }) + if (selected) { + handleFolderSelect(selected) + } + return + } + setIsFolderBrowserOpen(true) + } + + function handleBrowserSelect(path: string) { + setIsFolderBrowserOpen(false) + handleFolderSelect(path) + } + + function handleBinaryChange(binary: string) { + + setSelectedBinary(binary) + } + + function handleRemove(path: string, e?: Event) { + if (isLoading()) return + e?.stopPropagation() + removeRecentFolder(path) + + const folderList = folders() + if (selectedIndex() >= folderList.length && folderList.length > 0) { + setSelectedIndex(folderList.length - 1) + } + } + + + function getDisplayPath(path: string): string { + if (path.startsWith("/Users/")) { + return path.replace(/^\/Users\/[^/]+/, "~") + } + return path + } + + return ( + <> +
+
+ +
+ +
+
+
+
+ CodeNomad logo +
+

CodeNomad

+

Select a folder to start coding with AI

+
+ + +
+ + 0} + fallback={ +
+
+ +
+

No Recent Folders

+

Browse for a folder to get started

+
+ } + > +
+
+

Recent Folders

+

+ {folders().length} {folders().length === 1 ? "folder" : "folders"} available +

+
+
(recentListRef = el)}> + + {(folder, index) => ( +
+
+ + +
+
+ )} +
+
+
+
+ +
+ + +
+ +
+ + {/* Advanced settings section */} +
+ +
+
+
+ + +
+ +
+
+
+

Starting instance…

+

Hang tight while we prepare your workspace.

+
+
+ +
+ + props.onAdvancedSettingsClose?.()} + selectedBinary={selectedBinary()} + onBinaryChange={handleBinaryChange} + isLoading={props.isLoading} + /> + + setIsFolderBrowserOpen(false)} + onSelect={handleBrowserSelect} + /> + + ) +} + +export default FolderSelectionView diff --git a/packages/ui/src/components/hint-row.tsx b/packages/ui/src/components/hint-row.tsx new file mode 100644 index 0000000..40d3745 --- /dev/null +++ b/packages/ui/src/components/hint-row.tsx @@ -0,0 +1,12 @@ +import { Component, JSX } from "solid-js" + +interface HintRowProps { + children: JSX.Element + class?: string +} + +const HintRow: Component = (props) => { + return {props.children} +} + +export default HintRow diff --git a/packages/ui/src/components/info-view.tsx b/packages/ui/src/components/info-view.tsx new file mode 100644 index 0000000..2310554 --- /dev/null +++ b/packages/ui/src/components/info-view.tsx @@ -0,0 +1,161 @@ +import { Component, For, createSignal, createEffect, Show, onMount, onCleanup, createMemo } from "solid-js" +import { instances, getInstanceLogs, isInstanceLogStreaming, setInstanceLogStreaming } from "../stores/instances" +import { ChevronDown } from "lucide-solid" +import InstanceInfo from "./instance-info" + +interface InfoViewProps { + instanceId: string +} + +const logsScrollState = new Map() + +const InfoView: Component = (props) => { + let scrollRef: HTMLDivElement | undefined + const savedState = logsScrollState.get(props.instanceId) + const [autoScroll, setAutoScroll] = createSignal(savedState?.autoScroll ?? false) + + const instance = () => instances().get(props.instanceId) + const logs = createMemo(() => getInstanceLogs(props.instanceId)) + const streamingEnabled = createMemo(() => isInstanceLogStreaming(props.instanceId)) + + const handleEnableLogs = () => setInstanceLogStreaming(props.instanceId, true) + const handleDisableLogs = () => setInstanceLogStreaming(props.instanceId, false) + + onMount(() => { + + if (scrollRef && savedState) { + scrollRef.scrollTop = savedState.scrollTop + } + }) + + onCleanup(() => { + if (scrollRef) { + logsScrollState.set(props.instanceId, { + scrollTop: scrollRef.scrollTop, + autoScroll: autoScroll(), + }) + } + }) + + createEffect(() => { + if (autoScroll() && scrollRef && logs().length > 0) { + scrollRef.scrollTop = scrollRef.scrollHeight + } + }) + + const handleScroll = () => { + if (!scrollRef) return + + const isAtBottom = scrollRef.scrollHeight - scrollRef.scrollTop <= scrollRef.clientHeight + 50 + + setAutoScroll(isAtBottom) + } + + const scrollToBottom = () => { + if (scrollRef) { + scrollRef.scrollTop = scrollRef.scrollHeight + setAutoScroll(true) + } + } + + const formatTime = (timestamp: number) => { + const date = new Date(timestamp) + return date.toLocaleTimeString("en-US", { + hour12: false, + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }) + } + + const getLevelColor = (level: string) => { + switch (level) { + case "error": + return "log-level-error" + case "warn": + return "log-level-warn" + case "debug": + return "log-level-debug" + default: + return "log-level-default" + } + } + + return ( +
+
+
+ {(inst) => } +
+ +
+
+

Server Logs

+
+ + Show server logs + + } + > + + +
+
+ +
+ +

Server logs are paused

+

Enable streaming to watch your OpenCode server activity.

+ +
+ } + > + 0} + fallback={
Waiting for server output...
} + > + + {(entry) => ( +
+ + {formatTime(entry.timestamp)} + + {entry.message} +
+ )} +
+
+ +
+ + + + +
+
+
+ ) +} + + +export default InfoView diff --git a/packages/ui/src/components/instance-disconnected-modal.tsx b/packages/ui/src/components/instance-disconnected-modal.tsx new file mode 100644 index 0000000..af4c8c3 --- /dev/null +++ b/packages/ui/src/components/instance-disconnected-modal.tsx @@ -0,0 +1,47 @@ +import { Dialog } from "@kobalte/core/dialog" + +interface InstanceDisconnectedModalProps { + open: boolean + folder?: string + reason?: string + onClose: () => void +} + +export default function InstanceDisconnectedModal(props: InstanceDisconnectedModalProps) { + const folderLabel = props.folder || "this workspace" + const reasonLabel = props.reason || "The server stopped responding" + + return ( + + + +
+ +
+ Instance Disconnected + + {folderLabel} can no longer be reached. Close the tab to continue working. + +
+ +
+

Details

+

{reasonLabel}

+ {props.folder && ( +

+ Folder: {props.folder} +

+ )} +
+ +
+ +
+
+
+
+
+ ) +} diff --git a/packages/ui/src/components/instance-info.tsx b/packages/ui/src/components/instance-info.tsx new file mode 100644 index 0000000..15db4ac --- /dev/null +++ b/packages/ui/src/components/instance-info.tsx @@ -0,0 +1,162 @@ +import { Component, For, Show, createMemo } from "solid-js" +import type { Instance } from "../types/instance" +import { useOptionalInstanceMetadataContext } from "../lib/contexts/instance-metadata-context" +import InstanceServiceStatus from "./instance-service-status" + +interface InstanceInfoProps { + instance: Instance + compact?: boolean +} + +const InstanceInfo: Component = (props) => { + const metadataContext = useOptionalInstanceMetadataContext() + const isLoadingMetadata = metadataContext?.isLoading ?? (() => false) + const instanceAccessor = metadataContext?.instance ?? (() => props.instance) + const metadataAccessor = metadataContext?.metadata ?? (() => props.instance.metadata) + + const currentInstance = () => instanceAccessor() + const metadata = () => metadataAccessor() + const binaryVersion = () => currentInstance().binaryVersion || metadata()?.version + const environmentVariables = () => currentInstance().environmentVariables + const environmentEntries = createMemo(() => { + const env = environmentVariables() + return env ? Object.entries(env) : [] + }) + + return ( +
+
+

Instance Information

+
+
+
+
Folder
+
+ {currentInstance().folder} +
+
+ + + {(project) => ( + <> +
+
+ Project +
+
+ {project().id} +
+
+ + +
+
+ Version Control +
+
+ + + + {project().vcs} +
+
+
+ + )} +
+ + +
+
+ OpenCode Version +
+
+ v{binaryVersion()} +
+
+
+ + +
+
+ Binary Path +
+
+ {currentInstance().binaryPath} +
+
+
+ + 0}> +
+
+ Environment Variables ({environmentEntries().length}) +
+
+ + {([key, value]) => ( +
+ + {key} + + + {value} + +
+ )} +
+
+
+
+ + + + +
+
+ + + + + Loading... +
+
+
+ +
+
Server
+
+
+ Port: + {currentInstance().port} +
+
+ PID: + {currentInstance().pid} +
+
+ Status: + +
+ {currentInstance().status} + +
+
+
+
+
+ ) +} + +export default InstanceInfo diff --git a/packages/ui/src/components/instance-service-status.tsx b/packages/ui/src/components/instance-service-status.tsx new file mode 100644 index 0000000..dc61041 --- /dev/null +++ b/packages/ui/src/components/instance-service-status.tsx @@ -0,0 +1,224 @@ +import { For, Show, createMemo, createSignal, type Component } from "solid-js" +import Switch from "@suid/material/Switch" +import type { Instance, RawMcpStatus } from "../types/instance" +import { useOptionalInstanceMetadataContext } from "../lib/contexts/instance-metadata-context" +import { getLogger } from "../lib/logger" + +const log = getLogger("session") + +type ServiceSection = "lsp" | "mcp" + +interface InstanceServiceStatusProps { + sections?: ServiceSection[] + showSectionHeadings?: boolean + class?: string + initialInstance?: Instance +} + +type ParsedMcpStatus = { + name: string + status: "running" | "stopped" | "error" + error?: string +} + +function parseMcpStatus(status?: RawMcpStatus): ParsedMcpStatus[] { + if (!status || typeof status !== "object") return [] + const result: ParsedMcpStatus[] = [] + for (const [name, value] of Object.entries(status)) { + if (!value || typeof value !== "object") continue + const rawStatus = (value as { status?: string }).status + if (!rawStatus) continue + let mapped: ParsedMcpStatus["status"] + if (rawStatus === "connected") mapped = "running" + else if (rawStatus === "failed") mapped = "error" + else mapped = "stopped" + result.push({ + name, + status: mapped, + error: typeof (value as { error?: unknown }).error === "string" ? (value as { error?: string }).error : undefined, + }) + } + return result +} + +const InstanceServiceStatus: Component = (props) => { + const metadataContext = useOptionalInstanceMetadataContext() + const instance = metadataContext?.instance ?? (() => { + if (props.initialInstance) { + return props.initialInstance + } + throw new Error("InstanceServiceStatus requires InstanceMetadataProvider or initialInstance prop") + }) + const isLoading = metadataContext?.isLoading ?? (() => false) + const refreshMetadata = metadataContext?.refreshMetadata ?? (async () => Promise.resolve()) + const sections = createMemo(() => props.sections ?? ["lsp", "mcp"]) + const includeLsp = createMemo(() => sections().includes("lsp")) + const includeMcp = createMemo(() => sections().includes("mcp")) + const showHeadings = () => props.showSectionHeadings !== false + + const metadataAccessor = metadataContext?.metadata ?? (() => instance().metadata) + const metadata = createMemo(() => metadataAccessor()) + const hasLspMetadata = () => metadata()?.lspStatus !== undefined + const hasMcpMetadata = () => metadata()?.mcpStatus !== undefined + const lspServers = createMemo(() => metadata()?.lspStatus ?? []) + const mcpServers = createMemo(() => parseMcpStatus(metadata()?.mcpStatus ?? undefined)) + + const isLspLoading = () => isLoading() || !hasLspMetadata() + const isMcpLoading = () => isLoading() || !hasMcpMetadata() + + + const [pendingMcpActions, setPendingMcpActions] = createSignal>({}) + + const setPendingMcpAction = (name: string, action?: "connect" | "disconnect") => { + setPendingMcpActions((prev) => { + const next = { ...prev } + if (action) next[name] = action + else delete next[name] + return next + }) + } + + const toggleMcpServer = async (serverName: string, shouldEnable: boolean) => { + const client = instance().client + if (!client?.mcp) return + const action: "connect" | "disconnect" = shouldEnable ? "connect" : "disconnect" + setPendingMcpAction(serverName, action) + try { + if (shouldEnable) { + await client.mcp.connect({ path: { name: serverName } }) + } else { + await client.mcp.disconnect({ path: { name: serverName } }) + } + await refreshMetadata() + } catch (error) { + log.error("Failed to toggle MCP server", { serverName, action, error }) + } finally { + setPendingMcpAction(serverName) + } + } + + const renderEmptyState = (message: string) => ( +

+ {message} +

+ ) + + const renderLspSection = () => ( +
+ +
+ LSP Servers +
+
+ 0} + fallback={renderEmptyState(isLspLoading() ? "Loading LSP servers..." : "No LSP servers detected.")} + > +
+ + {(server) => ( +
+
+
+ {server.name ?? server.id} + + {server.root} + +
+
+
+ {server.status === "connected" ? "Connected" : "Error"} +
+
+
+ )} + +
+ +
+ ) + + const renderMcpSection = () => ( +
+ +
+ MCP Servers +
+
+ 0} + fallback={renderEmptyState(isMcpLoading() ? "Loading MCP servers..." : "No MCP servers detected.")} + > +
+ + {(server) => { + const pendingAction = () => pendingMcpActions()[server.name] + const isPending = () => Boolean(pendingAction()) + const isRunning = () => server.status === "running" + const switchDisabled = () => isPending() || !instance().client + const statusDotClass = () => { + if (isPending()) return "status-dot animate-pulse" + if (server.status === "running") return "status-dot ready animate-pulse" + if (server.status === "error") return "status-dot error" + return "status-dot stopped" + } + const statusDotStyle = () => (isPending() ? { background: "var(--status-warning)" } : undefined) + return ( +
+
+ {server.name} +
+
+ + + + + + +
+
+
+ { + if (switchDisabled()) return + void toggleMcpServer(server.name, Boolean(checked)) + }} + /> +
+
+ +
+ + {(error) => ( +
+ {error()} +
+ )} +
+
+ ) + }} + +
+ +
+ ) + + return ( +
+ {renderLspSection()} + {renderMcpSection()} +
+ ) +} + +export default InstanceServiceStatus diff --git a/packages/ui/src/components/instance-tab.tsx b/packages/ui/src/components/instance-tab.tsx new file mode 100644 index 0000000..5c4e821 --- /dev/null +++ b/packages/ui/src/components/instance-tab.tsx @@ -0,0 +1,59 @@ +import { Component } from "solid-js" +import type { Instance } from "../types/instance" +import { FolderOpen, X } from "lucide-solid" + +interface InstanceTabProps { + instance: Instance + active: boolean + onSelect: () => void + onClose: () => void +} + +function formatFolderName(path: string, instances: Instance[], currentInstance: Instance): string { + const name = path.split("/").pop() || path + + const duplicates = instances.filter((i) => { + const iName = i.folder.split("/").pop() || i.folder + return iName === name + }) + + if (duplicates.length > 1) { + const index = duplicates.findIndex((i) => i.id === currentInstance.id) + return `~/${name} (${index + 1})` + } + + return `~/${name}` +} + +const InstanceTab: Component = (props) => { + return ( +
+ +
+ ) +} + +export default InstanceTab diff --git a/packages/ui/src/components/instance-tabs.tsx b/packages/ui/src/components/instance-tabs.tsx new file mode 100644 index 0000000..35e7162 --- /dev/null +++ b/packages/ui/src/components/instance-tabs.tsx @@ -0,0 +1,71 @@ +import { Component, For, Show } from "solid-js" +import type { Instance } from "../types/instance" +import InstanceTab from "./instance-tab" +import KeyboardHint from "./keyboard-hint" +import { Plus, MonitorUp } from "lucide-solid" +import { keyboardRegistry } from "../lib/keyboard-registry" + +interface InstanceTabsProps { + instances: Map + activeInstanceId: string | null + onSelect: (instanceId: string) => void + onClose: (instanceId: string) => void + onNew: () => void + onOpenRemoteAccess?: () => void +} + +const InstanceTabs: Component = (props) => { + return ( +
+
+
+
+
+ + {([id, instance]) => ( + props.onSelect(id)} + onClose={() => props.onClose(id)} + /> + )} + + +
+
+ 1}> +
+ +
+
+ + + +
+
+
+
+ + ) +} + +export default InstanceTabs diff --git a/packages/ui/src/components/instance-welcome-view.tsx b/packages/ui/src/components/instance-welcome-view.tsx new file mode 100644 index 0000000..15ca036 --- /dev/null +++ b/packages/ui/src/components/instance-welcome-view.tsx @@ -0,0 +1,579 @@ +import { Component, createSignal, Show, For, createEffect, onMount, onCleanup, createMemo } from "solid-js" +import { Loader2, Pencil, Trash2 } from "lucide-solid" + +import type { Instance } from "../types/instance" +import { getParentSessions, createSession, setActiveParentSession, deleteSession, loading, renameSession } from "../stores/sessions" +import InstanceInfo from "./instance-info" +import Kbd from "./kbd" +import SessionRenameDialog from "./session-rename-dialog" +import { keyboardRegistry, type KeyboardShortcut } from "../lib/keyboard-registry" +import { isMac } from "../lib/keyboard-utils" +import { showToastNotification } from "../lib/notifications" +import { getLogger } from "../lib/logger" +const log = getLogger("actions") + + + +interface InstanceWelcomeViewProps { + instance: Instance +} + +const InstanceWelcomeView: Component = (props) => { + const [isCreating, setIsCreating] = createSignal(false) + const [selectedIndex, setSelectedIndex] = createSignal(0) + const [focusMode, setFocusMode] = createSignal<"sessions" | "new-session" | null>("sessions") + const [showInstanceInfoOverlay, setShowInstanceInfoOverlay] = createSignal(false) + const [isDesktopLayout, setIsDesktopLayout] = createSignal( + typeof window !== "undefined" ? window.matchMedia("(min-width: 1024px)").matches : false, + ) + const [renameTarget, setRenameTarget] = createSignal<{ id: string; title: string; label: string } | null>(null) + const [isRenaming, setIsRenaming] = createSignal(false) + + const parentSessions = () => getParentSessions(props.instance.id) + const isFetchingSessions = createMemo(() => Boolean(loading().fetchingSessions.get(props.instance.id))) + const isSessionDeleting = (sessionId: string) => { + const deleting = loading().deletingSession.get(props.instance.id) + return deleting ? deleting.has(sessionId) : false + } + const newSessionShortcut = createMemo(() => { + const registered = keyboardRegistry.get("session-new") + if (registered) return registered + return { + id: "session-new-display", + key: "n", + modifiers: { + shift: true, + meta: isMac(), + ctrl: !isMac(), + }, + handler: () => {}, + description: "New Session", + context: "global", + } + }) + const newSessionShortcutString = createMemo(() => (isMac() ? "cmd+shift+n" : "ctrl+shift+n")) + + createEffect(() => { + const sessions = parentSessions() + if (sessions.length === 0) { + setFocusMode("new-session") + setSelectedIndex(0) + } else { + setFocusMode("sessions") + setSelectedIndex(0) + } + }) + + const openInstanceInfoOverlay = () => { + if (isDesktopLayout()) return + setShowInstanceInfoOverlay(true) + } + const closeInstanceInfoOverlay = () => setShowInstanceInfoOverlay(false) + + function scrollToIndex(index: number) { + const element = document.querySelector(`[data-session-index="${index}"]`) + if (element) { + element.scrollIntoView({ block: "nearest", behavior: "auto" }) + } + } + + function handleKeyDown(e: KeyboardEvent) { + let activeElement: HTMLElement | null = null + if (typeof document !== "undefined") { + activeElement = document.activeElement as HTMLElement | null + } + const insideModal = activeElement?.closest(".modal-surface") || activeElement?.closest("[role='dialog']") + const isEditingField = + activeElement && + (["INPUT", "TEXTAREA", "SELECT"].includes(activeElement.tagName) || + activeElement.isContentEditable || + Boolean(insideModal)) + + if (isEditingField) { + if (insideModal && e.key === "Escape" && renameTarget()) { + e.preventDefault() + closeRenameDialog() + } + return + } + + if (showInstanceInfoOverlay()) { + if (e.key === "Escape") { + e.preventDefault() + closeInstanceInfoOverlay() + } + return + } + + const sessions = parentSessions() + + if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key.toLowerCase() === "n") { + e.preventDefault() + handleNewSession() + return + } + + if (sessions.length === 0) return + + const listFocused = focusMode() === "sessions" + + if (e.key === "ArrowDown") { + if (!listFocused) { + setFocusMode("sessions") + setSelectedIndex(0) + } + e.preventDefault() + const newIndex = Math.min(selectedIndex() + 1, sessions.length - 1) + setSelectedIndex(newIndex) + scrollToIndex(newIndex) + return + } + + if (e.key === "ArrowUp") { + if (!listFocused) { + setFocusMode("sessions") + setSelectedIndex(Math.max(parentSessions().length - 1, 0)) + } + e.preventDefault() + const newIndex = Math.max(selectedIndex() - 1, 0) + setSelectedIndex(newIndex) + scrollToIndex(newIndex) + return + } + + if (!listFocused) { + return + } + + if (e.key === "PageDown") { + e.preventDefault() + const pageSize = 5 + const newIndex = Math.min(selectedIndex() + pageSize, sessions.length - 1) + setSelectedIndex(newIndex) + scrollToIndex(newIndex) + } else if (e.key === "PageUp") { + e.preventDefault() + const pageSize = 5 + const newIndex = Math.max(selectedIndex() - pageSize, 0) + setSelectedIndex(newIndex) + scrollToIndex(newIndex) + } else if (e.key === "Home") { + e.preventDefault() + setSelectedIndex(0) + scrollToIndex(0) + } else if (e.key === "End") { + e.preventDefault() + const newIndex = sessions.length - 1 + setSelectedIndex(newIndex) + scrollToIndex(newIndex) + } else if (e.key === "Enter") { + e.preventDefault() + void handleEnterKey() + } else if (e.key === "Delete" || e.key === "Backspace") { + e.preventDefault() + void handleDeleteKey() + } + } + + + async function handleEnterKey() { + const sessions = parentSessions() + const index = selectedIndex() + + if (index < sessions.length) { + await handleSessionSelect(sessions[index].id) + } + } + + async function handleDeleteKey() { + const sessions = parentSessions() + const index = selectedIndex() + + if (index >= sessions.length) { + return + } + + await handleSessionDelete(sessions[index].id) + + const updatedSessions = parentSessions() + if (updatedSessions.length === 0) { + setFocusMode("new-session") + setSelectedIndex(0) + return + } + + const nextIndex = Math.min(index, updatedSessions.length - 1) + setSelectedIndex(nextIndex) + setFocusMode("sessions") + scrollToIndex(nextIndex) + } + + onMount(() => { + window.addEventListener("keydown", handleKeyDown) + + onCleanup(() => { + window.removeEventListener("keydown", handleKeyDown) + }) + }) + + onMount(() => { + const mediaQuery = window.matchMedia("(min-width: 1024px)") + const handleMediaChange = (matches: boolean) => { + setIsDesktopLayout(matches) + if (matches) { + closeInstanceInfoOverlay() + } + } + + const listener = (event: MediaQueryListEvent) => handleMediaChange(event.matches) + + if (typeof mediaQuery.addEventListener === "function") { + mediaQuery.addEventListener("change", listener) + onCleanup(() => { + mediaQuery.removeEventListener("change", listener) + }) + } else { + mediaQuery.addListener(listener) + onCleanup(() => { + mediaQuery.removeListener(listener) + }) + } + + handleMediaChange(mediaQuery.matches) + }) + + function formatRelativeTime(timestamp: number): string { + const seconds = Math.floor((Date.now() - timestamp) / 1000) + const minutes = Math.floor(seconds / 60) + const hours = Math.floor(minutes / 60) + const days = Math.floor(hours / 24) + + if (days > 0) return `${days}d ago` + if (hours > 0) return `${hours}h ago` + if (minutes > 0) return `${minutes}m ago` + return "just now" + } + + function formatTimestamp(timestamp: number): string { + return new Date(timestamp).toLocaleString() + } + + async function handleSessionSelect(sessionId: string) { + setActiveParentSession(props.instance.id, sessionId) + } + + async function handleSessionDelete(sessionId: string) { + if (isSessionDeleting(sessionId)) return + + try { + await deleteSession(props.instance.id, sessionId) + } catch (error) { + log.error("Failed to delete session:", error) + } + } + + function openRenameDialogForSession(sessionId: string, title: string) { + const label = title && title.trim() ? title : sessionId + setRenameTarget({ id: sessionId, title: title ?? "", label }) + } + + function closeRenameDialog() { + setRenameTarget(null) + } + + async function handleRenameSubmit(nextTitle: string) { + const target = renameTarget() + if (!target) return + + setIsRenaming(true) + try { + await renameSession(props.instance.id, target.id, nextTitle) + setRenameTarget(null) + } catch (error) { + log.error("Failed to rename session:", error) + showToastNotification({ message: "Unable to rename session", variant: "error" }) + } finally { + setIsRenaming(false) + } + } + + async function handleNewSession() { + if (isCreating()) return + + setIsCreating(true) + + try { + const session = await createSession(props.instance.id) + setActiveParentSession(props.instance.id, session.id) + } catch (error) { + log.error("Failed to create session:", error) + } finally { + setIsCreating(false) + } + } + + return ( +
+
+
+ 0} + fallback={ + +
+ + + +
+

No Previous Sessions

+

Create a new session below to get started

+ + + +
+ } + > +
+
+ +
+

Loading Sessions

+

Fetching your previous sessions...

+
+ + } + > +
+
+
+
+

Resume Session

+

+ {parentSessions().length} {parentSessions().length === 1 ? "session" : "sessions"} available +

+
+ + + +
+
+
+ + {(session, index) => { + const isFocused = () => focusMode() === "sessions" && selectedIndex() === index() + return ( +
+
+ + +
+ ↵ + + +
+
+
+
+ ) + }} +
+
+
+ + +
+
+

Start New Session

+

We’ll reuse your last agent/model automatically

+
+
+
+ +
+
+
+
+ + +
+ + +
+
+
event.stopPropagation()} + > +
+ +
+
+ +
+
+
+
+
+ + + + +
+ ) +} + +export default InstanceWelcomeView diff --git a/packages/ui/src/components/instance/instance-shell2.tsx b/packages/ui/src/components/instance/instance-shell2.tsx new file mode 100644 index 0000000..7f345f8 --- /dev/null +++ b/packages/ui/src/components/instance/instance-shell2.tsx @@ -0,0 +1,1308 @@ +import { + For, + Show, + batch, + createEffect, + createMemo, + createSignal, + onCleanup, + onMount, + type Accessor, + type Component, +} from "solid-js" +import type { ToolState } from "@opencode-ai/sdk" +import { Accordion } from "@kobalte/core" +import { ChevronDown } from "lucide-solid" +import AppBar from "@suid/material/AppBar" +import Box from "@suid/material/Box" +import Divider from "@suid/material/Divider" +import Drawer from "@suid/material/Drawer" +import IconButton from "@suid/material/IconButton" +import Toolbar from "@suid/material/Toolbar" +import Typography from "@suid/material/Typography" +import useMediaQuery from "@suid/material/useMediaQuery" +import CloseIcon from "@suid/icons-material/Close" +import MenuIcon from "@suid/icons-material/Menu" +import MenuOpenIcon from "@suid/icons-material/MenuOpen" +import PushPinIcon from "@suid/icons-material/PushPin" +import PushPinOutlinedIcon from "@suid/icons-material/PushPinOutlined" +import type { Instance } from "../../types/instance" +import type { Command } from "../../lib/commands" +import { + activeParentSessionId, + activeSessionId as activeSessionMap, + getSessionFamily, + getSessionInfo, + setActiveSession, +} from "../../stores/sessions" +import { keyboardRegistry, type KeyboardShortcut } from "../../lib/keyboard-registry" +import { messageStoreBus } from "../../stores/message-v2/bus" +import { clearSessionRenderCache } from "../message-block" +import { buildCustomCommandEntries } from "../../lib/command-utils" +import { getCommands as getInstanceCommands } from "../../stores/commands" +import { isOpen as isCommandPaletteOpen, hideCommandPalette, showCommandPalette } from "../../stores/command-palette" +import SessionList from "../session-list" +import KeyboardHint from "../keyboard-hint" +import InstanceWelcomeView from "../instance-welcome-view" +import InfoView from "../info-view" +import InstanceServiceStatus from "../instance-service-status" +import AgentSelector from "../agent-selector" +import ModelSelector from "../model-selector" +import CommandPalette from "../command-palette" +import Kbd from "../kbd" +import { TodoListView } from "../tool-call/renderers/todo" +import ContextUsagePanel from "../session/context-usage-panel" +import SessionView from "../session/session-view" +import { formatTokenTotal } from "../../lib/formatters" +import { sseManager } from "../../lib/sse-manager" +import { getLogger } from "../../lib/logger" +import { + SESSION_SIDEBAR_EVENT, + type SessionSidebarRequestAction, + type SessionSidebarRequestDetail, +} from "../../lib/session-sidebar-events" + +const log = getLogger("session") + +interface InstanceShellProps { + instance: Instance + escapeInDebounce: boolean + paletteCommands: Accessor + onCloseSession: (sessionId: string) => Promise | void + onNewSession: () => Promise | void + handleSidebarAgentChange: (sessionId: string, agent: string) => Promise + handleSidebarModelChange: (sessionId: string, model: { providerId: string; modelId: string }) => Promise + onExecuteCommand: (command: Command) => void + tabBarOffset: number +} + +const DEFAULT_SESSION_SIDEBAR_WIDTH = 280 +const MIN_SESSION_SIDEBAR_WIDTH = 220 +const MAX_SESSION_SIDEBAR_WIDTH = 360 +const RIGHT_DRAWER_WIDTH = 260 +const MIN_RIGHT_DRAWER_WIDTH = 200 +const MAX_RIGHT_DRAWER_WIDTH = 380 +const SESSION_CACHE_LIMIT = 2 +const APP_BAR_HEIGHT = 56 +const LEFT_DRAWER_STORAGE_KEY = "opencode-session-sidebar-width-v8" +const RIGHT_DRAWER_STORAGE_KEY = "opencode-session-right-drawer-width-v1" +const LEFT_PIN_STORAGE_KEY = "opencode-session-left-drawer-pinned-v1" +const RIGHT_PIN_STORAGE_KEY = "opencode-session-right-drawer-pinned-v1" + + + + +type LayoutMode = "desktop" | "tablet" | "phone" + +const clampWidth = (value: number) => Math.min(MAX_SESSION_SIDEBAR_WIDTH, Math.max(MIN_SESSION_SIDEBAR_WIDTH, value)) +const clampRightWidth = (value: number) => Math.min(MAX_RIGHT_DRAWER_WIDTH, Math.max(MIN_RIGHT_DRAWER_WIDTH, value)) +const getPinStorageKey = (side: "left" | "right") => (side === "left" ? LEFT_PIN_STORAGE_KEY : RIGHT_PIN_STORAGE_KEY) +function readStoredPinState(side: "left" | "right", defaultValue: boolean) { + if (typeof window === "undefined") return defaultValue + const stored = window.localStorage.getItem(getPinStorageKey(side)) + if (stored === "true") return true + if (stored === "false") return false + return defaultValue +} +function persistPinState(side: "left" | "right", value: boolean) { + if (typeof window === "undefined") return + window.localStorage.setItem(getPinStorageKey(side), value ? "true" : "false") +} + +const InstanceShell2: Component = (props) => { + const [sessionSidebarWidth, setSessionSidebarWidth] = createSignal(DEFAULT_SESSION_SIDEBAR_WIDTH) + const [rightDrawerWidth, setRightDrawerWidth] = createSignal(RIGHT_DRAWER_WIDTH) + const [leftPinned, setLeftPinned] = createSignal(true) + const [leftOpen, setLeftOpen] = createSignal(true) + const [rightPinned, setRightPinned] = createSignal(true) + const [rightOpen, setRightOpen] = createSignal(true) + const [cachedSessionIds, setCachedSessionIds] = createSignal([]) + const [pendingEvictions, setPendingEvictions] = createSignal([]) + const [drawerHost, setDrawerHost] = createSignal(null) + const [floatingDrawerTop, setFloatingDrawerTop] = createSignal(0) + const [floatingDrawerHeight, setFloatingDrawerHeight] = createSignal(0) + const [leftDrawerContentEl, setLeftDrawerContentEl] = createSignal(null) + const [rightDrawerContentEl, setRightDrawerContentEl] = createSignal(null) + const [leftToggleButtonEl, setLeftToggleButtonEl] = createSignal(null) + const [rightToggleButtonEl, setRightToggleButtonEl] = createSignal(null) + const [activeResizeSide, setActiveResizeSide] = createSignal<"left" | "right" | null>(null) + const [resizeStartX, setResizeStartX] = createSignal(0) + const [resizeStartWidth, setResizeStartWidth] = createSignal(0) + const [rightPanelExpandedItems, setRightPanelExpandedItems] = createSignal(["lsp", "mcp"]) + + const messageStore = createMemo(() => messageStoreBus.getOrCreate(props.instance.id)) + + const desktopQuery = useMediaQuery("(min-width: 1280px)") + + const tabletQuery = useMediaQuery("(min-width: 768px)") + + const layoutMode = createMemo(() => { + if (desktopQuery()) return "desktop" + if (tabletQuery()) return "tablet" + return "phone" + }) + + const isPhoneLayout = createMemo(() => layoutMode() === "phone") + const leftPinningSupported = createMemo(() => layoutMode() === "desktop") + const rightPinningSupported = createMemo(() => layoutMode() !== "phone") + + const persistPinIfSupported = (side: "left" | "right", value: boolean) => { + if (side === "left" && !leftPinningSupported()) return + if (side === "right" && !rightPinningSupported()) return + persistPinState(side, value) + } + + createEffect(() => { + switch (layoutMode()) { + case "desktop": { + const leftSaved = readStoredPinState("left", true) + const rightSaved = readStoredPinState("right", true) + setLeftPinned(leftSaved) + setLeftOpen(leftSaved) + setRightPinned(rightSaved) + setRightOpen(rightSaved) + break + } + case "tablet": { + const rightSaved = readStoredPinState("right", true) + setLeftPinned(false) + setLeftOpen(false) + setRightPinned(rightSaved) + setRightOpen(rightSaved) + break + } + default: + setLeftPinned(false) + setLeftOpen(false) + setRightPinned(false) + setRightOpen(false) + break + } + }) + + const measureDrawerHost = () => { + if (typeof window === "undefined") return + const host = drawerHost() + if (!host) return + const rect = host.getBoundingClientRect() + const toolbar = host.querySelector(".session-toolbar") + const toolbarHeight = toolbar?.offsetHeight ?? APP_BAR_HEIGHT + setFloatingDrawerTop(rect.top + toolbarHeight) + setFloatingDrawerHeight(Math.max(0, rect.height - toolbarHeight)) + } + + onMount(() => { + if (typeof window === "undefined") return + + const savedLeft = window.localStorage.getItem(LEFT_DRAWER_STORAGE_KEY) + if (savedLeft) { + const parsed = Number.parseInt(savedLeft, 10) + if (Number.isFinite(parsed)) { + setSessionSidebarWidth(clampWidth(parsed)) + } + } + + const savedRight = window.localStorage.getItem(RIGHT_DRAWER_STORAGE_KEY) + if (savedRight) { + const parsed = Number.parseInt(savedRight, 10) + if (Number.isFinite(parsed)) { + setRightDrawerWidth(clampRightWidth(parsed)) + } + } + + const handleResize = () => { + const width = clampWidth(window.innerWidth * 0.3) + setSessionSidebarWidth((current) => clampWidth(current || width)) + measureDrawerHost() + } + + handleResize() + window.addEventListener("resize", handleResize) + onCleanup(() => window.removeEventListener("resize", handleResize)) + }) + + onMount(() => { + if (typeof window === "undefined") return + const handler = (event: Event) => { + const detail = (event as CustomEvent).detail + if (!detail || detail.instanceId !== props.instance.id) return + handleSidebarRequest(detail.action) + } + window.addEventListener(SESSION_SIDEBAR_EVENT, handler) + onCleanup(() => window.removeEventListener(SESSION_SIDEBAR_EVENT, handler)) + }) + + createEffect(() => { + if (typeof window === "undefined") return + window.localStorage.setItem(LEFT_DRAWER_STORAGE_KEY, sessionSidebarWidth().toString()) + }) + + createEffect(() => { + if (typeof window === "undefined") return + window.localStorage.setItem(RIGHT_DRAWER_STORAGE_KEY, rightDrawerWidth().toString()) + }) + + createEffect(() => { + props.tabBarOffset + requestAnimationFrame(() => measureDrawerHost()) + }) + + const activeSessions = createMemo(() => { + const parentId = activeParentSessionId().get(props.instance.id) + if (!parentId) return new Map[number]>() + const sessionFamily = getSessionFamily(props.instance.id, parentId) + return new Map(sessionFamily.map((s) => [s.id, s])) + }) + + const activeSessionIdForInstance = createMemo(() => { + return activeSessionMap().get(props.instance.id) || null + }) + + const parentSessionIdForInstance = createMemo(() => { + return activeParentSessionId().get(props.instance.id) || null + }) + + const activeSessionForInstance = createMemo(() => { + const sessionId = activeSessionIdForInstance() + if (!sessionId || sessionId === "info") return null + return activeSessions().get(sessionId) ?? null + }) + + const activeSessionUsage = createMemo(() => { + const sessionId = activeSessionIdForInstance() + if (!sessionId) return null + const store = messageStore() + return store?.getSessionUsage(sessionId) ?? null + }) + + const activeSessionInfoDetails = createMemo(() => { + const sessionId = activeSessionIdForInstance() + if (!sessionId) return null + return getSessionInfo(props.instance.id, sessionId) ?? null + }) + + const tokenStats = createMemo(() => { + const usage = activeSessionUsage() + const info = activeSessionInfoDetails() + return { + used: usage?.actualUsageTokens ?? info?.actualUsageTokens ?? 0, + avail: info?.contextAvailableTokens ?? null, + } + }) + + const latestTodoSnapshot = createMemo(() => { + const sessionId = activeSessionIdForInstance() + if (!sessionId || sessionId === "info") return null + const store = messageStore() + if (!store) return null + const snapshot = store.state.latestTodos[sessionId] + return snapshot ?? null + }) + + const latestTodoState = createMemo(() => { + const snapshot = latestTodoSnapshot() + if (!snapshot) return null + const store = messageStore() + if (!store) return null + const message = store.getMessage(snapshot.messageId) + if (!message) return null + const partRecord = message.parts?.[snapshot.partId] + const part = partRecord?.data as { type?: string; tool?: string; state?: ToolState } + if (!part || part.type !== "tool" || part.tool !== "todowrite") return null + const state = part.state + if (!state || state.status !== "completed") return null + return state + }) + + const connectionStatus = () => sseManager.getStatus(props.instance.id) + const connectionStatusClass = () => { + const status = connectionStatus() + if (status === "connecting") return "connecting" + if (status === "connected") return "connected" + return "disconnected" + } + + const handleCommandPaletteClick = () => { + showCommandPalette(props.instance.id) + } + + const customCommands = createMemo(() => buildCustomCommandEntries(props.instance.id, getInstanceCommands(props.instance.id))) + + const instancePaletteCommands = createMemo(() => [...props.paletteCommands(), ...customCommands()]) + const paletteOpen = createMemo(() => isCommandPaletteOpen(props.instance.id)) + + const keyboardShortcuts = createMemo(() => + [keyboardRegistry.get("session-prev"), keyboardRegistry.get("session-next")].filter( + (shortcut): shortcut is KeyboardShortcut => Boolean(shortcut), + ), + ) + + interface PendingSidebarAction { + action: SessionSidebarRequestAction + id: number + } + + let sidebarActionId = 0 + const [pendingSidebarAction, setPendingSidebarAction] = createSignal(null) + + const triggerKeyboardEvent = (target: HTMLElement, options: { key: string; code: string; keyCode: number }) => { + target.dispatchEvent( + new KeyboardEvent("keydown", { + key: options.key, + code: options.code, + keyCode: options.keyCode, + which: options.keyCode, + bubbles: true, + cancelable: true, + }), + ) + } + + const focusAgentSelectorControl = () => { + const agentTrigger = leftDrawerContentEl()?.querySelector("[data-agent-selector]") as HTMLElement | null + if (!agentTrigger) return false + agentTrigger.focus() + setTimeout(() => triggerKeyboardEvent(agentTrigger, { key: "Enter", code: "Enter", keyCode: 13 }), 10) + return true + } + + const focusModelSelectorControl = () => { + const input = leftDrawerContentEl()?.querySelector("[data-model-selector]") + if (!input) return false + input.focus() + setTimeout(() => triggerKeyboardEvent(input, { key: "ArrowDown", code: "ArrowDown", keyCode: 40 }), 10) + return true + } + + createEffect(() => { + const pending = pendingSidebarAction() + if (!pending) return + const action = pending.action + const contentReady = Boolean(leftDrawerContentEl()) + if (!contentReady) { + return + } + if (action === "show-session-list") { + setPendingSidebarAction(null) + return + } + const handled = action === "focus-agent-selector" ? focusAgentSelectorControl() : focusModelSelectorControl() + if (handled) { + setPendingSidebarAction(null) + } + }) + + const handleSidebarRequest = (action: SessionSidebarRequestAction) => { + setPendingSidebarAction({ action, id: sidebarActionId++ }) + if (!leftPinned() && !leftOpen()) { + setLeftOpen(true) + measureDrawerHost() + } + } + + const closeFloatingDrawersIfAny = () => { + let handled = false + if (!leftPinned() && leftOpen()) { + setLeftOpen(false) + blurIfInside(leftDrawerContentEl()) + focusTarget(leftToggleButtonEl()) + handled = true + } + if (!rightPinned() && rightOpen()) { + setRightOpen(false) + blurIfInside(rightDrawerContentEl()) + focusTarget(rightToggleButtonEl()) + handled = true + } + return handled + } + + onMount(() => { + if (typeof window === "undefined") return + const handleEscape = (event: KeyboardEvent) => { + if (event.key !== "Escape") return + if (!closeFloatingDrawersIfAny()) return + event.preventDefault() + event.stopPropagation() + } + window.addEventListener("keydown", handleEscape, true) + onCleanup(() => window.removeEventListener("keydown", handleEscape, true)) + }) + + const handleSessionSelect = (sessionId: string) => { + setActiveSession(props.instance.id, sessionId) + } + + + const evictSession = (sessionId: string) => { + if (!sessionId) return + log.info("Evicting cached session", { instanceId: props.instance.id, sessionId }) + const store = messageStoreBus.getInstance(props.instance.id) + store?.clearSession(sessionId) + clearSessionRenderCache(props.instance.id, sessionId) + } + + const scheduleEvictions = (ids: string[]) => { + if (!ids.length) return + setPendingEvictions((current) => { + const existing = new Set(current) + const next = [...current] + ids.forEach((id) => { + if (!existing.has(id)) { + next.push(id) + existing.add(id) + } + }) + return next + }) + } + + createEffect(() => { + const pending = pendingEvictions() + if (!pending.length) return + const cached = new Set(cachedSessionIds()) + const remaining: string[] = [] + pending.forEach((id) => { + if (cached.has(id)) { + remaining.push(id) + } else { + evictSession(id) + } + }) + if (remaining.length !== pending.length) { + setPendingEvictions(remaining) + } + }) + + createEffect(() => { + const sessionsMap = activeSessions() + const parentId = parentSessionIdForInstance() + const activeId = activeSessionIdForInstance() + setCachedSessionIds((current) => { + const next: string[] = [] + const append = (id: string | null) => { + if (!id || id === "info") return + if (!sessionsMap.has(id)) return + if (next.includes(id)) return + next.push(id) + } + + append(parentId) + append(activeId) + + const limit = parentId ? SESSION_CACHE_LIMIT + 1 : SESSION_CACHE_LIMIT + const trimmed = next.length > limit ? next.slice(0, limit) : next + const trimmedSet = new Set(trimmed) + const removed = current.filter((id) => !trimmedSet.has(id)) + if (removed.length) { + scheduleEvictions(removed) + } + return trimmed + }) + }) + + const showEmbeddedSidebarToggle = createMemo(() => !leftPinned() && !leftOpen()) + + const drawerContainer = () => { + const host = drawerHost() + if (host) return host + if (typeof document !== "undefined") { + return document.body + } + return undefined + } + + const fallbackDrawerTop = () => APP_BAR_HEIGHT + props.tabBarOffset + const floatingTop = () => { + const measured = floatingDrawerTop() + if (measured > 0) return measured + return fallbackDrawerTop() + } + const floatingTopPx = () => `${floatingTop()}px` + const floatingHeight = () => { + const measured = floatingDrawerHeight() + if (measured > 0) return `${measured}px` + return `calc(100% - ${floatingTop()}px)` + } + + const scheduleDrawerMeasure = () => { + if (typeof window === "undefined") { + measureDrawerHost() + return + } + requestAnimationFrame(() => measureDrawerHost()) + } + + const applyDrawerWidth = (side: "left" | "right", width: number) => { + if (side === "left") { + setSessionSidebarWidth(width) + } else { + setRightDrawerWidth(width) + } + scheduleDrawerMeasure() + } + + const handleDrawerPointerMove = (clientX: number) => { + const side = activeResizeSide() + if (!side) return + const startWidth = resizeStartWidth() + const clamp = side === "left" ? clampWidth : clampRightWidth + const delta = side === "left" ? clientX - resizeStartX() : resizeStartX() - clientX + const nextWidth = clamp(startWidth + delta) + applyDrawerWidth(side, nextWidth) + } + + function stopDrawerResize() { + setActiveResizeSide(null) + document.removeEventListener("mousemove", drawerMouseMove) + document.removeEventListener("mouseup", drawerMouseUp) + document.removeEventListener("touchmove", drawerTouchMove) + document.removeEventListener("touchend", drawerTouchEnd) + } + + function drawerMouseMove(event: MouseEvent) { + event.preventDefault() + handleDrawerPointerMove(event.clientX) + } + + function drawerMouseUp() { + stopDrawerResize() + } + + function drawerTouchMove(event: TouchEvent) { + const touch = event.touches[0] + if (!touch) return + event.preventDefault() + handleDrawerPointerMove(touch.clientX) + } + + function drawerTouchEnd() { + stopDrawerResize() + } + + const startDrawerResize = (side: "left" | "right", clientX: number) => { + setActiveResizeSide(side) + setResizeStartX(clientX) + setResizeStartWidth(side === "left" ? sessionSidebarWidth() : rightDrawerWidth()) + document.addEventListener("mousemove", drawerMouseMove) + document.addEventListener("mouseup", drawerMouseUp) + document.addEventListener("touchmove", drawerTouchMove, { passive: false }) + document.addEventListener("touchend", drawerTouchEnd) + } + + const handleDrawerResizeMouseDown = (side: "left" | "right") => (event: MouseEvent) => { + event.preventDefault() + startDrawerResize(side, event.clientX) + } + + const handleDrawerResizeTouchStart = (side: "left" | "right") => (event: TouchEvent) => { + const touch = event.touches[0] + if (!touch) return + event.preventDefault() + startDrawerResize(side, touch.clientX) + } + + onCleanup(() => { + stopDrawerResize() + }) + + type DrawerViewState = "pinned" | "floating-open" | "floating-closed" + + + const leftDrawerState = createMemo(() => { + if (leftPinned()) return "pinned" + return leftOpen() ? "floating-open" : "floating-closed" + }) + + const rightDrawerState = createMemo(() => { + if (rightPinned()) return "pinned" + return rightOpen() ? "floating-open" : "floating-closed" + }) + + const leftAppBarButtonLabel = () => { + const state = leftDrawerState() + if (state === "pinned") return "Left drawer pinned" + if (state === "floating-closed") return "Open left drawer" + return "Close left drawer" + } + + const rightAppBarButtonLabel = () => { + const state = rightDrawerState() + if (state === "pinned") return "Right drawer pinned" + if (state === "floating-closed") return "Open right drawer" + return "Close right drawer" + } + + const leftAppBarButtonIcon = () => { + const state = leftDrawerState() + if (state === "floating-closed") return + return + } + + const rightAppBarButtonIcon = () => { + const state = rightDrawerState() + if (state === "floating-closed") return + return + } + + + + + const pinLeftDrawer = () => { + blurIfInside(leftDrawerContentEl()) + batch(() => { + setLeftPinned(true) + setLeftOpen(true) + }) + persistPinIfSupported("left", true) + measureDrawerHost() + } + + const unpinLeftDrawer = () => { + blurIfInside(leftDrawerContentEl()) + batch(() => { + setLeftPinned(false) + setLeftOpen(true) + }) + persistPinIfSupported("left", false) + measureDrawerHost() + } + + const pinRightDrawer = () => { + blurIfInside(rightDrawerContentEl()) + batch(() => { + setRightPinned(true) + setRightOpen(true) + }) + persistPinIfSupported("right", true) + measureDrawerHost() + } + + const unpinRightDrawer = () => { + blurIfInside(rightDrawerContentEl()) + batch(() => { + setRightPinned(false) + setRightOpen(true) + }) + persistPinIfSupported("right", false) + measureDrawerHost() + } + + const handleLeftAppBarButtonClick = () => { + const state = leftDrawerState() + if (state === "pinned") return + if (state === "floating-closed") { + setLeftOpen(true) + measureDrawerHost() + return + } + blurIfInside(leftDrawerContentEl()) + setLeftOpen(false) + focusTarget(leftToggleButtonEl()) + measureDrawerHost() + } + + const handleRightAppBarButtonClick = () => { + const state = rightDrawerState() + if (state === "pinned") return + if (state === "floating-closed") { + setRightOpen(true) + measureDrawerHost() + return + } + blurIfInside(rightDrawerContentEl()) + setRightOpen(false) + focusTarget(rightToggleButtonEl()) + measureDrawerHost() + } + + + const focusTarget = (element: HTMLElement | null) => { + if (!element) return + requestAnimationFrame(() => { + element.focus() + }) + } + + const blurIfInside = (element: HTMLElement | null) => { + if (typeof document === "undefined" || !element) return + const active = document.activeElement as HTMLElement | null + if (active && element.contains(active)) { + active.blur() + } + } + + const closeLeftDrawer = () => { + if (leftDrawerState() === "pinned") return + blurIfInside(leftDrawerContentEl()) + setLeftOpen(false) + focusTarget(leftToggleButtonEl()) + } + const closeRightDrawer = () => { + if (rightDrawerState() === "pinned") return + blurIfInside(rightDrawerContentEl()) + setRightOpen(false) + focusTarget(rightToggleButtonEl()) + } + + const formattedUsedTokens = () => formatTokenTotal(tokenStats().used) + + + const formattedAvailableTokens = () => { + const avail = tokenStats().avail + if (typeof avail === "number") { + return formatTokenTotal(avail) + } + return "--" + } + + const LeftDrawerContent = () => ( +
+
+
+ Sessions +
+ + + +
+
+
+ + (leftPinned() ? unpinLeftDrawer() : pinLeftDrawer())} + > + {leftPinned() ? : } + + +
+ +
+ +
+ { + const result = props.onCloseSession(id) + if (result instanceof Promise) { + void result.catch((error) => log.error("Failed to close session:", error)) + } + }} + onNew={() => { + const result = props.onNewSession() + if (result instanceof Promise) { + void result.catch((error) => log.error("Failed to create session:", error)) + } + }} + showHeader={false} + showFooter={false} + /> + + + + {(activeSession) => ( + <> + +
+ props.handleSidebarAgentChange(activeSession().id, agent)} + /> + + + + props.handleSidebarModelChange(activeSession().id, model)} + /> +
+ + )} +
+
+
+ ) + + const RightDrawerContent = () => { + const renderPlanSectionContent = () => { + const sessionId = activeSessionIdForInstance() + if (!sessionId || sessionId === "info") { + return

Select a session to view plan.

+ } + const todoState = latestTodoState() + if (!todoState) { + return

Nothing planned yet.

+ } + return + } + + const sections = [ + { + id: "lsp", + label: "LSP Servers", + render: () => ( + + ), + }, + { + id: "mcp", + label: "MCP Servers", + render: () => ( + + ), + }, + { + id: "plan", + label: "Plan", + render: renderPlanSectionContent, + }, + ] + + createEffect(() => { + const currentExpanded = new Set(rightPanelExpandedItems()) + if (sections.every((section) => currentExpanded.has(section.id))) return + setRightPanelExpandedItems(sections.map((section) => section.id)) + }) + + const handleAccordionChange = (values: string[]) => { + setRightPanelExpandedItems(values) + } + + const isSectionExpanded = (id: string) => rightPanelExpandedItems().includes(id) + + return ( +
+
+ + Status Panel + +
+ + (rightPinned() ? unpinRightDrawer() : pinRightDrawer())} + > + {rightPinned() ? : } + + +
+
+
+ + + {(section) => ( + + + + {section.label} + + + + + {section.render()} + + + )} + + +
+
+ ) + } + + const renderLeftPanel = () => { + if (leftPinned()) { + return ( + + + + ) +} + +export default LogsView diff --git a/packages/ui/src/components/markdown.tsx b/packages/ui/src/components/markdown.tsx new file mode 100644 index 0000000..5e898dc --- /dev/null +++ b/packages/ui/src/components/markdown.tsx @@ -0,0 +1,165 @@ +import { createEffect, createSignal, onMount, onCleanup } from "solid-js" +import { renderMarkdown, onLanguagesLoaded, initMarkdown, decodeHtmlEntities } from "../lib/markdown" +import type { TextPart, RenderCache } from "../types/message" +import { getLogger } from "../lib/logger" +const log = getLogger("session") + +const markdownRenderCache = new Map() + +function makeMarkdownCacheKey(partId: string, themeKey: string, highlightEnabled: boolean) { + return `${partId}:${themeKey}:${highlightEnabled ? 1 : 0}` +} + +interface MarkdownProps { + part: TextPart + isDark?: boolean + size?: "base" | "sm" | "tight" + disableHighlight?: boolean + onRendered?: () => void +} + +export function Markdown(props: MarkdownProps) { + const [html, setHtml] = createSignal("") + let containerRef: HTMLDivElement | undefined + let latestRequestedText = "" + + const notifyRendered = () => { + Promise.resolve().then(() => props.onRendered?.()) + } + + createEffect(async () => { + const part = props.part + const rawText = typeof part.text === "string" ? part.text : "" + const text = decodeHtmlEntities(rawText) + const dark = Boolean(props.isDark) + const themeKey = dark ? "dark" : "light" + const highlightEnabled = !props.disableHighlight + const partId = typeof part.id === "string" && part.id.length > 0 ? part.id : "__anonymous__" + const cacheKey = makeMarkdownCacheKey(partId, themeKey, highlightEnabled) + + latestRequestedText = text + + const localCache = part.renderCache + if (localCache && localCache.text === text && localCache.theme === themeKey) { + setHtml(localCache.html) + notifyRendered() + return + } + + const globalCache = markdownRenderCache.get(cacheKey) + if (globalCache && globalCache.text === text) { + setHtml(globalCache.html) + part.renderCache = globalCache + notifyRendered() + return + } + + if (!highlightEnabled) { + part.renderCache = undefined + + try { + const rendered = await renderMarkdown(text, { suppressHighlight: true }) + + if (latestRequestedText === text) { + const cacheEntry: RenderCache = { text, html: rendered, theme: themeKey } + setHtml(rendered) + part.renderCache = cacheEntry + markdownRenderCache.set(cacheKey, cacheEntry) + notifyRendered() + } + } catch (error) { + log.error("Failed to render markdown:", error) + if (latestRequestedText === text) { + const cacheEntry: RenderCache = { text, html: text, theme: themeKey } + setHtml(text) + part.renderCache = cacheEntry + markdownRenderCache.set(cacheKey, cacheEntry) + notifyRendered() + } + } + return + } + + try { + const rendered = await renderMarkdown(text) + + if (latestRequestedText === text) { + const cacheEntry: RenderCache = { text, html: rendered, theme: themeKey } + setHtml(rendered) + part.renderCache = cacheEntry + markdownRenderCache.set(cacheKey, cacheEntry) + notifyRendered() + } + } catch (error) { + log.error("Failed to render markdown:", error) + if (latestRequestedText === text) { + const cacheEntry: RenderCache = { text, html: text, theme: themeKey } + setHtml(text) + part.renderCache = cacheEntry + markdownRenderCache.set(cacheKey, cacheEntry) + notifyRendered() + } + } + }) + + onMount(() => { + const handleClick = async (e: Event) => { + const target = e.target as HTMLElement + const copyButton = target.closest(".code-block-copy") as HTMLButtonElement + + if (copyButton) { + e.preventDefault() + const code = copyButton.getAttribute("data-code") + if (code) { + const decodedCode = decodeURIComponent(code) + await navigator.clipboard.writeText(decodedCode) + const copyText = copyButton.querySelector(".copy-text") + if (copyText) { + copyText.textContent = "Copied!" + setTimeout(() => { + copyText.textContent = "Copy" + }, 2000) + } + } + } + } + + containerRef?.addEventListener("click", handleClick) + + // Register listener for language loading completion + const cleanupLanguageListener = onLanguagesLoaded(async () => { + if (props.disableHighlight) { + return + } + + const part = props.part + const rawText = typeof part.text === "string" ? part.text : "" + const text = decodeHtmlEntities(rawText) + + if (latestRequestedText !== text) { + return + } + + try { + const rendered = await renderMarkdown(text) + if (latestRequestedText === text) { + setHtml(rendered) + const themeKey = Boolean(props.isDark) ? "dark" : "light" + part.renderCache = { text, html: rendered, theme: themeKey } + notifyRendered() + } + } catch (error) { + log.error("Failed to re-render markdown after language load:", error) + } + }) + + onCleanup(() => { + containerRef?.removeEventListener("click", handleClick) + cleanupLanguageListener() + }) + }) + + const proseClass = () => "markdown-body" + + return
+} diff --git a/packages/ui/src/components/message-block-list.tsx b/packages/ui/src/components/message-block-list.tsx new file mode 100644 index 0000000..3db083c --- /dev/null +++ b/packages/ui/src/components/message-block-list.tsx @@ -0,0 +1,64 @@ +import { Index, type Accessor } from "solid-js" +import VirtualItem from "./virtual-item" +import MessageBlock from "./message-block" +import type { InstanceMessageStore } from "../stores/message-v2/instance-store" + +export function getMessageAnchorId(messageId: string) { + return `message-anchor-${messageId}` +} + +const VIRTUAL_ITEM_MARGIN_PX = 800 + +interface MessageBlockListProps { + instanceId: string + sessionId: string + store: () => InstanceMessageStore + messageIds: () => string[] + lastAssistantIndex: () => number + showThinking: () => boolean + thinkingDefaultExpanded: () => boolean + showUsageMetrics: () => boolean + scrollContainer: Accessor + loading?: boolean + onRevert?: (messageId: string) => void + onFork?: (messageId?: string) => void + onContentRendered?: () => void + setBottomSentinel: (element: HTMLDivElement | null) => void + suspendMeasurements?: () => boolean +} + +export default function MessageBlockList(props: MessageBlockListProps) { + return ( + <> + + {(messageId, index) => ( + !props.loading} + suspendMeasurements={props.suspendMeasurements} + > + + + )} + +