From ea2aadf5a1c45d2ebf201528ffad22655536225a Mon Sep 17 00:00:00 2001 From: techartdev Date: Sat, 26 Sep 2026 23:35:38 +0300 Subject: [PATCH] Give Database app a package icon --- app/dist/app.shellcanvas.json | 2 +- app/icon.svg | 13 +++++++++++++ app/shellcanvas.json | 1 + shellcanvas.repo.json | 8 ++++---- 4 files changed, 19 insertions(+), 5 deletions(-) create mode 100644 app/icon.svg diff --git a/app/dist/app.shellcanvas.json b/app/dist/app.shellcanvas.json index c63dbb4..a87da4b 100644 --- a/app/dist/app.shellcanvas.json +++ b/app/dist/app.shellcanvas.json @@ -1 +1 @@ -{"format":1,"kind":"app","id":"dev.shellcanvas.database","version":"0.1.1","title":"ShellCanvas Database","description":"Browse database schemas, run SQL, inspect typed results and export CSV.","permissions":["services.dev.shellcanvas.database","host.tcp"],"script":"\"use strict\";\n(() => {\n // node_modules/@techartdev/shellcanvas-app-sdk/dist/rpc.js\n var RpcError = class extends Error {\n code;\n constructor(code, message2) {\n super(message2);\n this.code = code;\n this.name = \"RpcError\";\n }\n };\n var codes = [\n \"invalid\",\n \"closed\",\n \"aborted\",\n \"denied\",\n \"unavailable\",\n \"busy\",\n \"failed\"\n ];\n var maxMessage = 4 * 1024 * 1024;\n var maxPending = 64;\n var methodName = /^[a-z][a-z0-9-]*(?:\\.[a-zA-Z][a-zA-Z0-9-]*)+$/;\n function isJsonValue(value) {\n const pending = [{ value, depth: 0 }];\n while (pending.length) {\n const item = pending.pop();\n if (item.depth > 64)\n return false;\n if (item.value === null || typeof item.value === \"string\" || typeof item.value === \"boolean\")\n continue;\n if (typeof item.value === \"number\" && Number.isFinite(item.value))\n continue;\n if (typeof item.value !== \"object\" || item.value === null)\n return false;\n if (!Array.isArray(item.value) && Object.getPrototypeOf(item.value) !== Object.prototype)\n return false;\n for (const child of Object.values(item.value))\n pending.push({ value: child, depth: item.depth + 1 });\n }\n return true;\n }\n function encode(value) {\n if (!isJsonValue(value))\n throw new RpcError(\"invalid\", \"Only finite JSON values with at most 64 nesting levels are supported.\");\n const text = JSON.stringify(value);\n if (text.length > maxMessage)\n throw new RpcError(\"invalid\", \"RPC message exceeds 4 Mi UTF-16 units; stream bulk data separately.\");\n return text;\n }\n function decode(raw) {\n if (typeof raw !== \"string\" || raw.length > maxMessage)\n throw new RpcError(\"invalid\", \"Invalid RPC message.\");\n const value = JSON.parse(raw);\n if (!isJsonValue(value) || !value || typeof value !== \"object\" || Array.isArray(value) || value.v !== 1)\n throw new RpcError(\"invalid\", \"Unsupported RPC envelope.\");\n if (value.type === \"close\")\n return { v: 1, type: \"close\" };\n const id = value.id;\n if (typeof id !== \"number\" || !Number.isSafeInteger(id) || id < 1)\n throw new RpcError(\"invalid\", \"Invalid request identity.\");\n if (value.type === \"request\" && typeof value.method === \"string\" && methodName.test(value.method) && Object.hasOwn(value, \"params\"))\n return {\n v: 1,\n type: \"request\",\n id,\n method: value.method,\n params: value.params\n };\n if (value.type === \"cancel\")\n return { v: 1, type: \"cancel\", id };\n if (value.type === \"result\" && Object.hasOwn(value, \"value\"))\n return { v: 1, type: \"result\", id, value: value.value };\n if (value.type === \"error\" && typeof value.code === \"string\" && codes.includes(value.code) && typeof value.message === \"string\")\n return {\n v: 1,\n type: \"error\",\n id,\n code: value.code,\n message: value.message\n };\n throw new RpcError(\"invalid\", \"Invalid RPC envelope.\");\n }\n var RpcPeer = class {\n transport;\n closed = false;\n serial = 0;\n received = 0;\n unsubscribe = () => {\n };\n pending = /* @__PURE__ */ new Map();\n running = /* @__PURE__ */ new Map();\n methods;\n grants;\n closeListeners = /* @__PURE__ */ new Set();\n constructor(transport, methods = /* @__PURE__ */ new Map(), grants = []) {\n this.transport = transport;\n this.methods = new Map([...methods].map(([name, method]) => {\n if (!methodName.test(name))\n throw new RpcError(\"invalid\", `Invalid method name: ${name}`);\n return [\n name,\n {\n grants: [...method.grants],\n invoke: method.invoke,\n available: method.available\n }\n ];\n }));\n this.grants = new Set(grants);\n this.unsubscribe = transport.subscribe((raw) => this.receive(raw), () => this.finish(new RpcError(\"closed\", \"Extension channel disconnected.\")));\n }\n get isClosed() {\n return this.closed;\n }\n onClose(listener) {\n if (this.closed) {\n listener();\n return () => {\n };\n }\n this.closeListeners.add(listener);\n return () => {\n this.closeListeners.delete(listener);\n };\n }\n call(method, params = null, signal) {\n if (this.closed)\n return Promise.reject(new RpcError(\"closed\", \"Extension instance has closed.\"));\n if (signal?.aborted)\n return Promise.reject(new RpcError(\"aborted\", \"Request canceled.\"));\n if (!methodName.test(method))\n return Promise.reject(new RpcError(\"invalid\", \"Invalid method name.\"));\n if (this.pending.size >= maxPending)\n return Promise.reject(new RpcError(\"busy\", \"Too many pending requests.\"));\n const id = ++this.serial;\n if (!Number.isSafeInteger(id)) {\n this.close();\n return Promise.reject(new RpcError(\"closed\", \"Request identities exhausted.\"));\n }\n return new Promise((resolve, reject) => {\n const abort = () => {\n const request = this.pending.get(id);\n if (!request)\n return;\n this.pending.delete(id);\n request.cleanup();\n reject(new RpcError(\"aborted\", \"Request canceled; an already dispatched write may have completed.\"));\n this.send({ v: 1, type: \"cancel\", id });\n };\n this.pending.set(id, {\n resolve,\n reject,\n cleanup: () => signal?.removeEventListener(\"abort\", abort)\n });\n signal?.addEventListener(\"abort\", abort, { once: true });\n try {\n this.transport.send(encode({ v: 1, type: \"request\", id, method, params }));\n } catch (error) {\n this.pending.get(id)?.cleanup();\n this.pending.delete(id);\n reject(error instanceof RpcError ? error : new RpcError(\"closed\", \"Unable to send request.\"));\n if (!(error instanceof RpcError))\n this.close();\n }\n });\n }\n /** Revocation retires the whole instance, so no outstanding operation keeps an old grant. */\n close() {\n if (this.closed)\n return;\n this.send({ v: 1, type: \"close\" });\n this.finish(new RpcError(\"closed\", \"Extension instance has closed.\"));\n }\n finish(reason) {\n if (this.closed)\n return;\n this.closed = true;\n this.unsubscribe();\n for (const request of this.pending.values()) {\n request.cleanup();\n request.reject(reason);\n }\n this.pending.clear();\n for (const controller of this.running.values())\n controller.abort();\n this.running.clear();\n for (const listener of this.closeListeners) {\n try {\n listener();\n } catch {\n }\n }\n this.closeListeners.clear();\n this.transport.close();\n }\n send(value) {\n if (this.closed)\n return;\n try {\n this.transport.send(encode(value));\n } catch {\n this.finish(new RpcError(\"closed\", \"Extension channel failed.\"));\n }\n }\n receive(raw) {\n if (this.closed)\n return;\n let value;\n try {\n value = decode(raw);\n } catch {\n this.close();\n return;\n }\n if (value.type === \"close\") {\n this.finish(new RpcError(\"closed\", \"Extension instance has closed.\"));\n return;\n }\n if (value.type === \"cancel\") {\n this.running.get(value.id)?.abort();\n return;\n }\n if (value.type === \"result\" || value.type === \"error\") {\n const request = this.pending.get(value.id);\n if (!request)\n return;\n this.pending.delete(value.id);\n request.cleanup();\n if (value.type === \"result\")\n request.resolve(value.value);\n else\n request.reject(new RpcError(value.code, value.message));\n return;\n }\n if (value.id <= this.received) {\n this.close();\n return;\n }\n this.received = value.id;\n void this.dispatch(value);\n }\n async dispatch(request) {\n const fail = (code, message2) => this.send({ v: 1, type: \"error\", id: request.id, code, message: message2 });\n const method = this.methods.get(request.method);\n if (!method) {\n fail(\"unavailable\", \"Unknown service method.\");\n return;\n }\n if (method.grants.some((grant) => !this.grants.has(grant))) {\n fail(\"denied\", \"The extension does not hold the required permission.\");\n return;\n }\n if (this.running.size >= maxPending) {\n fail(\"busy\", \"Too many active requests.\");\n return;\n }\n const controller = new AbortController();\n this.running.set(request.id, controller);\n try {\n if (method.available && !method.available())\n throw new RpcError(\"unavailable\", \"This service is currently unavailable.\");\n const result = await method.invoke(request.params, controller.signal);\n if (!this.closed && !controller.signal.aborted) {\n encode({ v: 1, type: \"result\", id: request.id, value: result });\n this.send({ v: 1, type: \"result\", id: request.id, value: result });\n }\n } catch (error) {\n if (!controller.signal.aborted && !this.closed)\n fail(error instanceof RpcError ? error.code : \"failed\", error instanceof RpcError ? error.message : \"Service operation failed.\");\n } finally {\n this.running.delete(request.id);\n }\n }\n };\n function messagePortTransport(port) {\n return {\n send: (message2) => port.postMessage(message2),\n subscribe(receive, disconnected) {\n const message2 = (event) => receive(event.data);\n port.addEventListener(\"message\", message2);\n port.addEventListener(\"messageerror\", disconnected);\n port.start();\n return () => {\n port.removeEventListener(\"message\", message2);\n port.removeEventListener(\"messageerror\", disconnected);\n };\n },\n close: () => port.close()\n };\n }\n\n // node_modules/@techartdev/shellcanvas-app-sdk/dist/network-client.js\n function appNetworkClient(peer) {\n return {\n profile: async (slot, signal) => await peer.call(\"system.network.profile\", { slot }, signal),\n configure: async (options, signal) => await peer.call(\"system.network.configure\", {\n slot: options.slot,\n suggestedEndpoint: options.suggestedEndpoint ?? \"\"\n }, signal),\n forget: async (slot, signal) => {\n await peer.call(\"system.network.forget\", { slot }, signal);\n },\n async postJSON(options, signal) {\n const id = crypto.randomUUID();\n let closed2 = false, reading = false;\n let closing;\n const close = () => {\n closed2 = true;\n signal?.removeEventListener(\"abort\", abort);\n return closing ??= peer.call(\"system.network.close\", { id }).then(() => {\n });\n };\n const abort = () => {\n void close().catch(() => {\n });\n };\n signal?.throwIfAborted();\n const starting = peer.call(\"system.network.start\", { id, ...options }, signal);\n signal?.addEventListener(\"abort\", abort, { once: true });\n try {\n const head = await starting;\n if (closed2 || signal?.aborted)\n throw new RpcError(\"aborted\", \"HTTP request canceled.\");\n return {\n ...head,\n close,\n async read(control) {\n if (closed2)\n throw new RpcError(\"closed\", \"HTTP response closed.\");\n if (reading)\n throw new RpcError(\"busy\", \"Wait for the current response read.\");\n reading = true;\n control?.addEventListener(\"abort\", abort, { once: true });\n try {\n control?.throwIfAborted();\n const bytes = await peer.call(\"system.network.read\", { id }, control);\n if (bytes === null) {\n await close();\n return null;\n }\n return Uint8Array.from(bytes);\n } catch (error) {\n abort();\n throw error;\n } finally {\n reading = false;\n control?.removeEventListener(\"abort\", abort);\n }\n }\n };\n } catch (error) {\n abort();\n throw error;\n }\n }\n };\n }\n\n // node_modules/@techartdev/shellcanvas-app-sdk/dist/file-client.js\n function appFileClient(peer) {\n return Object.freeze({\n makeDirectory: async ({ binding, parent, name }, signal) => await peer.call(\"system.files.makeDirectory\", { binding, parent, name }, signal),\n renameEntry: async ({ binding, path, revision }, name, signal) => await peer.call(\"system.files.renameEntry\", { binding, path, revision, name }, signal),\n moveEntry: async ({ binding, path, revision }, parent, signal) => await peer.call(\"system.files.moveEntry\", {\n binding,\n path,\n revision,\n parent: parent.path,\n parentBinding: parent.binding\n }, signal),\n removeEntry: async ({ binding, path, revision }, signal) => {\n await peer.call(\"system.files.removeEntry\", { binding, path, revision }, signal);\n },\n async *list({ binding, path }, signal) {\n const id = crypto.randomUUID();\n let closing;\n const close = () => closing ??= peer.call(\"system.files.listClose\", { id }).catch(() => {\n });\n const cancel = () => {\n void close();\n };\n signal?.addEventListener(\"abort\", cancel, { once: true });\n try {\n let result = await peer.call(\"system.files.listStart\", { id, binding, path: path ?? null }, signal);\n while (true) {\n yield result.directory;\n if (result.done)\n break;\n result = await peer.call(\"system.files.listNext\", { id }, signal);\n }\n } finally {\n signal?.removeEventListener(\"abort\", cancel);\n await close();\n }\n },\n readText: async ({ binding, path }, signal) => await peer.call(\"system.files.readText\", { binding, path }, signal),\n saveText: async ({ binding, path, revision }, text, signal) => await peer.call(\"system.files.saveText\", { binding, path, revision, text }, signal),\n createText: async ({ binding, parent, name, text }, signal) => await peer.call(\"system.files.createText\", { binding, parent, name, text }, signal)\n });\n }\n\n // node_modules/@techartdev/shellcanvas-app-sdk/dist/console-client.js\n function appConsoleClient(peer) {\n return {\n async open(options, signal) {\n const id = crypto.randomUUID();\n let closed2 = false;\n let closing;\n let reading = false;\n let writing = false;\n let stopPeer = () => {\n };\n const close = () => {\n if (closing)\n return closing;\n closed2 = true;\n stopPeer();\n signal?.removeEventListener(\"abort\", abort);\n closing = peer.call(\"system.console.close\", { id }).then(() => {\n });\n return closing;\n };\n const abort = () => {\n void close().catch(() => {\n });\n };\n stopPeer = peer.onClose(() => {\n closed2 = true;\n signal?.removeEventListener(\"abort\", abort);\n closing ??= Promise.resolve();\n });\n if (signal?.aborted) {\n stopPeer();\n throw new RpcError(\"aborted\", \"Console opening canceled.\");\n }\n const opening = peer.call(\"system.console.open\", {\n id,\n binding: options.binding,\n cols: options.cols ?? 80,\n rows: options.rows ?? 24\n }, signal);\n signal?.addEventListener(\"abort\", abort, { once: true });\n try {\n const result = await opening;\n if (closed2 || signal?.aborted)\n throw new RpcError(\"aborted\", \"Console opening canceled.\");\n const call2 = async (method, params, control) => {\n if (closed2)\n throw new RpcError(\"closed\", \"Console is closed.\");\n const onAbort = () => {\n void close().catch(() => {\n });\n };\n control?.addEventListener(\"abort\", onAbort, { once: true });\n try {\n if (control?.aborted) {\n onAbort();\n throw new RpcError(\"aborted\", \"Console operation canceled; the console was closed.\");\n }\n return await peer.call(method, params, control);\n } finally {\n control?.removeEventListener(\"abort\", onAbort);\n }\n };\n return {\n binding: result.binding,\n resizable: result.resizable,\n close,\n async read(control) {\n if (reading)\n throw new RpcError(\"busy\", \"Wait for the current console read.\");\n reading = true;\n try {\n const value = await call2(\"system.console.read\", { id }, control);\n return value === null ? null : Uint8Array.from(value.bytes);\n } finally {\n reading = false;\n }\n },\n async write(data, control) {\n if (writing)\n throw new RpcError(\"busy\", \"Wait for the current console write.\");\n const bytes = typeof data === \"string\" ? new TextEncoder().encode(data) : new Uint8Array(data);\n writing = true;\n try {\n for (let offset = 0; offset < bytes.length || offset === 0; offset += 65536)\n await call2(\"system.console.write\", {\n id,\n bytes: Array.from(bytes.subarray(offset, offset + 65536))\n }, control);\n } finally {\n writing = false;\n }\n },\n async resize(cols, rows, control) {\n if (!result.resizable)\n throw new RpcError(\"unavailable\", \"This console has fixed dimensions.\");\n await call2(\"system.console.resize\", { id, cols, rows }, control);\n }\n };\n } catch (error) {\n void close().catch(() => {\n });\n throw error;\n }\n }\n };\n }\n\n // node_modules/@techartdev/shellcanvas-app-sdk/dist/transfer-client.js\n function appTransferClient(peer) {\n const handle = (item) => {\n let closed2 = false;\n let running;\n let closing;\n const call2 = (method, extra = {}, signal) => {\n if (closed2)\n return Promise.reject(new RpcError(\"closed\", \"Transfer handle is closed.\"));\n return peer.call(`system.transfers.${method}`, { id: item.id, ...extra }, signal);\n };\n const cancel = async () => {\n await call2(\"cancel\");\n };\n return {\n binding: item.binding,\n name: item.name,\n size: item.size,\n direction: item.direction,\n async run(signal) {\n const abort = () => {\n void cancel().catch(() => {\n });\n };\n signal?.addEventListener(\"abort\", abort, { once: true });\n try {\n if (signal?.aborted)\n await cancel();\n running ??= call2(\"run\");\n return await running;\n } finally {\n signal?.removeEventListener(\"abort\", abort);\n }\n },\n async status() {\n return await call2(\"status\", {\n after: null\n });\n },\n async *watch(signal) {\n let after = null;\n for (; ; ) {\n const snapshot = await call2(\"status\", { after }, signal);\n yield snapshot;\n if (snapshot.result)\n return;\n after = snapshot.revision;\n }\n },\n cancel,\n close() {\n if (closing)\n return closing;\n closing = call2(\"close\").then(() => {\n closed2 = true;\n }, (error) => {\n closing = void 0;\n throw error;\n });\n return closing;\n }\n };\n };\n async function prepare(method, options, signal) {\n const id = crypto.randomUUID();\n try {\n const items = await peer.call(`system.transfers.${method}`, { id, ...options }, signal);\n if (signal?.aborted)\n throw new RpcError(\"aborted\", \"Transfer preparation canceled.\");\n return items.map(handle);\n } catch (error) {\n void peer.call(\"system.transfers.cancelPreparation\", { id }).catch(() => {\n });\n throw error;\n }\n }\n return {\n pasteClipboard: async (destination2, signal) => {\n const snapshot = await peer.call(\"system.transfers.clipboardInspect\", { binding: destination2.binding }, signal);\n if (snapshot.kind === \"empty\")\n return [];\n return prepare(snapshot.sequence === null ? \"clipboardPaste\" : snapshot.kind === \"remote\" ? snapshot.intent === \"move\" ? \"clipboardMoveSnapshot\" : \"clipboardCopySnapshot\" : \"clipboardPasteSnapshot\", {\n binding: destination2.binding,\n parent: destination2.path,\n ...snapshot.sequence === null ? {} : { sequence: snapshot.sequence }\n }, signal);\n },\n upload: (destination2, options, signal) => prepare(options?.folder ? \"uploadFolder\" : \"upload\", { binding: destination2.binding, parent: destination2.path }, signal),\n async download(entry, signal) {\n return (await prepare(\"download\", {\n binding: entry.binding,\n path: entry.path,\n revision: entry.revision\n }, signal))[0] ?? null;\n },\n downloadMany: (entries, signal) => prepare(\"downloadMany\", {\n binding: entries[0]?.binding ?? \"\",\n entries: entries.map((entry) => ({\n binding: entry.binding,\n path: entry.path,\n revision: entry.revision\n }))\n }, signal),\n async copy(entry, destination2, signal) {\n return (await prepare(\"copy\", {\n binding: entry.binding,\n path: entry.path,\n revision: entry.revision,\n parent: destination2.path,\n parentBinding: destination2.binding\n }, signal))[0];\n }\n };\n }\n\n // node_modules/@techartdev/shellcanvas-app-sdk/dist/host-settings-client.js\n function appHostSettingsClient(peer) {\n return {\n read: async (location, signal) => await peer.call(\"system.hostSettings.read\", { binding: location.binding }, signal),\n apply: async (setting, value, signal) => await peer.call(\"system.hostSettings.apply\", {\n binding: setting.binding,\n id: setting.id,\n revision: setting.revision,\n value\n }, signal)\n };\n }\n\n // node_modules/@techartdev/shellcanvas-app-sdk/dist/events-client.js\n function appEventClient(peer) {\n const listeners = /* @__PURE__ */ new Set();\n const latest = /* @__PURE__ */ new Map();\n let cursor = null, controller;\n const error = (listener, failure) => {\n try {\n listener.error?.(failure);\n } catch {\n }\n };\n const deliver = (listener, batch) => {\n if (listeners.has(listener))\n try {\n void Promise.resolve(listener.receive(structuredClone(batch))).catch((failure) => error(listener, failure));\n } catch (failure) {\n error(listener, failure);\n }\n };\n const poll = async (active) => {\n try {\n while (listeners.size && controller === active) {\n const batch = await peer.call(\"system.events.next\", { after: cursor }, active.signal);\n if (controller !== active || active.signal.aborted)\n return;\n cursor = batch.cursor;\n if (batch.reset)\n latest.clear();\n for (const event of batch.events)\n latest.set(event.topic, event);\n for (const listener of [...listeners])\n deliver(listener, batch);\n }\n } catch (failure) {\n if (controller !== active || active.signal.aborted)\n return;\n const stopped = [...listeners];\n listeners.clear();\n controller = void 0;\n latest.clear();\n cursor = null;\n for (const listener of stopped)\n error(listener, failure);\n }\n };\n return Object.freeze({\n subscribe(receive, onError) {\n const listener = { receive, error: onError };\n listeners.add(listener);\n if (!controller) {\n controller = new AbortController();\n void poll(controller);\n } else if (cursor !== null) {\n const snapshot = {\n cursor,\n reset: true,\n events: [...latest.values()].sort((a, b) => a.sequence - b.sequence)\n };\n queueMicrotask(() => deliver(listener, snapshot));\n }\n return () => {\n listeners.delete(listener);\n if (!listeners.size) {\n const previous = controller;\n controller = void 0;\n previous?.abort();\n cursor = null;\n latest.clear();\n }\n };\n }\n });\n }\n\n // node_modules/@techartdev/shellcanvas-app-sdk/dist/clipboard-image.js\n var imageChunkSize = 32 * 1024;\n function imageByteLength(width, height) {\n if (typeof width !== \"number\" || typeof height !== \"number\" || !Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0 || width > 4294967295 || height > 4294967295 || !Number.isSafeInteger(width * height * 4))\n throw new RpcError(\"invalid\", \"Provide positive image dimensions with a representable RGBA length.\");\n return width * height * 4;\n }\n function imageBytes(value) {\n return Array.isArray(value) && value.length > 0 && value.length <= imageChunkSize && value.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255);\n }\n function appImageClipboardClient(peer) {\n const release = (id) => peer.call(\"system.clipboard.image.release\", { id }).catch(() => {\n });\n return {\n async readImage(signal) {\n const id = crypto.randomUUID();\n try {\n const meta = await peer.call(\"system.clipboard.image.readStart\", { id }, signal);\n const length = imageByteLength(meta?.width, meta?.height);\n const rgba = new Uint8Array(length);\n let offset = 0;\n while (offset < length) {\n const bytes = await peer.call(\"system.clipboard.image.readChunk\", { id, offset }, signal);\n if (!imageBytes(bytes) || offset + bytes.length > length)\n throw new RpcError(\"failed\", \"Invalid clipboard image stream.\");\n rgba.set(bytes, offset);\n offset += bytes.length;\n }\n return { width: meta.width, height: meta.height, rgba };\n } finally {\n await release(id);\n }\n },\n async writeImage(image, signal) {\n const length = imageByteLength(image?.width, image?.height);\n if (!(image.rgba instanceof Uint8Array) || image.rgba.length !== length)\n throw new RpcError(\"invalid\", \"Image dimensions must match the RGBA bytes.\");\n const rgba = image.rgba.slice(), width = image.width, height = image.height;\n const id = crypto.randomUUID();\n try {\n await peer.call(\"system.clipboard.image.writeStart\", { id, width, height }, signal);\n for (let offset = 0; offset < length; offset += imageChunkSize)\n await peer.call(\"system.clipboard.image.writeChunk\", {\n id,\n offset,\n bytes: Array.from(rgba.subarray(offset, offset + imageChunkSize))\n }, signal);\n await peer.call(\"system.clipboard.image.writeCommit\", { id }, signal);\n } finally {\n await release(id);\n }\n }\n };\n }\n\n // node_modules/@techartdev/shellcanvas-app-sdk/dist/clipboard-client.js\n var chunkSize = 64 * 1024;\n function appClipboardClient(peer) {\n const release = (id) => peer.call(\"system.clipboard.release\", { id }).catch(() => {\n });\n return Object.freeze({\n async cutFile(entry, signal) {\n if (!entry || typeof entry.binding !== \"string\" || !entry.binding || typeof entry.path !== \"string\" || !entry.path || typeof entry.revision !== \"string\" || !entry.revision)\n throw new RpcError(\"invalid\", \"Cut a current file entry from an accepted workspace binding.\");\n const { binding, path, revision } = entry;\n const id = crypto.randomUUID();\n try {\n await peer.call(\"system.clipboard.files.cut\", { id, binding, path, revision }, signal);\n } finally {\n await peer.call(\"system.clipboard.files.release\", { id }).catch(() => {\n });\n }\n },\n async copyFiles(entries, signal) {\n const snapshot = entries.map(({ binding, path, revision }) => ({\n binding,\n path,\n revision\n }));\n if (!snapshot.length || snapshot.some((entry) => entry.binding !== snapshot[0].binding || !entry.path || !entry.revision))\n throw new RpcError(\"invalid\", \"Copy current file entries from one workspace binding.\");\n const id = crypto.randomUUID();\n try {\n await peer.call(\"system.clipboard.files.start\", { id, binding: snapshot[0].binding }, signal);\n for (let offset = 0; offset < snapshot.length; ) {\n const chunk = [];\n let size = 0;\n while (offset + chunk.length < snapshot.length && chunk.length < 128) {\n const { path, revision } = snapshot[offset + chunk.length];\n const entry = { path, revision };\n const length = JSON.stringify(entry).length;\n if (chunk.length && size + length > 64 * 1024)\n break;\n chunk.push(entry);\n size += length;\n }\n await peer.call(\"system.clipboard.files.append\", {\n id,\n offset,\n entries: chunk\n }, signal);\n offset += chunk.length;\n }\n await peer.call(\"system.clipboard.files.commit\", { id }, signal);\n } finally {\n await peer.call(\"system.clipboard.files.release\", { id }).catch(() => {\n });\n }\n },\n pasteFiles: appTransferClient(peer).pasteClipboard,\n ...appImageClipboardClient(peer),\n async readText(signal) {\n const id = crypto.randomUUID();\n const chunks = [];\n try {\n const start2 = await peer.call(\"system.clipboard.readStart\", { id }, signal);\n let offset = 0;\n while (offset < start2.length) {\n const chunk = await peer.call(\"system.clipboard.readChunk\", { id: start2.id, offset }, signal);\n if (typeof chunk !== \"string\" || !chunk.length || offset + chunk.length > start2.length)\n throw new RpcError(\"failed\", \"Invalid clipboard text stream.\");\n chunks.push(chunk);\n offset += chunk.length;\n }\n return chunks.join(\"\");\n } finally {\n await release(id);\n }\n },\n async writeText(text, signal) {\n if (typeof text !== \"string\")\n throw new RpcError(\"invalid\", \"Clipboard text must be a string.\");\n const id = crypto.randomUUID();\n try {\n await peer.call(\"system.clipboard.writeStart\", { id, length: text.length }, signal);\n for (let offset = 0; offset < text.length; offset += chunkSize)\n await peer.call(\"system.clipboard.writeChunk\", { id, offset, text: text.slice(offset, offset + chunkSize) }, signal);\n await peer.call(\"system.clipboard.writeCommit\", { id }, signal);\n } finally {\n await release(id);\n }\n }\n });\n }\n\n // node_modules/@techartdev/shellcanvas-app-sdk/dist/client.js\n function connectToShellCanvas(timeoutMs = 1e4) {\n return new Promise((resolve, reject) => {\n const token = document.querySelector('meta[name=\"shellcanvas-instance\"]')?.content;\n if (!token) {\n reject(new Error(\"Missing ShellCanvas instance identity.\"));\n return;\n }\n if (window.parent === window) {\n reject(new Error(\"Launch this app inside ShellCanvas.\"));\n return;\n }\n const timer = setTimeout(() => {\n window.removeEventListener(\"message\", receive);\n reject(new Error(\"ShellCanvas did not establish an app channel.\"));\n }, timeoutMs);\n const receive = (event) => {\n if (event.source !== window.parent || event.data?.type !== \"shellcanvas:connect:v1\" || event.data?.token !== token || event.ports.length !== 1)\n return;\n clearTimeout(timer);\n window.removeEventListener(\"message\", receive);\n const peer = new RpcPeer(messagePortTransport(event.ports[0]));\n let lastFocus = document.activeElement;\n const rememberFocus = (event2) => {\n if (event2.target instanceof HTMLElement && event2.target !== document.body)\n lastFocus = event2.target;\n };\n document.addEventListener(\"focusin\", rememberFocus);\n const dispose = () => {\n document.removeEventListener(\"focusin\", rememberFocus);\n window.removeEventListener(\"pagehide\", dispose);\n peer.close();\n };\n const call2 = async (method, params, signal) => {\n const origin = document.activeElement === document.body ? lastFocus : document.activeElement;\n try {\n return await peer.call(method, JSON.parse(JSON.stringify(params)), signal);\n } finally {\n requestAnimationFrame(() => requestAnimationFrame(() => {\n if (!peer.isClosed && origin instanceof HTMLElement && origin.isConnected)\n origin.focus();\n }));\n }\n };\n const system = Object.freeze({\n apiVersion: 1,\n dialogs: Object.freeze({\n messageBox: async (options, control) => await call2(\"system.dialogs.messageBox\", options, control?.signal),\n openFile: async (options = {}, control) => await call2(\"system.dialogs.openFile\", options, control?.signal),\n saveFile: async (options = {}, control) => await call2(\"system.dialogs.saveFile\", options, control?.signal)\n }),\n files: Object.freeze({\n saveTextAs: async (options, control) => await call2(\"system.files.saveTextAs\", options, control?.signal)\n })\n });\n window.addEventListener(\"pagehide\", dispose, { once: true });\n const storage = (namespace) => Object.freeze({\n get: async (key, signal) => await peer.call(`system.${namespace}.get`, { key }, signal),\n put: async (key, value, expectedRevision, signal) => await peer.call(`system.${namespace}.put`, { key, value, expectedRevision }, signal),\n remove: async (key, expectedRevision, signal) => {\n await peer.call(`system.${namespace}.remove`, { key, expectedRevision }, signal);\n },\n list: async (options = {}, signal) => await peer.call(`system.${namespace}.list`, JSON.parse(JSON.stringify(options)), signal)\n });\n resolve({\n network: appNetworkClient(peer),\n system,\n files: appFileClient(peer),\n console: appConsoleClient(peer),\n transfers: appTransferClient(peer),\n hostSettings: appHostSettingsClient(peer),\n storage: storage(\"storage\"),\n settings: storage(\"settings\"),\n environment: Object.freeze({\n get: async (signal) => await peer.call(\"system.environment.get\", null, signal)\n }),\n services: Object.freeze({\n call: (method, params = null, signal) => peer.call(\"system.services.call\", { method, params }, signal),\n list: async (signal) => await peer.call(\"system.services.list\", null, signal)\n }),\n events: appEventClient(peer),\n clipboard: appClipboardClient(peer),\n window: Object.freeze({\n getState: async (signal) => await peer.call(\"system.window.getState\", null, signal),\n focus: async (signal) => {\n await peer.call(\"system.window.focus\", null, signal);\n requestAnimationFrame(() => requestAnimationFrame(() => {\n if (!peer.isClosed && lastFocus instanceof HTMLElement && lastFocus.isConnected)\n lastFocus.focus({ preventScroll: true });\n }));\n },\n minimize: async (signal) => {\n await peer.call(\"system.window.minimize\", null, signal);\n },\n maximize: async (signal) => {\n await peer.call(\"system.window.maximize\", null, signal);\n },\n restore: async (signal) => {\n await peer.call(\"system.window.restore\", null, signal);\n },\n requestClose: async (signal) => {\n await peer.call(\"system.window.requestClose\", null, signal);\n },\n setDocumentState: async (state) => {\n await peer.call(\"system.window.setDocumentState\", JSON.parse(JSON.stringify(state)));\n }\n }),\n call: (method, params, signal) => peer.call(method, params, signal),\n dispose\n });\n };\n window.addEventListener(\"message\", receive);\n window.parent.postMessage({ type: \"shellcanvas:ready:v1\", token }, \"*\");\n });\n }\n\n // paging.js\n function nextOffset(page) {\n return page.offset + page.rows.length;\n }\n\n // connection-state.js\n var prefix = \"dev.shellcanvas.database.\";\n var required = [\n \"capabilities\",\n \"schemas\",\n \"tables\",\n \"query\",\n \"page\",\n \"closeResult\",\n \"exportBegin\",\n \"exportStatus\",\n \"exportCancel\"\n ];\n function connectionState(environment, methods, ownConnection = false) {\n const database = methods.filter((method) => method.name.startsWith(prefix));\n if (database.some((method) => !method.granted))\n return {\n ready: false,\n title: \"Database permission needed\",\n detail: \"Review this app in App Manager and allow its Database service permission.\"\n };\n if (!ownConnection && environment.connection === \"review-required\")\n return {\n ready: false,\n title: \"Review the reconnected host\",\n detail: \"Use the reconnected host in the bar above this app, or connect a database directly.\"\n };\n if (!ownConnection && environment.connection === \"disconnected\")\n return {\n ready: false,\n title: \"Workspace disconnected\",\n detail: \"Reconnect the workspace, or create a database connection directly from this app.\"\n };\n if (!database.length)\n return {\n ready: false,\n title: \"Connect a database\",\n detail: \"Choose a database server or an existing SQLite file to browse tables and run queries.\"\n };\n if (!required.every(\n (name) => database.some(\n (method) => method.name === prefix + name && method.available && method.granted\n )\n ))\n return {\n ready: false,\n title: \"Database service unavailable\",\n detail: \"The selected workspace does not expose all database operations. Reconnect it, or connect a database directly.\"\n };\n return {\n ready: true,\n title: \"Connected\",\n detail: \"Database services are available.\"\n };\n }\n var colors = [\n \"base\",\n \"surface\",\n \"raised\",\n \"inset\",\n \"text\",\n \"muted\",\n \"accent\",\n \"onAccent\",\n \"border\",\n \"hover\",\n \"selection\",\n \"danger\",\n \"dangerSoft\",\n \"warning\",\n \"warningSoft\",\n \"success\"\n ];\n function applyAppearance(root2, appearance) {\n if (!appearance) return;\n root2.dataset.mode = appearance.mode === \"light\" ? \"light\" : \"dark\";\n root2.style.setProperty(\"color-scheme\", root2.dataset.mode);\n for (const key of colors) {\n const value = appearance.colors[key];\n if (typeof value === \"string\" && /^#[0-9a-f]{6}$/i.test(value))\n root2.style.setProperty(`--sc-${key}`, value);\n }\n }\n\n // main.ts\n var service = \"dev.shellcanvas.database\";\n var databaseIcon = '';\n var root = document.querySelector(\"#root\");\n root.innerHTML = `\n
\n
${databaseIcon}

Database

No database connected

Not connected
\n
${databaseIcon}

Connect a database

Choose a database server or an existing SQLite file to browse tables and run queries.

SQL ServerPostgreSQLMySQL / MariaDBSQLite

Database connections use your connected SSH host. No public database port is needed.

\n \n
Checking the app connection\\u2026Database workspace
\n
\n
${databaseIcon}

Connect a database

Connect to a database on your SSH host or its network. Enter its address and credentials.

For SQL Server named instances, enter the database server and its TCP port.

Passwords are not saved.
`;\n var $ = (id) => document.getElementById(id);\n var input = (id) => $(id);\n var button = (id) => $(id);\n var sql = $(\"sql\");\n var dialog = $(\"connect-dialog\");\n var status = $(\"status\");\n var client;\n var current = null;\n var queryController = null;\n var connectController = null;\n var provider = \"\";\n var generation = 0;\n var schemaOp = 0;\n var pageOp = 0;\n var bindOp = 0;\n var exportJob = null;\n var pageBusy = false;\n var ready = false;\n var exportBusy = false;\n var previousOffsets = [];\n var signature = null;\n var privateConnection = false;\n var companionAvailable = false;\n var destination = \"\";\n var sshAvailable = false;\n var sshHostName = \"\";\n var closed = false;\n var pageSize = 100;\n function message(error) {\n return error instanceof RpcError ? error.message : error instanceof Error ? error.message : String(error);\n }\n function report(error) {\n status.textContent = message(error);\n }\n function setControls() {\n button(\"run\").disabled = !ready || !!queryController || !!connectController || exportBusy;\n button(\"cancel\").disabled = !queryController;\n button(\"reload\").disabled = !ready || !!connectController;\n button(\"export\").disabled = !ready || !current?.columns.length || exportBusy || !!queryController;\n button(\"prev\").disabled = !ready || pageBusy || !previousOffsets.length;\n button(\"next\").disabled = !ready || pageBusy || !current?.hasMore;\n button(\"disconnect\").hidden = !privateConnection || !ready;\n button(\"disconnect\").disabled = !!connectController;\n button(\"connection-settings\").disabled = !companionAvailable || !!connectController || !!queryController || exportBusy;\n button(\"setup-connect\").disabled = !companionAvailable || !!connectController;\n }\n async function call(method, params = {}, signal) {\n return privateConnection ? await client.call(\n \"system.companion.call\",\n { method: `${service}.${method}`, params },\n signal\n ) : await client.services.call(\n `${service}.${method}`,\n params,\n signal\n );\n }\n function unavailable(title, detail) {\n ready = false;\n $(\"connection-state\").textContent = \"Not connected\";\n $(\"connection-state\").classList.remove(\"connected\");\n $(\"destination\").textContent = \"No database connected\";\n $(\"setup\").hidden = false;\n $(\"workspace\").hidden = true;\n $(\"setup-title\").textContent = title;\n $(\"setup-detail\").textContent = detail;\n button(\"connection-settings\").textContent = \"Connect database\";\n status.textContent = detail;\n setControls();\n }\n function clearResult(note = \"Run a query to see results.\", release = true) {\n const old = current;\n current = null;\n if (release && old)\n void call(\"closeResult\", {\n resultId: old.resultId,\n offset: 0,\n pageSize: 1\n }).catch(() => {\n });\n document.querySelector(\"thead\").replaceChildren();\n document.querySelector(\"tbody\").replaceChildren();\n $(\"summary\").textContent = \"Results\";\n $(\"page\").textContent = \"\";\n $(\"warnings\").hidden = true;\n $(\"empty\").hidden = false;\n $(\"empty\").textContent = note;\n previousOffsets = [];\n setControls();\n }\n function render(page) {\n current = page;\n const head = document.querySelector(\"thead\"), body = document.querySelector(\"tbody\");\n head.replaceChildren();\n body.replaceChildren();\n if (page.columns.length) {\n const tr = document.createElement(\"tr\");\n for (const column of page.columns) {\n const th = document.createElement(\"th\");\n th.textContent = column.name;\n th.title = column.databaseType;\n tr.append(th);\n }\n head.append(tr);\n }\n for (const row of page.rows) {\n const tr = document.createElement(\"tr\");\n for (const cell of row) {\n const td = document.createElement(\"td\");\n td.textContent = cell.kind === \"null\" ? \"NULL\" : cell.display;\n td.className = `cell-${cell.kind}`;\n tr.append(td);\n }\n body.append(tr);\n }\n $(\"empty\").hidden = page.rows.length > 0;\n $(\"empty\").textContent = page.columns.length ? \"The query returned no rows.\" : `Statement completed. ${page.affectedRows.toLocaleString()} rows affected.`;\n $(\"summary\").textContent = page.columns.length ? `${page.totalRows.toLocaleString()} rows${page.truncated ? \" \\xB7 limit reached\" : \"\"}` : `${page.affectedRows.toLocaleString()} rows affected`;\n $(\"page\").textContent = page.totalRows ? `${page.offset + 1}\\u2013${page.offset + page.rows.length} of ${page.totalRows.toLocaleString()}` : \"\";\n $(\"warnings\").textContent = page.warnings.join(\" \\xB7 \");\n $(\"warnings\").hidden = !page.warnings.length;\n setControls();\n }\n function quote(name) {\n return provider === \"mysql\" ? `\\`${name.replaceAll(\"`\", \"``\")}\\`` : provider === \"sqlserver\" ? `[${name.replaceAll(\"]\", \"]]\")}]` : `\"${name.replaceAll('\"', '\"\"')}\"`;\n }\n function tableSql(schema, table) {\n const full = provider === \"sqlite\" ? quote(table) : `${quote(schema)}.${quote(table)}`;\n return provider === \"sqlserver\" ? `SELECT TOP (200) * FROM ${full};` : `SELECT * FROM ${full} LIMIT 200;`;\n }\n async function loadSchemas() {\n const op = ++schemaOp, gen = generation, tree = $(\"tree\");\n tree.textContent = \"Loading schemas\\u2026\";\n try {\n const value = await call(\"schemas\");\n if (op !== schemaOp || gen !== generation) return;\n tree.replaceChildren();\n for (const schema of value.schemas) {\n const details = document.createElement(\"details\"), summary = document.createElement(\"summary\");\n summary.textContent = schema;\n details.append(summary);\n details.addEventListener(\"toggle\", () => {\n if (details.open && details.childElementCount === 1)\n void loadTables(details, schema, gen);\n });\n tree.append(details);\n if (value.schemas.length === 1) details.open = true;\n }\n if (!value.schemas.length)\n tree.textContent = \"No schemas visible to this account.\";\n } catch (error) {\n if (op === schemaOp && gen === generation) {\n tree.textContent = \"Could not load schemas. Use Refresh to retry.\";\n report(error);\n }\n }\n }\n async function loadTables(details, schema, gen) {\n const wait = document.createElement(\"p\");\n wait.textContent = \"Loading tables\\u2026\";\n details.append(wait);\n try {\n const value = await call(\"tables\", { schema });\n if (gen !== generation || !details.isConnected) return;\n wait.remove();\n for (const table of value.tables) {\n const item = document.createElement(\"button\");\n item.className = \"table\";\n item.textContent = table.name;\n item.title = `${table.kind} \\xB7 insert a query`;\n item.onclick = () => {\n sql.value = tableSql(schema, table.name);\n sql.focus();\n };\n details.append(item);\n }\n if (!value.tables.length) {\n wait.textContent = \"No tables\";\n details.append(wait);\n }\n } catch (error) {\n if (gen === generation && details.isConnected) {\n wait.textContent = \"Could not load tables. Refresh schemas to retry.\";\n report(error);\n }\n }\n }\n async function execute() {\n if (!ready || queryController || !sql.value.trim() || connectController || exportBusy)\n return;\n const gen = generation, active = new AbortController();\n queryController = active;\n clearResult(\"Running query\\u2026\");\n setControls();\n status.textContent = \"Running query\\u2026\";\n try {\n const page = await call(\n \"query\",\n { sql: sql.value, pageSize },\n active.signal\n );\n if (gen !== generation) return;\n render(page);\n status.textContent = \"Query completed.\";\n } catch (error) {\n if (gen === generation) {\n clearResult(\n active.signal.aborted ? \"Query canceled. Check the database before retrying statements that change data.\" : \"The query failed.\"\n );\n report(error);\n }\n } finally {\n if (queryController === active) queryController = null;\n setControls();\n }\n }\n async function move(offset, previous = false) {\n if (!current || pageBusy || !ready) return;\n const op = ++pageOp, gen = generation, original = current;\n pageBusy = true;\n setControls();\n try {\n const page = await call(\"page\", {\n resultId: original.resultId,\n offset,\n pageSize\n });\n if (op === pageOp && gen === generation && current === original) {\n if (previous) previousOffsets.pop();\n else previousOffsets.push(original.offset);\n render(page);\n }\n } catch (error) {\n if (op === pageOp && gen === generation) report(error);\n } finally {\n if (op === pageOp) pageBusy = false;\n setControls();\n }\n }\n async function cancelExport() {\n const job = exportJob;\n exportJob = null;\n if (job) await call(\"exportCancel\", { jobId: job }).catch(() => {\n });\n }\n async function exportCsv() {\n if (!current || exportBusy) return;\n const gen = generation;\n exportBusy = true;\n setControls();\n status.textContent = \"Choose a CSV destination\\u2026\";\n try {\n const start2 = await call(\"exportBegin\", {\n resultId: current.resultId,\n suggestedName: \"query-results.csv\",\n spreadsheetSafe: input(\"safe\").checked\n });\n if (gen !== generation) {\n await call(\"exportCancel\", { jobId: start2.jobId }).catch(() => {\n });\n return;\n }\n exportJob = start2.jobId;\n setControls();\n for (let polls = 0; polls < 180 && exportJob === start2.jobId; polls++) {\n await new Promise((resolve) => setTimeout(resolve, 350));\n if (gen !== generation) return;\n const job = await call(\"exportStatus\", { jobId: start2.jobId });\n if (gen !== generation) return;\n if (job.state === \"saved\" || job.state === \"canceled\") {\n status.textContent = job.state === \"saved\" ? `Saved ${job.name ?? \"CSV\"}.` : \"CSV save canceled.\";\n exportJob = null;\n return;\n }\n if (job.state === \"failed\")\n throw new Error(job.message ?? \"CSV export failed\");\n if (job.state !== \"choosing\" && job.state !== \"writing\")\n throw new Error(\"Unknown CSV export state\");\n }\n throw new Error(\"CSV save timed out\");\n } catch (error) {\n if (gen === generation) {\n await cancelExport();\n report(error);\n }\n } finally {\n exportBusy = false;\n setControls();\n }\n }\n async function refresh(force = false) {\n if (closed || !client) return;\n const op = ++bindOp;\n try {\n const env = await client.environment.get();\n if (op !== bindOp || closed) return;\n applyAppearance(document.documentElement, env.appearance);\n const systemMethods = await client.services.list();\n if (op !== bindOp || closed) return;\n companionAvailable = systemMethods.some(\n (method) => method.name === \"system.companion.connect\" && method.available && method.granted\n );\n const own = companionAvailable ? await client.call(\"system.companion.status\") : { connected: false, binding: null };\n if (op !== bindOp || closed || connectController) return;\n const methods = own.connected ? await client.call(\n \"system.companion.list\"\n ) : systemMethods;\n if (op !== bindOp || closed) return;\n const routes = companionAvailable ? await client.call(\"system.companion.routes\").catch(() => ({ ssh: false })) : { ssh: false };\n if (op !== bindOp || closed) return;\n sshAvailable = routes.ssh;\n sshHostName = env.host?.name ?? \"connected host\";\n updateRoute();\n const next = JSON.stringify([\n own.connected,\n own.binding,\n own.connected ? null : env.binding,\n own.connected ? null : env.connection,\n methods.filter((m) => m.name.startsWith(service + \".\")).map((m) => [m.name, m.available, m.granted, m.source])\n ]);\n if (!force && signature === next) return;\n generation++;\n schemaOp++;\n pageOp++;\n pageBusy = false;\n ready = false;\n queryController?.abort();\n queryController = null;\n await cancelExport();\n if (op !== bindOp || closed) return;\n privateConnection = own.connected;\n clearResult(\"Run a query to see results.\", false);\n const state = connectionState(env, methods, own.connected);\n if (!state.ready) {\n signature = next;\n unavailable(state.title, state.detail);\n return;\n }\n $(\"connection-state\").textContent = \"Checking database\\u2026\";\n const info = await call(\"capabilities\");\n if (op !== bindOp || closed) return;\n signature = next;\n provider = info.provider;\n ready = true;\n $(\"setup\").hidden = true;\n $(\"workspace\").hidden = false;\n $(\"connection-state\").textContent = \"Connected\";\n $(\"connection-state\").classList.add(\"connected\");\n $(\"destination\").textContent = privateConnection ? destination || `${provider} \\xB7 app connection` : `${provider} \\xB7 ${env.host?.name ?? \"workspace\"}`;\n button(\"connection-settings\").textContent = \"Connection\\u2026\";\n status.textContent = \"Ready. Choose a table or write a query.\";\n setControls();\n await loadSchemas();\n } catch (error) {\n if (op === bindOp && !closed) {\n signature = null;\n unavailable(\"Connection unavailable\", message(error));\n }\n }\n }\n function updateRoute() {\n const remote = input(\"db-route\").value === \"ssh\";\n $(\"route-hint\").textContent = remote ? sshAvailable ? \"Through \" + sshHostName + \". 127.0.0.1 means that remote host.\" : \"Connect an SSH host first. If it was reconnected, accept the new connection.\" : \"Direct from this PC. 127.0.0.1 means this PC.\";\n }\n function selectProvider() {\n const sqlite = input(\"db-provider\").value === \"sqlite\";\n $(\"network-fields\").hidden = sqlite;\n $(\"sqlite-fields\").hidden = !sqlite;\n for (const id of [\n \"db-route\",\n \"db-host\",\n \"db-port\",\n \"db-name\",\n \"db-user\",\n \"db-password\",\n \"db-tls\",\n \"db-trust\"\n ])\n input(id).disabled = sqlite;\n input(\"db-path\").disabled = !sqlite;\n input(\"db-trust\").disabled = sqlite || !input(\"db-tls\").checked;\n for (const id of [\"db-host\", \"db-port\", \"db-name\", \"db-user\"])\n input(id).required = !sqlite;\n input(\"db-path\").required = sqlite;\n input(\"db-port\").value = String(\n {\n sqlserver: 1433,\n postgresql: 5432,\n mysql: 3306,\n mariadb: 3306\n }[input(\"db-provider\").value] ?? 0\n );\n }\n function openConnection() {\n if (!companionAvailable) return;\n $(\"connect-error\").hidden = true;\n if (!input(\"db-host\").value) input(\"db-host\").value = \"127.0.0.1\";\n updateRoute();\n dialog.showModal();\n $(\"db-provider\").focus();\n }\n function closeConnection() {\n connectController?.abort();\n input(\"db-password\").value = \"\";\n dialog.close();\n }\n async function connect() {\n if (connectController || !$(\"connect-form\").reportValidity())\n return;\n const route = input(\"db-provider\").value === \"sqlite\" ? \"direct\" : input(\"db-route\").value;\n if (route === \"ssh\" && !sshAvailable) {\n $(\"connect-error\").textContent = \"Connect an SSH host first and allow host TCP access when reviewing this app.\";\n $(\"connect-error\").hidden = false;\n return;\n }\n const active = new AbortController();\n connectController = active;\n setControls();\n $(\"connection-fields\").disabled = true;\n button(\"connect-submit\").disabled = true;\n button(\"connect-submit\").textContent = \"Connecting\\u2026\";\n $(\"connect-error\").hidden = true;\n const selected = input(\"db-provider\").value;\n const configuration = selected === \"sqlite\" ? { provider: selected, sqlitePath: input(\"db-path\").value.trim() } : {\n provider: selected,\n host: input(\"db-host\").value.trim(),\n port: Number(input(\"db-port\").value),\n database: input(\"db-name\").value.trim(),\n username: input(\"db-user\").value.trim(),\n password: input(\"db-password\").value,\n tls: input(\"db-tls\").checked,\n trustServerCertificate: input(\"db-tls\").checked && input(\"db-trust\").checked\n };\n const target = selected === \"sqlite\" ? input(\"db-path\").value.trim() : `${configuration.host}:${configuration.port} / ${configuration.database}`;\n status.textContent = `Connecting to ${target}${route === \"ssh\" ? ` through ${sshHostName}` : \" from this PC\"}\\u2026`;\n try {\n await client.call(\n \"system.companion.connect\",\n { configuration, route },\n active.signal\n );\n destination = `${$(\"db-provider\").selectedOptions[0].text} \\xB7 ${target} \\xB7 ${route === \"ssh\" ? sshHostName : \"this PC\"}`;\n input(\"db-password\").value = \"\";\n dialog.close();\n } catch (error) {\n if (!active.signal.aborted) {\n $(\"connect-error\").textContent = message(error);\n $(\"connect-error\").hidden = false;\n }\n status.textContent = active.signal.aborted ? \"Connection canceled.\" : `Could not connect to ${target}.`;\n } finally {\n connectController = null;\n $(\"connection-fields\").disabled = false;\n button(\"connect-submit\").disabled = false;\n button(\"connect-submit\").textContent = \"Connect\";\n await refresh(true);\n }\n }\n async function start() {\n client = await connectToShellCanvas();\n client.events.subscribe((batch) => {\n for (const event of batch.events)\n if (event.topic === \"system.environment\")\n applyAppearance(\n document.documentElement,\n event.value?.appearance\n );\n if (batch.events.some(\n (event) => [\"system.environment\", \"system.services\", \"system.companion\"].includes(\n event.topic\n )\n ))\n void refresh();\n }, report);\n window.addEventListener(\n \"pagehide\",\n () => {\n closed = true;\n generation++;\n bindOp++;\n queryController?.abort();\n connectController?.abort();\n client.dispose();\n },\n { once: true }\n );\n await refresh();\n }\n button(\"run\").onclick = () => void execute();\n button(\"cancel\").onclick = () => queryController?.abort();\n button(\"reload\").onclick = () => void loadSchemas();\n button(\"export\").onclick = () => void exportCsv();\n button(\"prev\").onclick = () => {\n const offset = previousOffsets.at(-1);\n if (offset !== void 0) void move(offset, true);\n };\n button(\"next\").onclick = () => {\n if (current) void move(nextOffset(current));\n };\n button(\"retry\").onclick = () => void refresh(true);\n button(\"connection-settings\").onclick = openConnection;\n button(\"setup-connect\").onclick = openConnection;\n button(\"close-dialog\").onclick = closeConnection;\n button(\"connect-cancel\").onclick = closeConnection;\n dialog.addEventListener(\"cancel\", (event) => {\n event.preventDefault();\n closeConnection();\n });\n button(\"connect-submit\").onclick = () => void connect();\n $(\"connect-form\").addEventListener(\"submit\", (event) => event.preventDefault());\n $(\"connect-form\").addEventListener(\"keydown\", (event) => {\n if (event.key === \"Enter\" && !event.isComposing && !event.ctrlKey && !event.metaKey && !event.altKey && event.target instanceof HTMLInputElement && event.target.type !== \"checkbox\") {\n event.preventDefault();\n void connect();\n }\n });\n $(\"db-provider\").addEventListener(\"change\", selectProvider);\n $(\"db-route\").addEventListener(\"change\", updateRoute);\n input(\"db-tls\").addEventListener(\"change\", () => {\n input(\"db-trust\").disabled = !input(\"db-tls\").checked;\n });\n button(\"disconnect\").onclick = () => {\n void (async () => {\n queryController?.abort();\n await cancelExport();\n await client.call(\"system.companion.disconnect\");\n destination = \"\";\n await refresh(true);\n })().catch(report);\n };\n sql.addEventListener(\"keydown\", (event) => {\n if ((event.ctrlKey || event.metaKey) && event.key === \"Enter\") {\n event.preventDefault();\n void execute();\n }\n });\n selectProvider();\n setControls();\n void start().catch(\n (error) => unavailable(\"ShellCanvas connection unavailable\", message(error))\n );\n})();\n","style":"/* SPDX-License-Identifier: MPL-2.0 */\r\n:root {\r\n --sc-base: #19191c;\r\n --sc-surface: #232326;\r\n --sc-raised: #303034;\r\n --sc-inset: #1d1d20;\r\n --sc-text: #e8e8ec;\r\n --sc-muted: #aaaab4;\r\n --sc-accent: #7ab8ff;\r\n --sc-onAccent: #14283e;\r\n --sc-border: #44444b;\r\n --sc-hover: #38383e;\r\n --sc-selection: #293f58;\r\n --sc-danger: #f2a4a4;\r\n --sc-dangerSoft: #482d32;\r\n --sc-warning: #eec778;\r\n --sc-success: #88cda5;\r\n font-family: \"Segoe UI\", system-ui, sans-serif;\r\n font-size: 13px;\r\n color-scheme: dark;\r\n}\r\n* {\r\n box-sizing: border-box;\r\n}\r\n[hidden] {\r\n display: none !important;\r\n}\r\nhtml,\r\nbody {\r\n margin: 0;\r\n height: 100%;\r\n background: var(--sc-surface);\r\n color: var(--sc-text);\r\n font:\r\n 13px/1.5 \"Segoe UI\",\r\n system-ui,\r\n sans-serif;\r\n}\r\nbutton,\r\ninput,\r\nselect,\r\ntextarea {\r\n font: inherit;\r\n}\r\nbutton {\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n gap: 7px;\r\n min-height: 32px;\r\n padding: 6px 12px;\r\n border: 1px solid var(--sc-border);\r\n border-radius: 6px;\r\n background: transparent;\r\n color: var(--sc-text);\r\n cursor: pointer;\r\n white-space: nowrap;\r\n}\r\nbutton:hover:not(:disabled) {\r\n background: var(--sc-hover);\r\n}\r\nbutton:disabled {\r\n opacity: 0.45;\r\n cursor: default;\r\n}\r\nbutton.primary {\r\n background: var(--sc-accent);\r\n border-color: var(--sc-accent);\r\n color: var(--sc-onAccent);\r\n font-weight: 600;\r\n}\r\nbutton.primary:hover:not(:disabled) {\r\n filter: brightness(1.08);\r\n background: var(--sc-accent);\r\n}\r\n:focus-visible {\r\n outline: 2px solid var(--sc-accent);\r\n outline-offset: 2px;\r\n}\r\nmain {\r\n height: 100vh;\r\n display: flex;\r\n flex-direction: column;\r\n overflow: hidden;\r\n}\r\nh1,\r\nh2,\r\np {\r\n margin: 0;\r\n}\r\nh1 {\r\n font-size: 15px;\r\n font-weight: 600;\r\n}\r\nh2 {\r\n font-size: 13px;\r\n font-weight: 600;\r\n}\r\nsvg {\r\n width: 22px;\r\n height: 22px;\r\n flex-shrink: 0;\r\n}\r\n.app-header {\r\n display: flex;\r\n gap: 20px;\r\n align-items: center;\r\n justify-content: space-between;\r\n min-height: 76px;\r\n padding: 14px 20px;\r\n border-bottom: 1px solid var(--sc-border);\r\n}\r\n.app-heading,\r\n.header-actions {\r\n display: flex;\r\n align-items: center;\r\n gap: 12px;\r\n min-width: 0;\r\n}\r\n.app-heading p {\r\n color: var(--sc-muted);\r\n font-size: 12px;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n max-width: 50vw;\r\n}\r\n.app-icon {\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n color: var(--sc-accent);\r\n width: 38px;\r\n height: 38px;\r\n border-radius: 10px;\r\n background: var(--sc-selection);\r\n flex-shrink: 0;\r\n}\r\n.connection-state {\r\n font-size: 12px;\r\n color: var(--sc-muted);\r\n white-space: nowrap;\r\n}\r\n.connection-state::before {\r\n content: \"\";\r\n display: inline-block;\r\n width: 6px;\r\n height: 6px;\r\n margin-right: 7px;\r\n border-radius: 50%;\r\n background: currentColor;\r\n}\r\n.connection-state.connected {\r\n color: var(--sc-success);\r\n}\r\n.setup {\r\n display: grid;\r\n place-items: center;\r\n flex: 1;\r\n overflow: auto;\r\n padding: 28px;\r\n}\r\n.setup-card {\r\n text-align: center;\r\n max-width: 490px;\r\n}\r\n.setup-icon {\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n width: 64px;\r\n height: 64px;\r\n border: 1px solid var(--sc-border);\r\n border-radius: 16px;\r\n color: var(--sc-accent);\r\n background: var(--sc-raised);\r\n margin-bottom: 20px;\r\n}\r\n.setup-icon svg {\r\n width: 32px;\r\n height: 32px;\r\n}\r\n.setup h2 {\r\n font-size: 23px;\r\n font-weight: 600;\r\n margin-bottom: 10px;\r\n}\r\n.setup p {\r\n color: var(--sc-muted);\r\n line-height: 1.7;\r\n}\r\n.provider-list {\r\n display: flex;\r\n flex-wrap: wrap;\r\n gap: 8px;\r\n justify-content: center;\r\n margin: 22px 0;\r\n}\r\n.provider-list span {\r\n font-size: 11px;\r\n border: 1px solid var(--sc-border);\r\n padding: 4px 9px;\r\n border-radius: 5px;\r\n color: var(--sc-muted);\r\n}\r\n.setup button {\r\n margin: 0 3px;\r\n}\r\n.setup .setup-note {\r\n font-size: 12px;\r\n margin: 24px auto 0;\r\n max-width: 360px;\r\n}\r\n.workspace {\r\n display: grid;\r\n grid-template-columns: 210px minmax(0, 1fr);\r\n min-height: 0;\r\n flex: 1;\r\n}\r\naside {\r\n background: var(--sc-inset);\r\n border-right: 1px solid var(--sc-border);\r\n padding: 16px 12px;\r\n overflow: auto;\r\n}\r\n.side-head {\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-between;\r\n gap: 10px;\r\n padding: 0 6px 12px;\r\n}\r\n.side-head h2 {\r\n color: var(--sc-muted);\r\n font-size: 11px;\r\n text-transform: uppercase;\r\n letter-spacing: 0.08em;\r\n}\r\n.icon-button {\r\n padding: 0;\r\n min-height: 28px;\r\n width: 28px;\r\n font-size: 18px;\r\n border-color: transparent;\r\n}\r\n#tree {\r\n color: var(--sc-muted);\r\n font-size: 12px;\r\n}\r\ndetails {\r\n margin: 4px 0;\r\n}\r\nsummary {\r\n padding: 5px 6px;\r\n cursor: pointer;\r\n color: var(--sc-text);\r\n}\r\ndetails p {\r\n padding: 6px 16px;\r\n}\r\n.table {\r\n display: block;\r\n border: 0;\r\n width: 100%;\r\n text-align: left;\r\n min-height: 30px;\r\n padding: 5px 12px 5px 25px;\r\n font-size: 12px;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n}\r\n.work {\r\n padding: 18px;\r\n display: flex;\r\n flex-direction: column;\r\n gap: 10px;\r\n min-width: 0;\r\n min-height: 0;\r\n}\r\n.editor-heading {\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-between;\r\n font-size: 12px;\r\n}\r\n.editor-heading span {\r\n font-size: 11px;\r\n color: var(--sc-muted);\r\n}\r\ntextarea {\r\n width: 100%;\r\n min-height: 90px;\r\n height: 150px;\r\n max-height: 50vh;\r\n flex-shrink: 0;\r\n resize: vertical;\r\n border: 1px solid var(--sc-border);\r\n border-radius: 6px;\r\n background: var(--sc-inset);\r\n color: var(--sc-text);\r\n padding: 12px 14px;\r\n font:\r\n 13px/1.7 Consolas,\r\n \"Cascadia Code\",\r\n monospace;\r\n tab-size: 2;\r\n}\r\n.toolbar,\r\n.result-head,\r\n.result-head > div,\r\n.pager {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n}\r\n.query-note {\r\n margin-left: auto;\r\n color: var(--sc-muted);\r\n font-size: 11px;\r\n}\r\n.result-head {\r\n justify-content: space-between;\r\n margin-top: 10px;\r\n}\r\n.safe {\r\n font-size: 11px;\r\n color: var(--sc-muted);\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 5px;\r\n}\r\ninput[type=\"checkbox\"] {\r\n accent-color: var(--sc-accent);\r\n}\r\n.grid-wrap {\r\n overflow: auto;\r\n min-height: 100px;\r\n flex: 1;\r\n border: 1px solid var(--sc-border);\r\n border-radius: 6px;\r\n background: var(--sc-inset);\r\n}\r\ntable {\r\n border-collapse: separate;\r\n border-spacing: 0;\r\n min-width: 100%;\r\n font:\r\n 12px/1.5 Consolas,\r\n monospace;\r\n}\r\nth,\r\ntd {\r\n padding: 8px 12px;\r\n border-right: 1px solid var(--sc-border);\r\n border-bottom: 1px solid var(--sc-border);\r\n white-space: pre;\r\n max-width: 420px;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n text-align: left;\r\n}\r\nth {\r\n position: sticky;\r\n top: 0;\r\n background: var(--sc-raised);\r\n color: var(--sc-muted);\r\n font-weight: 500;\r\n}\r\ntr:hover td {\r\n background: var(--sc-hover);\r\n}\r\n.cell-null {\r\n color: var(--sc-muted);\r\n font-style: italic;\r\n}\r\n.cell-binary {\r\n color: var(--sc-accent);\r\n}\r\n.cell-unsupported,\r\n#warnings {\r\n color: var(--sc-warning);\r\n}\r\n#warnings {\r\n font-size: 12px;\r\n}\r\n#empty {\r\n padding: 28px;\r\n text-align: center;\r\n color: var(--sc-muted);\r\n font-size: 12px;\r\n}\r\n.pager {\r\n justify-content: flex-end;\r\n font-size: 11px;\r\n color: var(--sc-muted);\r\n}\r\n.pager #page {\r\n margin-right: auto;\r\n}\r\n.pager button {\r\n font-size: 11px;\r\n min-height: 28px;\r\n padding: 4px 10px;\r\n}\r\nfooter {\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-between;\r\n gap: 12px;\r\n min-height: 32px;\r\n padding: 6px 16px;\r\n border-top: 1px solid var(--sc-border);\r\n color: var(--sc-muted);\r\n font-size: 11px;\r\n}\r\nfooter > :first-child {\r\n overflow-wrap: anywhere;\r\n}\r\nfooter > :last-child {\r\n white-space: nowrap;\r\n}\r\ndialog {\r\n color: var(--sc-text);\r\n background: var(--sc-surface);\r\n border: 1px solid var(--sc-border);\r\n border-radius: 14px;\r\n padding: 26px;\r\n width: min(520px, calc(100vw - 32px));\r\n max-height: calc(100vh - 32px);\r\n overflow: auto;\r\n box-shadow: 0 20px 70px #0006;\r\n}\r\ndialog::backdrop {\r\n background: #0007;\r\n}\r\n.dialog-heading {\r\n display: flex;\r\n justify-content: space-between;\r\n align-items: center;\r\n margin-bottom: 14px;\r\n}\r\ndialog h2 {\r\n font-size: 23px;\r\n margin-bottom: 8px;\r\n}\r\ndialog p {\r\n color: var(--sc-muted);\r\n font-size: 12px;\r\n line-height: 1.6;\r\n}\r\nfieldset {\r\n border: 0;\r\n margin: 18px 0 0;\r\n padding: 0;\r\n min-width: 0;\r\n}\r\nfieldset label {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 6px;\r\n font-size: 12px;\r\n margin-bottom: 12px;\r\n}\r\nfieldset input:not([type=\"checkbox\"]),\r\nselect {\r\n width: 100%;\r\n min-width: 0;\r\n padding: 9px 10px;\r\n border: 1px solid var(--sc-border);\r\n border-radius: 6px;\r\n background: var(--sc-raised);\r\n color: var(--sc-text);\r\n}\r\n.field-row {\r\n display: flex;\r\n gap: 12px;\r\n}\r\n.field-row label {\r\n flex: 1;\r\n min-width: 0;\r\n}\r\n.field-row .port-field {\r\n flex: 0 0 100px;\r\n}\r\nfieldset .checkbox {\r\n flex-direction: row;\r\n align-items: center;\r\n gap: 7px;\r\n margin: 10px 0;\r\n}\r\n.field-hint {\r\n margin: 10px 0;\r\n}\r\n.dialog-actions {\r\n display: flex;\r\n gap: 8px;\r\n align-items: center;\r\n margin-top: 20px;\r\n}\r\n.dialog-actions span {\r\n margin-right: auto;\r\n font-size: 11px;\r\n color: var(--sc-muted);\r\n}\r\n.error {\r\n margin-top: 14px;\r\n padding: 10px 12px;\r\n border-radius: 6px;\r\n background: var(--sc-dangerSoft);\r\n color: var(--sc-danger);\r\n}\r\n@media (max-width: 700px) {\r\n .app-header {\r\n padding: 12px;\r\n gap: 10px;\r\n }\r\n .connection-state {\r\n display: none;\r\n }\r\n .workspace {\r\n grid-template-columns: 155px minmax(0, 1fr);\r\n }\r\n .work {\r\n padding: 12px;\r\n }\r\n .query-note {\r\n display: none;\r\n }\r\n .result-head {\r\n align-items: flex-start;\r\n gap: 8px;\r\n flex-wrap: wrap;\r\n }\r\n .safe {\r\n font-size: 10px;\r\n }\r\n footer > :last-child {\r\n display: none;\r\n }\r\n}\r\n@media (max-width: 480px) {\r\n .workspace {\r\n grid-template-columns: 1fr;\r\n }\r\n aside {\r\n max-height: 145px;\r\n border-right: 0;\r\n border-bottom: 1px solid var(--sc-border);\r\n }\r\n .app-heading p {\r\n max-width: 40vw;\r\n }\r\n .app-header .app-icon {\r\n display: none;\r\n }\r\n .field-row {\r\n flex-wrap: wrap;\r\n }\r\n}\r\n"} \ No newline at end of file +{"format":1,"kind":"app","id":"dev.shellcanvas.database","version":"0.1.1","title":"ShellCanvas Database","description":"Browse database schemas, run SQL, inspect typed results and export CSV.","permissions":["services.dev.shellcanvas.database","host.tcp"],"icon":"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjggMTI4Ij4KICA8ZGVmcz4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iYmciIHgyPSIxIiB5Mj0iMSI+CiAgICAgIDxzdG9wIHN0b3AtY29sb3I9IiMyOTRkNmEiLz4KICAgICAgPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMTkyOTNlIi8+CiAgICA8L2xpbmVhckdyYWRpZW50PgogIDwvZGVmcz4KICA8cmVjdCB3aWR0aD0iMTI4IiBoZWlnaHQ9IjEyOCIgcng9IjI4IiBmaWxsPSJ1cmwoI2JnKSIvPgogIDxwYXRoIGQ9Ik0yNyA0MXY0M2MwIDkgMTYgMTcgMzcgMTdzMzctOCAzNy0xN1Y0MSIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjODVjNGZmIiBzdHJva2Utd2lkdGg9IjciIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgogIDxlbGxpcHNlIGN4PSI2NCIgY3k9IjQxIiByeD0iMzciIHJ5PSIxNSIgZmlsbD0iIzM4NmM5MSIgc3Ryb2tlPSIjYTJkNWZmIiBzdHJva2Utd2lkdGg9IjciLz4KICA8cGF0aCBkPSJNMjcgNjNjMCA5IDE2IDE3IDM3IDE3czM3LTggMzctMTciIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzg1YzRmZiIgc3Ryb2tlLXdpZHRoPSI2Ii8+CiAgPGNpcmNsZSBjeD0iODciIGN5PSI4MyIgcj0iNSIgZmlsbD0iI2I3ZTRmZiIvPgo8L3N2Zz4K","script":"\"use strict\";\n(() => {\n // node_modules/@techartdev/shellcanvas-app-sdk/dist/rpc.js\n var RpcError = class extends Error {\n code;\n constructor(code, message2) {\n super(message2);\n this.code = code;\n this.name = \"RpcError\";\n }\n };\n var codes = [\n \"invalid\",\n \"closed\",\n \"aborted\",\n \"denied\",\n \"unavailable\",\n \"busy\",\n \"failed\"\n ];\n var maxMessage = 4 * 1024 * 1024;\n var maxPending = 64;\n var methodName = /^[a-z][a-z0-9-]*(?:\\.[a-zA-Z][a-zA-Z0-9-]*)+$/;\n function isJsonValue(value) {\n const pending = [{ value, depth: 0 }];\n while (pending.length) {\n const item = pending.pop();\n if (item.depth > 64)\n return false;\n if (item.value === null || typeof item.value === \"string\" || typeof item.value === \"boolean\")\n continue;\n if (typeof item.value === \"number\" && Number.isFinite(item.value))\n continue;\n if (typeof item.value !== \"object\" || item.value === null)\n return false;\n if (!Array.isArray(item.value) && Object.getPrototypeOf(item.value) !== Object.prototype)\n return false;\n for (const child of Object.values(item.value))\n pending.push({ value: child, depth: item.depth + 1 });\n }\n return true;\n }\n function encode(value) {\n if (!isJsonValue(value))\n throw new RpcError(\"invalid\", \"Only finite JSON values with at most 64 nesting levels are supported.\");\n const text = JSON.stringify(value);\n if (text.length > maxMessage)\n throw new RpcError(\"invalid\", \"RPC message exceeds 4 Mi UTF-16 units; stream bulk data separately.\");\n return text;\n }\n function decode(raw) {\n if (typeof raw !== \"string\" || raw.length > maxMessage)\n throw new RpcError(\"invalid\", \"Invalid RPC message.\");\n const value = JSON.parse(raw);\n if (!isJsonValue(value) || !value || typeof value !== \"object\" || Array.isArray(value) || value.v !== 1)\n throw new RpcError(\"invalid\", \"Unsupported RPC envelope.\");\n if (value.type === \"close\")\n return { v: 1, type: \"close\" };\n const id = value.id;\n if (typeof id !== \"number\" || !Number.isSafeInteger(id) || id < 1)\n throw new RpcError(\"invalid\", \"Invalid request identity.\");\n if (value.type === \"request\" && typeof value.method === \"string\" && methodName.test(value.method) && Object.hasOwn(value, \"params\"))\n return {\n v: 1,\n type: \"request\",\n id,\n method: value.method,\n params: value.params\n };\n if (value.type === \"cancel\")\n return { v: 1, type: \"cancel\", id };\n if (value.type === \"result\" && Object.hasOwn(value, \"value\"))\n return { v: 1, type: \"result\", id, value: value.value };\n if (value.type === \"error\" && typeof value.code === \"string\" && codes.includes(value.code) && typeof value.message === \"string\")\n return {\n v: 1,\n type: \"error\",\n id,\n code: value.code,\n message: value.message\n };\n throw new RpcError(\"invalid\", \"Invalid RPC envelope.\");\n }\n var RpcPeer = class {\n transport;\n closed = false;\n serial = 0;\n received = 0;\n unsubscribe = () => {\n };\n pending = /* @__PURE__ */ new Map();\n running = /* @__PURE__ */ new Map();\n methods;\n grants;\n closeListeners = /* @__PURE__ */ new Set();\n constructor(transport, methods = /* @__PURE__ */ new Map(), grants = []) {\n this.transport = transport;\n this.methods = new Map([...methods].map(([name, method]) => {\n if (!methodName.test(name))\n throw new RpcError(\"invalid\", `Invalid method name: ${name}`);\n return [\n name,\n {\n grants: [...method.grants],\n invoke: method.invoke,\n available: method.available\n }\n ];\n }));\n this.grants = new Set(grants);\n this.unsubscribe = transport.subscribe((raw) => this.receive(raw), () => this.finish(new RpcError(\"closed\", \"Extension channel disconnected.\")));\n }\n get isClosed() {\n return this.closed;\n }\n onClose(listener) {\n if (this.closed) {\n listener();\n return () => {\n };\n }\n this.closeListeners.add(listener);\n return () => {\n this.closeListeners.delete(listener);\n };\n }\n call(method, params = null, signal) {\n if (this.closed)\n return Promise.reject(new RpcError(\"closed\", \"Extension instance has closed.\"));\n if (signal?.aborted)\n return Promise.reject(new RpcError(\"aborted\", \"Request canceled.\"));\n if (!methodName.test(method))\n return Promise.reject(new RpcError(\"invalid\", \"Invalid method name.\"));\n if (this.pending.size >= maxPending)\n return Promise.reject(new RpcError(\"busy\", \"Too many pending requests.\"));\n const id = ++this.serial;\n if (!Number.isSafeInteger(id)) {\n this.close();\n return Promise.reject(new RpcError(\"closed\", \"Request identities exhausted.\"));\n }\n return new Promise((resolve, reject) => {\n const abort = () => {\n const request = this.pending.get(id);\n if (!request)\n return;\n this.pending.delete(id);\n request.cleanup();\n reject(new RpcError(\"aborted\", \"Request canceled; an already dispatched write may have completed.\"));\n this.send({ v: 1, type: \"cancel\", id });\n };\n this.pending.set(id, {\n resolve,\n reject,\n cleanup: () => signal?.removeEventListener(\"abort\", abort)\n });\n signal?.addEventListener(\"abort\", abort, { once: true });\n try {\n this.transport.send(encode({ v: 1, type: \"request\", id, method, params }));\n } catch (error) {\n this.pending.get(id)?.cleanup();\n this.pending.delete(id);\n reject(error instanceof RpcError ? error : new RpcError(\"closed\", \"Unable to send request.\"));\n if (!(error instanceof RpcError))\n this.close();\n }\n });\n }\n /** Revocation retires the whole instance, so no outstanding operation keeps an old grant. */\n close() {\n if (this.closed)\n return;\n this.send({ v: 1, type: \"close\" });\n this.finish(new RpcError(\"closed\", \"Extension instance has closed.\"));\n }\n finish(reason) {\n if (this.closed)\n return;\n this.closed = true;\n this.unsubscribe();\n for (const request of this.pending.values()) {\n request.cleanup();\n request.reject(reason);\n }\n this.pending.clear();\n for (const controller of this.running.values())\n controller.abort();\n this.running.clear();\n for (const listener of this.closeListeners) {\n try {\n listener();\n } catch {\n }\n }\n this.closeListeners.clear();\n this.transport.close();\n }\n send(value) {\n if (this.closed)\n return;\n try {\n this.transport.send(encode(value));\n } catch {\n this.finish(new RpcError(\"closed\", \"Extension channel failed.\"));\n }\n }\n receive(raw) {\n if (this.closed)\n return;\n let value;\n try {\n value = decode(raw);\n } catch {\n this.close();\n return;\n }\n if (value.type === \"close\") {\n this.finish(new RpcError(\"closed\", \"Extension instance has closed.\"));\n return;\n }\n if (value.type === \"cancel\") {\n this.running.get(value.id)?.abort();\n return;\n }\n if (value.type === \"result\" || value.type === \"error\") {\n const request = this.pending.get(value.id);\n if (!request)\n return;\n this.pending.delete(value.id);\n request.cleanup();\n if (value.type === \"result\")\n request.resolve(value.value);\n else\n request.reject(new RpcError(value.code, value.message));\n return;\n }\n if (value.id <= this.received) {\n this.close();\n return;\n }\n this.received = value.id;\n void this.dispatch(value);\n }\n async dispatch(request) {\n const fail = (code, message2) => this.send({ v: 1, type: \"error\", id: request.id, code, message: message2 });\n const method = this.methods.get(request.method);\n if (!method) {\n fail(\"unavailable\", \"Unknown service method.\");\n return;\n }\n if (method.grants.some((grant) => !this.grants.has(grant))) {\n fail(\"denied\", \"The extension does not hold the required permission.\");\n return;\n }\n if (this.running.size >= maxPending) {\n fail(\"busy\", \"Too many active requests.\");\n return;\n }\n const controller = new AbortController();\n this.running.set(request.id, controller);\n try {\n if (method.available && !method.available())\n throw new RpcError(\"unavailable\", \"This service is currently unavailable.\");\n const result = await method.invoke(request.params, controller.signal);\n if (!this.closed && !controller.signal.aborted) {\n encode({ v: 1, type: \"result\", id: request.id, value: result });\n this.send({ v: 1, type: \"result\", id: request.id, value: result });\n }\n } catch (error) {\n if (!controller.signal.aborted && !this.closed)\n fail(error instanceof RpcError ? error.code : \"failed\", error instanceof RpcError ? error.message : \"Service operation failed.\");\n } finally {\n this.running.delete(request.id);\n }\n }\n };\n function messagePortTransport(port) {\n return {\n send: (message2) => port.postMessage(message2),\n subscribe(receive, disconnected) {\n const message2 = (event) => receive(event.data);\n port.addEventListener(\"message\", message2);\n port.addEventListener(\"messageerror\", disconnected);\n port.start();\n return () => {\n port.removeEventListener(\"message\", message2);\n port.removeEventListener(\"messageerror\", disconnected);\n };\n },\n close: () => port.close()\n };\n }\n\n // node_modules/@techartdev/shellcanvas-app-sdk/dist/network-client.js\n function appNetworkClient(peer) {\n return {\n profile: async (slot, signal) => await peer.call(\"system.network.profile\", { slot }, signal),\n configure: async (options, signal) => await peer.call(\"system.network.configure\", {\n slot: options.slot,\n suggestedEndpoint: options.suggestedEndpoint ?? \"\"\n }, signal),\n forget: async (slot, signal) => {\n await peer.call(\"system.network.forget\", { slot }, signal);\n },\n async postJSON(options, signal) {\n const id = crypto.randomUUID();\n let closed2 = false, reading = false;\n let closing;\n const close = () => {\n closed2 = true;\n signal?.removeEventListener(\"abort\", abort);\n return closing ??= peer.call(\"system.network.close\", { id }).then(() => {\n });\n };\n const abort = () => {\n void close().catch(() => {\n });\n };\n signal?.throwIfAborted();\n const starting = peer.call(\"system.network.start\", { id, ...options }, signal);\n signal?.addEventListener(\"abort\", abort, { once: true });\n try {\n const head = await starting;\n if (closed2 || signal?.aborted)\n throw new RpcError(\"aborted\", \"HTTP request canceled.\");\n return {\n ...head,\n close,\n async read(control) {\n if (closed2)\n throw new RpcError(\"closed\", \"HTTP response closed.\");\n if (reading)\n throw new RpcError(\"busy\", \"Wait for the current response read.\");\n reading = true;\n control?.addEventListener(\"abort\", abort, { once: true });\n try {\n control?.throwIfAborted();\n const bytes = await peer.call(\"system.network.read\", { id }, control);\n if (bytes === null) {\n await close();\n return null;\n }\n return Uint8Array.from(bytes);\n } catch (error) {\n abort();\n throw error;\n } finally {\n reading = false;\n control?.removeEventListener(\"abort\", abort);\n }\n }\n };\n } catch (error) {\n abort();\n throw error;\n }\n }\n };\n }\n\n // node_modules/@techartdev/shellcanvas-app-sdk/dist/file-client.js\n function appFileClient(peer) {\n return Object.freeze({\n makeDirectory: async ({ binding, parent, name }, signal) => await peer.call(\"system.files.makeDirectory\", { binding, parent, name }, signal),\n renameEntry: async ({ binding, path, revision }, name, signal) => await peer.call(\"system.files.renameEntry\", { binding, path, revision, name }, signal),\n moveEntry: async ({ binding, path, revision }, parent, signal) => await peer.call(\"system.files.moveEntry\", {\n binding,\n path,\n revision,\n parent: parent.path,\n parentBinding: parent.binding\n }, signal),\n removeEntry: async ({ binding, path, revision }, signal) => {\n await peer.call(\"system.files.removeEntry\", { binding, path, revision }, signal);\n },\n async *list({ binding, path }, signal) {\n const id = crypto.randomUUID();\n let closing;\n const close = () => closing ??= peer.call(\"system.files.listClose\", { id }).catch(() => {\n });\n const cancel = () => {\n void close();\n };\n signal?.addEventListener(\"abort\", cancel, { once: true });\n try {\n let result = await peer.call(\"system.files.listStart\", { id, binding, path: path ?? null }, signal);\n while (true) {\n yield result.directory;\n if (result.done)\n break;\n result = await peer.call(\"system.files.listNext\", { id }, signal);\n }\n } finally {\n signal?.removeEventListener(\"abort\", cancel);\n await close();\n }\n },\n readText: async ({ binding, path }, signal) => await peer.call(\"system.files.readText\", { binding, path }, signal),\n saveText: async ({ binding, path, revision }, text, signal) => await peer.call(\"system.files.saveText\", { binding, path, revision, text }, signal),\n createText: async ({ binding, parent, name, text }, signal) => await peer.call(\"system.files.createText\", { binding, parent, name, text }, signal)\n });\n }\n\n // node_modules/@techartdev/shellcanvas-app-sdk/dist/console-client.js\n function appConsoleClient(peer) {\n return {\n async open(options, signal) {\n const id = crypto.randomUUID();\n let closed2 = false;\n let closing;\n let reading = false;\n let writing = false;\n let stopPeer = () => {\n };\n const close = () => {\n if (closing)\n return closing;\n closed2 = true;\n stopPeer();\n signal?.removeEventListener(\"abort\", abort);\n closing = peer.call(\"system.console.close\", { id }).then(() => {\n });\n return closing;\n };\n const abort = () => {\n void close().catch(() => {\n });\n };\n stopPeer = peer.onClose(() => {\n closed2 = true;\n signal?.removeEventListener(\"abort\", abort);\n closing ??= Promise.resolve();\n });\n if (signal?.aborted) {\n stopPeer();\n throw new RpcError(\"aborted\", \"Console opening canceled.\");\n }\n const opening = peer.call(\"system.console.open\", {\n id,\n binding: options.binding,\n cols: options.cols ?? 80,\n rows: options.rows ?? 24\n }, signal);\n signal?.addEventListener(\"abort\", abort, { once: true });\n try {\n const result = await opening;\n if (closed2 || signal?.aborted)\n throw new RpcError(\"aborted\", \"Console opening canceled.\");\n const call2 = async (method, params, control) => {\n if (closed2)\n throw new RpcError(\"closed\", \"Console is closed.\");\n const onAbort = () => {\n void close().catch(() => {\n });\n };\n control?.addEventListener(\"abort\", onAbort, { once: true });\n try {\n if (control?.aborted) {\n onAbort();\n throw new RpcError(\"aborted\", \"Console operation canceled; the console was closed.\");\n }\n return await peer.call(method, params, control);\n } finally {\n control?.removeEventListener(\"abort\", onAbort);\n }\n };\n return {\n binding: result.binding,\n resizable: result.resizable,\n close,\n async read(control) {\n if (reading)\n throw new RpcError(\"busy\", \"Wait for the current console read.\");\n reading = true;\n try {\n const value = await call2(\"system.console.read\", { id }, control);\n return value === null ? null : Uint8Array.from(value.bytes);\n } finally {\n reading = false;\n }\n },\n async write(data, control) {\n if (writing)\n throw new RpcError(\"busy\", \"Wait for the current console write.\");\n const bytes = typeof data === \"string\" ? new TextEncoder().encode(data) : new Uint8Array(data);\n writing = true;\n try {\n for (let offset = 0; offset < bytes.length || offset === 0; offset += 65536)\n await call2(\"system.console.write\", {\n id,\n bytes: Array.from(bytes.subarray(offset, offset + 65536))\n }, control);\n } finally {\n writing = false;\n }\n },\n async resize(cols, rows, control) {\n if (!result.resizable)\n throw new RpcError(\"unavailable\", \"This console has fixed dimensions.\");\n await call2(\"system.console.resize\", { id, cols, rows }, control);\n }\n };\n } catch (error) {\n void close().catch(() => {\n });\n throw error;\n }\n }\n };\n }\n\n // node_modules/@techartdev/shellcanvas-app-sdk/dist/transfer-client.js\n function appTransferClient(peer) {\n const handle = (item) => {\n let closed2 = false;\n let running;\n let closing;\n const call2 = (method, extra = {}, signal) => {\n if (closed2)\n return Promise.reject(new RpcError(\"closed\", \"Transfer handle is closed.\"));\n return peer.call(`system.transfers.${method}`, { id: item.id, ...extra }, signal);\n };\n const cancel = async () => {\n await call2(\"cancel\");\n };\n return {\n binding: item.binding,\n name: item.name,\n size: item.size,\n direction: item.direction,\n async run(signal) {\n const abort = () => {\n void cancel().catch(() => {\n });\n };\n signal?.addEventListener(\"abort\", abort, { once: true });\n try {\n if (signal?.aborted)\n await cancel();\n running ??= call2(\"run\");\n return await running;\n } finally {\n signal?.removeEventListener(\"abort\", abort);\n }\n },\n async status() {\n return await call2(\"status\", {\n after: null\n });\n },\n async *watch(signal) {\n let after = null;\n for (; ; ) {\n const snapshot = await call2(\"status\", { after }, signal);\n yield snapshot;\n if (snapshot.result)\n return;\n after = snapshot.revision;\n }\n },\n cancel,\n close() {\n if (closing)\n return closing;\n closing = call2(\"close\").then(() => {\n closed2 = true;\n }, (error) => {\n closing = void 0;\n throw error;\n });\n return closing;\n }\n };\n };\n async function prepare(method, options, signal) {\n const id = crypto.randomUUID();\n try {\n const items = await peer.call(`system.transfers.${method}`, { id, ...options }, signal);\n if (signal?.aborted)\n throw new RpcError(\"aborted\", \"Transfer preparation canceled.\");\n return items.map(handle);\n } catch (error) {\n void peer.call(\"system.transfers.cancelPreparation\", { id }).catch(() => {\n });\n throw error;\n }\n }\n return {\n pasteClipboard: async (destination2, signal) => {\n const snapshot = await peer.call(\"system.transfers.clipboardInspect\", { binding: destination2.binding }, signal);\n if (snapshot.kind === \"empty\")\n return [];\n return prepare(snapshot.sequence === null ? \"clipboardPaste\" : snapshot.kind === \"remote\" ? snapshot.intent === \"move\" ? \"clipboardMoveSnapshot\" : \"clipboardCopySnapshot\" : \"clipboardPasteSnapshot\", {\n binding: destination2.binding,\n parent: destination2.path,\n ...snapshot.sequence === null ? {} : { sequence: snapshot.sequence }\n }, signal);\n },\n upload: (destination2, options, signal) => prepare(options?.folder ? \"uploadFolder\" : \"upload\", { binding: destination2.binding, parent: destination2.path }, signal),\n async download(entry, signal) {\n return (await prepare(\"download\", {\n binding: entry.binding,\n path: entry.path,\n revision: entry.revision\n }, signal))[0] ?? null;\n },\n downloadMany: (entries, signal) => prepare(\"downloadMany\", {\n binding: entries[0]?.binding ?? \"\",\n entries: entries.map((entry) => ({\n binding: entry.binding,\n path: entry.path,\n revision: entry.revision\n }))\n }, signal),\n async copy(entry, destination2, signal) {\n return (await prepare(\"copy\", {\n binding: entry.binding,\n path: entry.path,\n revision: entry.revision,\n parent: destination2.path,\n parentBinding: destination2.binding\n }, signal))[0];\n }\n };\n }\n\n // node_modules/@techartdev/shellcanvas-app-sdk/dist/host-settings-client.js\n function appHostSettingsClient(peer) {\n return {\n read: async (location, signal) => await peer.call(\"system.hostSettings.read\", { binding: location.binding }, signal),\n apply: async (setting, value, signal) => await peer.call(\"system.hostSettings.apply\", {\n binding: setting.binding,\n id: setting.id,\n revision: setting.revision,\n value\n }, signal)\n };\n }\n\n // node_modules/@techartdev/shellcanvas-app-sdk/dist/events-client.js\n function appEventClient(peer) {\n const listeners = /* @__PURE__ */ new Set();\n const latest = /* @__PURE__ */ new Map();\n let cursor = null, controller;\n const error = (listener, failure) => {\n try {\n listener.error?.(failure);\n } catch {\n }\n };\n const deliver = (listener, batch) => {\n if (listeners.has(listener))\n try {\n void Promise.resolve(listener.receive(structuredClone(batch))).catch((failure) => error(listener, failure));\n } catch (failure) {\n error(listener, failure);\n }\n };\n const poll = async (active) => {\n try {\n while (listeners.size && controller === active) {\n const batch = await peer.call(\"system.events.next\", { after: cursor }, active.signal);\n if (controller !== active || active.signal.aborted)\n return;\n cursor = batch.cursor;\n if (batch.reset)\n latest.clear();\n for (const event of batch.events)\n latest.set(event.topic, event);\n for (const listener of [...listeners])\n deliver(listener, batch);\n }\n } catch (failure) {\n if (controller !== active || active.signal.aborted)\n return;\n const stopped = [...listeners];\n listeners.clear();\n controller = void 0;\n latest.clear();\n cursor = null;\n for (const listener of stopped)\n error(listener, failure);\n }\n };\n return Object.freeze({\n subscribe(receive, onError) {\n const listener = { receive, error: onError };\n listeners.add(listener);\n if (!controller) {\n controller = new AbortController();\n void poll(controller);\n } else if (cursor !== null) {\n const snapshot = {\n cursor,\n reset: true,\n events: [...latest.values()].sort((a, b) => a.sequence - b.sequence)\n };\n queueMicrotask(() => deliver(listener, snapshot));\n }\n return () => {\n listeners.delete(listener);\n if (!listeners.size) {\n const previous = controller;\n controller = void 0;\n previous?.abort();\n cursor = null;\n latest.clear();\n }\n };\n }\n });\n }\n\n // node_modules/@techartdev/shellcanvas-app-sdk/dist/clipboard-image.js\n var imageChunkSize = 32 * 1024;\n function imageByteLength(width, height) {\n if (typeof width !== \"number\" || typeof height !== \"number\" || !Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0 || width > 4294967295 || height > 4294967295 || !Number.isSafeInteger(width * height * 4))\n throw new RpcError(\"invalid\", \"Provide positive image dimensions with a representable RGBA length.\");\n return width * height * 4;\n }\n function imageBytes(value) {\n return Array.isArray(value) && value.length > 0 && value.length <= imageChunkSize && value.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255);\n }\n function appImageClipboardClient(peer) {\n const release = (id) => peer.call(\"system.clipboard.image.release\", { id }).catch(() => {\n });\n return {\n async readImage(signal) {\n const id = crypto.randomUUID();\n try {\n const meta = await peer.call(\"system.clipboard.image.readStart\", { id }, signal);\n const length = imageByteLength(meta?.width, meta?.height);\n const rgba = new Uint8Array(length);\n let offset = 0;\n while (offset < length) {\n const bytes = await peer.call(\"system.clipboard.image.readChunk\", { id, offset }, signal);\n if (!imageBytes(bytes) || offset + bytes.length > length)\n throw new RpcError(\"failed\", \"Invalid clipboard image stream.\");\n rgba.set(bytes, offset);\n offset += bytes.length;\n }\n return { width: meta.width, height: meta.height, rgba };\n } finally {\n await release(id);\n }\n },\n async writeImage(image, signal) {\n const length = imageByteLength(image?.width, image?.height);\n if (!(image.rgba instanceof Uint8Array) || image.rgba.length !== length)\n throw new RpcError(\"invalid\", \"Image dimensions must match the RGBA bytes.\");\n const rgba = image.rgba.slice(), width = image.width, height = image.height;\n const id = crypto.randomUUID();\n try {\n await peer.call(\"system.clipboard.image.writeStart\", { id, width, height }, signal);\n for (let offset = 0; offset < length; offset += imageChunkSize)\n await peer.call(\"system.clipboard.image.writeChunk\", {\n id,\n offset,\n bytes: Array.from(rgba.subarray(offset, offset + imageChunkSize))\n }, signal);\n await peer.call(\"system.clipboard.image.writeCommit\", { id }, signal);\n } finally {\n await release(id);\n }\n }\n };\n }\n\n // node_modules/@techartdev/shellcanvas-app-sdk/dist/clipboard-client.js\n var chunkSize = 64 * 1024;\n function appClipboardClient(peer) {\n const release = (id) => peer.call(\"system.clipboard.release\", { id }).catch(() => {\n });\n return Object.freeze({\n async cutFile(entry, signal) {\n if (!entry || typeof entry.binding !== \"string\" || !entry.binding || typeof entry.path !== \"string\" || !entry.path || typeof entry.revision !== \"string\" || !entry.revision)\n throw new RpcError(\"invalid\", \"Cut a current file entry from an accepted workspace binding.\");\n const { binding, path, revision } = entry;\n const id = crypto.randomUUID();\n try {\n await peer.call(\"system.clipboard.files.cut\", { id, binding, path, revision }, signal);\n } finally {\n await peer.call(\"system.clipboard.files.release\", { id }).catch(() => {\n });\n }\n },\n async copyFiles(entries, signal) {\n const snapshot = entries.map(({ binding, path, revision }) => ({\n binding,\n path,\n revision\n }));\n if (!snapshot.length || snapshot.some((entry) => entry.binding !== snapshot[0].binding || !entry.path || !entry.revision))\n throw new RpcError(\"invalid\", \"Copy current file entries from one workspace binding.\");\n const id = crypto.randomUUID();\n try {\n await peer.call(\"system.clipboard.files.start\", { id, binding: snapshot[0].binding }, signal);\n for (let offset = 0; offset < snapshot.length; ) {\n const chunk = [];\n let size = 0;\n while (offset + chunk.length < snapshot.length && chunk.length < 128) {\n const { path, revision } = snapshot[offset + chunk.length];\n const entry = { path, revision };\n const length = JSON.stringify(entry).length;\n if (chunk.length && size + length > 64 * 1024)\n break;\n chunk.push(entry);\n size += length;\n }\n await peer.call(\"system.clipboard.files.append\", {\n id,\n offset,\n entries: chunk\n }, signal);\n offset += chunk.length;\n }\n await peer.call(\"system.clipboard.files.commit\", { id }, signal);\n } finally {\n await peer.call(\"system.clipboard.files.release\", { id }).catch(() => {\n });\n }\n },\n pasteFiles: appTransferClient(peer).pasteClipboard,\n ...appImageClipboardClient(peer),\n async readText(signal) {\n const id = crypto.randomUUID();\n const chunks = [];\n try {\n const start2 = await peer.call(\"system.clipboard.readStart\", { id }, signal);\n let offset = 0;\n while (offset < start2.length) {\n const chunk = await peer.call(\"system.clipboard.readChunk\", { id: start2.id, offset }, signal);\n if (typeof chunk !== \"string\" || !chunk.length || offset + chunk.length > start2.length)\n throw new RpcError(\"failed\", \"Invalid clipboard text stream.\");\n chunks.push(chunk);\n offset += chunk.length;\n }\n return chunks.join(\"\");\n } finally {\n await release(id);\n }\n },\n async writeText(text, signal) {\n if (typeof text !== \"string\")\n throw new RpcError(\"invalid\", \"Clipboard text must be a string.\");\n const id = crypto.randomUUID();\n try {\n await peer.call(\"system.clipboard.writeStart\", { id, length: text.length }, signal);\n for (let offset = 0; offset < text.length; offset += chunkSize)\n await peer.call(\"system.clipboard.writeChunk\", { id, offset, text: text.slice(offset, offset + chunkSize) }, signal);\n await peer.call(\"system.clipboard.writeCommit\", { id }, signal);\n } finally {\n await release(id);\n }\n }\n });\n }\n\n // node_modules/@techartdev/shellcanvas-app-sdk/dist/client.js\n function connectToShellCanvas(timeoutMs = 1e4) {\n return new Promise((resolve, reject) => {\n const token = document.querySelector('meta[name=\"shellcanvas-instance\"]')?.content;\n if (!token) {\n reject(new Error(\"Missing ShellCanvas instance identity.\"));\n return;\n }\n if (window.parent === window) {\n reject(new Error(\"Launch this app inside ShellCanvas.\"));\n return;\n }\n const timer = setTimeout(() => {\n window.removeEventListener(\"message\", receive);\n reject(new Error(\"ShellCanvas did not establish an app channel.\"));\n }, timeoutMs);\n const receive = (event) => {\n if (event.source !== window.parent || event.data?.type !== \"shellcanvas:connect:v1\" || event.data?.token !== token || event.ports.length !== 1)\n return;\n clearTimeout(timer);\n window.removeEventListener(\"message\", receive);\n const peer = new RpcPeer(messagePortTransport(event.ports[0]));\n let lastFocus = document.activeElement;\n const rememberFocus = (event2) => {\n if (event2.target instanceof HTMLElement && event2.target !== document.body)\n lastFocus = event2.target;\n };\n document.addEventListener(\"focusin\", rememberFocus);\n const dispose = () => {\n document.removeEventListener(\"focusin\", rememberFocus);\n window.removeEventListener(\"pagehide\", dispose);\n peer.close();\n };\n const call2 = async (method, params, signal) => {\n const origin = document.activeElement === document.body ? lastFocus : document.activeElement;\n try {\n return await peer.call(method, JSON.parse(JSON.stringify(params)), signal);\n } finally {\n requestAnimationFrame(() => requestAnimationFrame(() => {\n if (!peer.isClosed && origin instanceof HTMLElement && origin.isConnected)\n origin.focus();\n }));\n }\n };\n const system = Object.freeze({\n apiVersion: 1,\n dialogs: Object.freeze({\n messageBox: async (options, control) => await call2(\"system.dialogs.messageBox\", options, control?.signal),\n openFile: async (options = {}, control) => await call2(\"system.dialogs.openFile\", options, control?.signal),\n saveFile: async (options = {}, control) => await call2(\"system.dialogs.saveFile\", options, control?.signal)\n }),\n files: Object.freeze({\n saveTextAs: async (options, control) => await call2(\"system.files.saveTextAs\", options, control?.signal)\n })\n });\n window.addEventListener(\"pagehide\", dispose, { once: true });\n const storage = (namespace) => Object.freeze({\n get: async (key, signal) => await peer.call(`system.${namespace}.get`, { key }, signal),\n put: async (key, value, expectedRevision, signal) => await peer.call(`system.${namespace}.put`, { key, value, expectedRevision }, signal),\n remove: async (key, expectedRevision, signal) => {\n await peer.call(`system.${namespace}.remove`, { key, expectedRevision }, signal);\n },\n list: async (options = {}, signal) => await peer.call(`system.${namespace}.list`, JSON.parse(JSON.stringify(options)), signal)\n });\n resolve({\n network: appNetworkClient(peer),\n system,\n files: appFileClient(peer),\n console: appConsoleClient(peer),\n transfers: appTransferClient(peer),\n hostSettings: appHostSettingsClient(peer),\n storage: storage(\"storage\"),\n settings: storage(\"settings\"),\n environment: Object.freeze({\n get: async (signal) => await peer.call(\"system.environment.get\", null, signal)\n }),\n services: Object.freeze({\n call: (method, params = null, signal) => peer.call(\"system.services.call\", { method, params }, signal),\n list: async (signal) => await peer.call(\"system.services.list\", null, signal)\n }),\n events: appEventClient(peer),\n clipboard: appClipboardClient(peer),\n window: Object.freeze({\n getState: async (signal) => await peer.call(\"system.window.getState\", null, signal),\n focus: async (signal) => {\n await peer.call(\"system.window.focus\", null, signal);\n requestAnimationFrame(() => requestAnimationFrame(() => {\n if (!peer.isClosed && lastFocus instanceof HTMLElement && lastFocus.isConnected)\n lastFocus.focus({ preventScroll: true });\n }));\n },\n minimize: async (signal) => {\n await peer.call(\"system.window.minimize\", null, signal);\n },\n maximize: async (signal) => {\n await peer.call(\"system.window.maximize\", null, signal);\n },\n restore: async (signal) => {\n await peer.call(\"system.window.restore\", null, signal);\n },\n requestClose: async (signal) => {\n await peer.call(\"system.window.requestClose\", null, signal);\n },\n setDocumentState: async (state) => {\n await peer.call(\"system.window.setDocumentState\", JSON.parse(JSON.stringify(state)));\n }\n }),\n call: (method, params, signal) => peer.call(method, params, signal),\n dispose\n });\n };\n window.addEventListener(\"message\", receive);\n window.parent.postMessage({ type: \"shellcanvas:ready:v1\", token }, \"*\");\n });\n }\n\n // paging.js\n function nextOffset(page) {\n return page.offset + page.rows.length;\n }\n\n // connection-state.js\n var prefix = \"dev.shellcanvas.database.\";\n var required = [\n \"capabilities\",\n \"schemas\",\n \"tables\",\n \"query\",\n \"page\",\n \"closeResult\",\n \"exportBegin\",\n \"exportStatus\",\n \"exportCancel\"\n ];\n function connectionState(environment, methods, ownConnection = false) {\n const database = methods.filter((method) => method.name.startsWith(prefix));\n if (database.some((method) => !method.granted))\n return {\n ready: false,\n title: \"Database permission needed\",\n detail: \"Review this app in App Manager and allow its Database service permission.\"\n };\n if (!ownConnection && environment.connection === \"review-required\")\n return {\n ready: false,\n title: \"Review the reconnected host\",\n detail: \"Use the reconnected host in the bar above this app, or connect a database directly.\"\n };\n if (!ownConnection && environment.connection === \"disconnected\")\n return {\n ready: false,\n title: \"Workspace disconnected\",\n detail: \"Reconnect the workspace, or create a database connection directly from this app.\"\n };\n if (!database.length)\n return {\n ready: false,\n title: \"Connect a database\",\n detail: \"Choose a database server or an existing SQLite file to browse tables and run queries.\"\n };\n if (!required.every(\n (name) => database.some(\n (method) => method.name === prefix + name && method.available && method.granted\n )\n ))\n return {\n ready: false,\n title: \"Database service unavailable\",\n detail: \"The selected workspace does not expose all database operations. Reconnect it, or connect a database directly.\"\n };\n return {\n ready: true,\n title: \"Connected\",\n detail: \"Database services are available.\"\n };\n }\n var colors = [\n \"base\",\n \"surface\",\n \"raised\",\n \"inset\",\n \"text\",\n \"muted\",\n \"accent\",\n \"onAccent\",\n \"border\",\n \"hover\",\n \"selection\",\n \"danger\",\n \"dangerSoft\",\n \"warning\",\n \"warningSoft\",\n \"success\"\n ];\n function applyAppearance(root2, appearance) {\n if (!appearance) return;\n root2.dataset.mode = appearance.mode === \"light\" ? \"light\" : \"dark\";\n root2.style.setProperty(\"color-scheme\", root2.dataset.mode);\n for (const key of colors) {\n const value = appearance.colors[key];\n if (typeof value === \"string\" && /^#[0-9a-f]{6}$/i.test(value))\n root2.style.setProperty(`--sc-${key}`, value);\n }\n }\n\n // main.ts\n var service = \"dev.shellcanvas.database\";\n var databaseIcon = '';\n var root = document.querySelector(\"#root\");\n root.innerHTML = `\n
\n
${databaseIcon}

Database

No database connected

Not connected
\n
${databaseIcon}

Connect a database

Choose a database server or an existing SQLite file to browse tables and run queries.

SQL ServerPostgreSQLMySQL / MariaDBSQLite

Database connections use your connected SSH host. No public database port is needed.

\n \n
Checking the app connection\\u2026Database workspace
\n
\n
${databaseIcon}

Connect a database

Connect to a database on your SSH host or its network. Enter its address and credentials.

For SQL Server named instances, enter the database server and its TCP port.

Passwords are not saved.
`;\n var $ = (id) => document.getElementById(id);\n var input = (id) => $(id);\n var button = (id) => $(id);\n var sql = $(\"sql\");\n var dialog = $(\"connect-dialog\");\n var status = $(\"status\");\n var client;\n var current = null;\n var queryController = null;\n var connectController = null;\n var provider = \"\";\n var generation = 0;\n var schemaOp = 0;\n var pageOp = 0;\n var bindOp = 0;\n var exportJob = null;\n var pageBusy = false;\n var ready = false;\n var exportBusy = false;\n var previousOffsets = [];\n var signature = null;\n var privateConnection = false;\n var companionAvailable = false;\n var destination = \"\";\n var sshAvailable = false;\n var sshHostName = \"\";\n var closed = false;\n var pageSize = 100;\n function message(error) {\n return error instanceof RpcError ? error.message : error instanceof Error ? error.message : String(error);\n }\n function report(error) {\n status.textContent = message(error);\n }\n function setControls() {\n button(\"run\").disabled = !ready || !!queryController || !!connectController || exportBusy;\n button(\"cancel\").disabled = !queryController;\n button(\"reload\").disabled = !ready || !!connectController;\n button(\"export\").disabled = !ready || !current?.columns.length || exportBusy || !!queryController;\n button(\"prev\").disabled = !ready || pageBusy || !previousOffsets.length;\n button(\"next\").disabled = !ready || pageBusy || !current?.hasMore;\n button(\"disconnect\").hidden = !privateConnection || !ready;\n button(\"disconnect\").disabled = !!connectController;\n button(\"connection-settings\").disabled = !companionAvailable || !!connectController || !!queryController || exportBusy;\n button(\"setup-connect\").disabled = !companionAvailable || !!connectController;\n }\n async function call(method, params = {}, signal) {\n return privateConnection ? await client.call(\n \"system.companion.call\",\n { method: `${service}.${method}`, params },\n signal\n ) : await client.services.call(\n `${service}.${method}`,\n params,\n signal\n );\n }\n function unavailable(title, detail) {\n ready = false;\n $(\"connection-state\").textContent = \"Not connected\";\n $(\"connection-state\").classList.remove(\"connected\");\n $(\"destination\").textContent = \"No database connected\";\n $(\"setup\").hidden = false;\n $(\"workspace\").hidden = true;\n $(\"setup-title\").textContent = title;\n $(\"setup-detail\").textContent = detail;\n button(\"connection-settings\").textContent = \"Connect database\";\n status.textContent = detail;\n setControls();\n }\n function clearResult(note = \"Run a query to see results.\", release = true) {\n const old = current;\n current = null;\n if (release && old)\n void call(\"closeResult\", {\n resultId: old.resultId,\n offset: 0,\n pageSize: 1\n }).catch(() => {\n });\n document.querySelector(\"thead\").replaceChildren();\n document.querySelector(\"tbody\").replaceChildren();\n $(\"summary\").textContent = \"Results\";\n $(\"page\").textContent = \"\";\n $(\"warnings\").hidden = true;\n $(\"empty\").hidden = false;\n $(\"empty\").textContent = note;\n previousOffsets = [];\n setControls();\n }\n function render(page) {\n current = page;\n const head = document.querySelector(\"thead\"), body = document.querySelector(\"tbody\");\n head.replaceChildren();\n body.replaceChildren();\n if (page.columns.length) {\n const tr = document.createElement(\"tr\");\n for (const column of page.columns) {\n const th = document.createElement(\"th\");\n th.textContent = column.name;\n th.title = column.databaseType;\n tr.append(th);\n }\n head.append(tr);\n }\n for (const row of page.rows) {\n const tr = document.createElement(\"tr\");\n for (const cell of row) {\n const td = document.createElement(\"td\");\n td.textContent = cell.kind === \"null\" ? \"NULL\" : cell.display;\n td.className = `cell-${cell.kind}`;\n tr.append(td);\n }\n body.append(tr);\n }\n $(\"empty\").hidden = page.rows.length > 0;\n $(\"empty\").textContent = page.columns.length ? \"The query returned no rows.\" : `Statement completed. ${page.affectedRows.toLocaleString()} rows affected.`;\n $(\"summary\").textContent = page.columns.length ? `${page.totalRows.toLocaleString()} rows${page.truncated ? \" \\xB7 limit reached\" : \"\"}` : `${page.affectedRows.toLocaleString()} rows affected`;\n $(\"page\").textContent = page.totalRows ? `${page.offset + 1}\\u2013${page.offset + page.rows.length} of ${page.totalRows.toLocaleString()}` : \"\";\n $(\"warnings\").textContent = page.warnings.join(\" \\xB7 \");\n $(\"warnings\").hidden = !page.warnings.length;\n setControls();\n }\n function quote(name) {\n return provider === \"mysql\" ? `\\`${name.replaceAll(\"`\", \"``\")}\\`` : provider === \"sqlserver\" ? `[${name.replaceAll(\"]\", \"]]\")}]` : `\"${name.replaceAll('\"', '\"\"')}\"`;\n }\n function tableSql(schema, table) {\n const full = provider === \"sqlite\" ? quote(table) : `${quote(schema)}.${quote(table)}`;\n return provider === \"sqlserver\" ? `SELECT TOP (200) * FROM ${full};` : `SELECT * FROM ${full} LIMIT 200;`;\n }\n async function loadSchemas() {\n const op = ++schemaOp, gen = generation, tree = $(\"tree\");\n tree.textContent = \"Loading schemas\\u2026\";\n try {\n const value = await call(\"schemas\");\n if (op !== schemaOp || gen !== generation) return;\n tree.replaceChildren();\n for (const schema of value.schemas) {\n const details = document.createElement(\"details\"), summary = document.createElement(\"summary\");\n summary.textContent = schema;\n details.append(summary);\n details.addEventListener(\"toggle\", () => {\n if (details.open && details.childElementCount === 1)\n void loadTables(details, schema, gen);\n });\n tree.append(details);\n if (value.schemas.length === 1) details.open = true;\n }\n if (!value.schemas.length)\n tree.textContent = \"No schemas visible to this account.\";\n } catch (error) {\n if (op === schemaOp && gen === generation) {\n tree.textContent = \"Could not load schemas. Use Refresh to retry.\";\n report(error);\n }\n }\n }\n async function loadTables(details, schema, gen) {\n const wait = document.createElement(\"p\");\n wait.textContent = \"Loading tables\\u2026\";\n details.append(wait);\n try {\n const value = await call(\"tables\", { schema });\n if (gen !== generation || !details.isConnected) return;\n wait.remove();\n for (const table of value.tables) {\n const item = document.createElement(\"button\");\n item.className = \"table\";\n item.textContent = table.name;\n item.title = `${table.kind} \\xB7 insert a query`;\n item.onclick = () => {\n sql.value = tableSql(schema, table.name);\n sql.focus();\n };\n details.append(item);\n }\n if (!value.tables.length) {\n wait.textContent = \"No tables\";\n details.append(wait);\n }\n } catch (error) {\n if (gen === generation && details.isConnected) {\n wait.textContent = \"Could not load tables. Refresh schemas to retry.\";\n report(error);\n }\n }\n }\n async function execute() {\n if (!ready || queryController || !sql.value.trim() || connectController || exportBusy)\n return;\n const gen = generation, active = new AbortController();\n queryController = active;\n clearResult(\"Running query\\u2026\");\n setControls();\n status.textContent = \"Running query\\u2026\";\n try {\n const page = await call(\n \"query\",\n { sql: sql.value, pageSize },\n active.signal\n );\n if (gen !== generation) return;\n render(page);\n status.textContent = \"Query completed.\";\n } catch (error) {\n if (gen === generation) {\n clearResult(\n active.signal.aborted ? \"Query canceled. Check the database before retrying statements that change data.\" : \"The query failed.\"\n );\n report(error);\n }\n } finally {\n if (queryController === active) queryController = null;\n setControls();\n }\n }\n async function move(offset, previous = false) {\n if (!current || pageBusy || !ready) return;\n const op = ++pageOp, gen = generation, original = current;\n pageBusy = true;\n setControls();\n try {\n const page = await call(\"page\", {\n resultId: original.resultId,\n offset,\n pageSize\n });\n if (op === pageOp && gen === generation && current === original) {\n if (previous) previousOffsets.pop();\n else previousOffsets.push(original.offset);\n render(page);\n }\n } catch (error) {\n if (op === pageOp && gen === generation) report(error);\n } finally {\n if (op === pageOp) pageBusy = false;\n setControls();\n }\n }\n async function cancelExport() {\n const job = exportJob;\n exportJob = null;\n if (job) await call(\"exportCancel\", { jobId: job }).catch(() => {\n });\n }\n async function exportCsv() {\n if (!current || exportBusy) return;\n const gen = generation;\n exportBusy = true;\n setControls();\n status.textContent = \"Choose a CSV destination\\u2026\";\n try {\n const start2 = await call(\"exportBegin\", {\n resultId: current.resultId,\n suggestedName: \"query-results.csv\",\n spreadsheetSafe: input(\"safe\").checked\n });\n if (gen !== generation) {\n await call(\"exportCancel\", { jobId: start2.jobId }).catch(() => {\n });\n return;\n }\n exportJob = start2.jobId;\n setControls();\n for (let polls = 0; polls < 180 && exportJob === start2.jobId; polls++) {\n await new Promise((resolve) => setTimeout(resolve, 350));\n if (gen !== generation) return;\n const job = await call(\"exportStatus\", { jobId: start2.jobId });\n if (gen !== generation) return;\n if (job.state === \"saved\" || job.state === \"canceled\") {\n status.textContent = job.state === \"saved\" ? `Saved ${job.name ?? \"CSV\"}.` : \"CSV save canceled.\";\n exportJob = null;\n return;\n }\n if (job.state === \"failed\")\n throw new Error(job.message ?? \"CSV export failed\");\n if (job.state !== \"choosing\" && job.state !== \"writing\")\n throw new Error(\"Unknown CSV export state\");\n }\n throw new Error(\"CSV save timed out\");\n } catch (error) {\n if (gen === generation) {\n await cancelExport();\n report(error);\n }\n } finally {\n exportBusy = false;\n setControls();\n }\n }\n async function refresh(force = false) {\n if (closed || !client) return;\n const op = ++bindOp;\n try {\n const env = await client.environment.get();\n if (op !== bindOp || closed) return;\n applyAppearance(document.documentElement, env.appearance);\n const systemMethods = await client.services.list();\n if (op !== bindOp || closed) return;\n companionAvailable = systemMethods.some(\n (method) => method.name === \"system.companion.connect\" && method.available && method.granted\n );\n const own = companionAvailable ? await client.call(\"system.companion.status\") : { connected: false, binding: null };\n if (op !== bindOp || closed || connectController) return;\n const methods = own.connected ? await client.call(\n \"system.companion.list\"\n ) : systemMethods;\n if (op !== bindOp || closed) return;\n const routes = companionAvailable ? await client.call(\"system.companion.routes\").catch(() => ({ ssh: false })) : { ssh: false };\n if (op !== bindOp || closed) return;\n sshAvailable = routes.ssh;\n sshHostName = env.host?.name ?? \"connected host\";\n updateRoute();\n const next = JSON.stringify([\n own.connected,\n own.binding,\n own.connected ? null : env.binding,\n own.connected ? null : env.connection,\n methods.filter((m) => m.name.startsWith(service + \".\")).map((m) => [m.name, m.available, m.granted, m.source])\n ]);\n if (!force && signature === next) return;\n generation++;\n schemaOp++;\n pageOp++;\n pageBusy = false;\n ready = false;\n queryController?.abort();\n queryController = null;\n await cancelExport();\n if (op !== bindOp || closed) return;\n privateConnection = own.connected;\n clearResult(\"Run a query to see results.\", false);\n const state = connectionState(env, methods, own.connected);\n if (!state.ready) {\n signature = next;\n unavailable(state.title, state.detail);\n return;\n }\n $(\"connection-state\").textContent = \"Checking database\\u2026\";\n const info = await call(\"capabilities\");\n if (op !== bindOp || closed) return;\n signature = next;\n provider = info.provider;\n ready = true;\n $(\"setup\").hidden = true;\n $(\"workspace\").hidden = false;\n $(\"connection-state\").textContent = \"Connected\";\n $(\"connection-state\").classList.add(\"connected\");\n $(\"destination\").textContent = privateConnection ? destination || `${provider} \\xB7 app connection` : `${provider} \\xB7 ${env.host?.name ?? \"workspace\"}`;\n button(\"connection-settings\").textContent = \"Connection\\u2026\";\n status.textContent = \"Ready. Choose a table or write a query.\";\n setControls();\n await loadSchemas();\n } catch (error) {\n if (op === bindOp && !closed) {\n signature = null;\n unavailable(\"Connection unavailable\", message(error));\n }\n }\n }\n function updateRoute() {\n const remote = input(\"db-route\").value === \"ssh\";\n $(\"route-hint\").textContent = remote ? sshAvailable ? \"Through \" + sshHostName + \". 127.0.0.1 means that remote host.\" : \"Connect an SSH host first. If it was reconnected, accept the new connection.\" : \"Direct from this PC. 127.0.0.1 means this PC.\";\n }\n function selectProvider() {\n const sqlite = input(\"db-provider\").value === \"sqlite\";\n $(\"network-fields\").hidden = sqlite;\n $(\"sqlite-fields\").hidden = !sqlite;\n for (const id of [\n \"db-route\",\n \"db-host\",\n \"db-port\",\n \"db-name\",\n \"db-user\",\n \"db-password\",\n \"db-tls\",\n \"db-trust\"\n ])\n input(id).disabled = sqlite;\n input(\"db-path\").disabled = !sqlite;\n input(\"db-trust\").disabled = sqlite || !input(\"db-tls\").checked;\n for (const id of [\"db-host\", \"db-port\", \"db-name\", \"db-user\"])\n input(id).required = !sqlite;\n input(\"db-path\").required = sqlite;\n input(\"db-port\").value = String(\n {\n sqlserver: 1433,\n postgresql: 5432,\n mysql: 3306,\n mariadb: 3306\n }[input(\"db-provider\").value] ?? 0\n );\n }\n function openConnection() {\n if (!companionAvailable) return;\n $(\"connect-error\").hidden = true;\n if (!input(\"db-host\").value) input(\"db-host\").value = \"127.0.0.1\";\n updateRoute();\n dialog.showModal();\n $(\"db-provider\").focus();\n }\n function closeConnection() {\n connectController?.abort();\n input(\"db-password\").value = \"\";\n dialog.close();\n }\n async function connect() {\n if (connectController || !$(\"connect-form\").reportValidity())\n return;\n const route = input(\"db-provider\").value === \"sqlite\" ? \"direct\" : input(\"db-route\").value;\n if (route === \"ssh\" && !sshAvailable) {\n $(\"connect-error\").textContent = \"Connect an SSH host first and allow host TCP access when reviewing this app.\";\n $(\"connect-error\").hidden = false;\n return;\n }\n const active = new AbortController();\n connectController = active;\n setControls();\n $(\"connection-fields\").disabled = true;\n button(\"connect-submit\").disabled = true;\n button(\"connect-submit\").textContent = \"Connecting\\u2026\";\n $(\"connect-error\").hidden = true;\n const selected = input(\"db-provider\").value;\n const configuration = selected === \"sqlite\" ? { provider: selected, sqlitePath: input(\"db-path\").value.trim() } : {\n provider: selected,\n host: input(\"db-host\").value.trim(),\n port: Number(input(\"db-port\").value),\n database: input(\"db-name\").value.trim(),\n username: input(\"db-user\").value.trim(),\n password: input(\"db-password\").value,\n tls: input(\"db-tls\").checked,\n trustServerCertificate: input(\"db-tls\").checked && input(\"db-trust\").checked\n };\n const target = selected === \"sqlite\" ? input(\"db-path\").value.trim() : `${configuration.host}:${configuration.port} / ${configuration.database}`;\n status.textContent = `Connecting to ${target}${route === \"ssh\" ? ` through ${sshHostName}` : \" from this PC\"}\\u2026`;\n try {\n await client.call(\n \"system.companion.connect\",\n { configuration, route },\n active.signal\n );\n destination = `${$(\"db-provider\").selectedOptions[0].text} \\xB7 ${target} \\xB7 ${route === \"ssh\" ? sshHostName : \"this PC\"}`;\n input(\"db-password\").value = \"\";\n dialog.close();\n } catch (error) {\n if (!active.signal.aborted) {\n $(\"connect-error\").textContent = message(error);\n $(\"connect-error\").hidden = false;\n }\n status.textContent = active.signal.aborted ? \"Connection canceled.\" : `Could not connect to ${target}.`;\n } finally {\n connectController = null;\n $(\"connection-fields\").disabled = false;\n button(\"connect-submit\").disabled = false;\n button(\"connect-submit\").textContent = \"Connect\";\n await refresh(true);\n }\n }\n async function start() {\n client = await connectToShellCanvas();\n client.events.subscribe((batch) => {\n for (const event of batch.events)\n if (event.topic === \"system.environment\")\n applyAppearance(\n document.documentElement,\n event.value?.appearance\n );\n if (batch.events.some(\n (event) => [\"system.environment\", \"system.services\", \"system.companion\"].includes(\n event.topic\n )\n ))\n void refresh();\n }, report);\n window.addEventListener(\n \"pagehide\",\n () => {\n closed = true;\n generation++;\n bindOp++;\n queryController?.abort();\n connectController?.abort();\n client.dispose();\n },\n { once: true }\n );\n await refresh();\n }\n button(\"run\").onclick = () => void execute();\n button(\"cancel\").onclick = () => queryController?.abort();\n button(\"reload\").onclick = () => void loadSchemas();\n button(\"export\").onclick = () => void exportCsv();\n button(\"prev\").onclick = () => {\n const offset = previousOffsets.at(-1);\n if (offset !== void 0) void move(offset, true);\n };\n button(\"next\").onclick = () => {\n if (current) void move(nextOffset(current));\n };\n button(\"retry\").onclick = () => void refresh(true);\n button(\"connection-settings\").onclick = openConnection;\n button(\"setup-connect\").onclick = openConnection;\n button(\"close-dialog\").onclick = closeConnection;\n button(\"connect-cancel\").onclick = closeConnection;\n dialog.addEventListener(\"cancel\", (event) => {\n event.preventDefault();\n closeConnection();\n });\n button(\"connect-submit\").onclick = () => void connect();\n $(\"connect-form\").addEventListener(\"submit\", (event) => event.preventDefault());\n $(\"connect-form\").addEventListener(\"keydown\", (event) => {\n if (event.key === \"Enter\" && !event.isComposing && !event.ctrlKey && !event.metaKey && !event.altKey && event.target instanceof HTMLInputElement && event.target.type !== \"checkbox\") {\n event.preventDefault();\n void connect();\n }\n });\n $(\"db-provider\").addEventListener(\"change\", selectProvider);\n $(\"db-route\").addEventListener(\"change\", updateRoute);\n input(\"db-tls\").addEventListener(\"change\", () => {\n input(\"db-trust\").disabled = !input(\"db-tls\").checked;\n });\n button(\"disconnect\").onclick = () => {\n void (async () => {\n queryController?.abort();\n await cancelExport();\n await client.call(\"system.companion.disconnect\");\n destination = \"\";\n await refresh(true);\n })().catch(report);\n };\n sql.addEventListener(\"keydown\", (event) => {\n if ((event.ctrlKey || event.metaKey) && event.key === \"Enter\") {\n event.preventDefault();\n void execute();\n }\n });\n selectProvider();\n setControls();\n void start().catch(\n (error) => unavailable(\"ShellCanvas connection unavailable\", message(error))\n );\n})();\n","style":"/* SPDX-License-Identifier: MPL-2.0 */\r\n:root {\r\n --sc-base: #19191c;\r\n --sc-surface: #232326;\r\n --sc-raised: #303034;\r\n --sc-inset: #1d1d20;\r\n --sc-text: #e8e8ec;\r\n --sc-muted: #aaaab4;\r\n --sc-accent: #7ab8ff;\r\n --sc-onAccent: #14283e;\r\n --sc-border: #44444b;\r\n --sc-hover: #38383e;\r\n --sc-selection: #293f58;\r\n --sc-danger: #f2a4a4;\r\n --sc-dangerSoft: #482d32;\r\n --sc-warning: #eec778;\r\n --sc-success: #88cda5;\r\n font-family: \"Segoe UI\", system-ui, sans-serif;\r\n font-size: 13px;\r\n color-scheme: dark;\r\n}\r\n* {\r\n box-sizing: border-box;\r\n}\r\n[hidden] {\r\n display: none !important;\r\n}\r\nhtml,\r\nbody {\r\n margin: 0;\r\n height: 100%;\r\n background: var(--sc-surface);\r\n color: var(--sc-text);\r\n font:\r\n 13px/1.5 \"Segoe UI\",\r\n system-ui,\r\n sans-serif;\r\n}\r\nbutton,\r\ninput,\r\nselect,\r\ntextarea {\r\n font: inherit;\r\n}\r\nbutton {\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n gap: 7px;\r\n min-height: 32px;\r\n padding: 6px 12px;\r\n border: 1px solid var(--sc-border);\r\n border-radius: 6px;\r\n background: transparent;\r\n color: var(--sc-text);\r\n cursor: pointer;\r\n white-space: nowrap;\r\n}\r\nbutton:hover:not(:disabled) {\r\n background: var(--sc-hover);\r\n}\r\nbutton:disabled {\r\n opacity: 0.45;\r\n cursor: default;\r\n}\r\nbutton.primary {\r\n background: var(--sc-accent);\r\n border-color: var(--sc-accent);\r\n color: var(--sc-onAccent);\r\n font-weight: 600;\r\n}\r\nbutton.primary:hover:not(:disabled) {\r\n filter: brightness(1.08);\r\n background: var(--sc-accent);\r\n}\r\n:focus-visible {\r\n outline: 2px solid var(--sc-accent);\r\n outline-offset: 2px;\r\n}\r\nmain {\r\n height: 100vh;\r\n display: flex;\r\n flex-direction: column;\r\n overflow: hidden;\r\n}\r\nh1,\r\nh2,\r\np {\r\n margin: 0;\r\n}\r\nh1 {\r\n font-size: 15px;\r\n font-weight: 600;\r\n}\r\nh2 {\r\n font-size: 13px;\r\n font-weight: 600;\r\n}\r\nsvg {\r\n width: 22px;\r\n height: 22px;\r\n flex-shrink: 0;\r\n}\r\n.app-header {\r\n display: flex;\r\n gap: 20px;\r\n align-items: center;\r\n justify-content: space-between;\r\n min-height: 76px;\r\n padding: 14px 20px;\r\n border-bottom: 1px solid var(--sc-border);\r\n}\r\n.app-heading,\r\n.header-actions {\r\n display: flex;\r\n align-items: center;\r\n gap: 12px;\r\n min-width: 0;\r\n}\r\n.app-heading p {\r\n color: var(--sc-muted);\r\n font-size: 12px;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n max-width: 50vw;\r\n}\r\n.app-icon {\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n color: var(--sc-accent);\r\n width: 38px;\r\n height: 38px;\r\n border-radius: 10px;\r\n background: var(--sc-selection);\r\n flex-shrink: 0;\r\n}\r\n.connection-state {\r\n font-size: 12px;\r\n color: var(--sc-muted);\r\n white-space: nowrap;\r\n}\r\n.connection-state::before {\r\n content: \"\";\r\n display: inline-block;\r\n width: 6px;\r\n height: 6px;\r\n margin-right: 7px;\r\n border-radius: 50%;\r\n background: currentColor;\r\n}\r\n.connection-state.connected {\r\n color: var(--sc-success);\r\n}\r\n.setup {\r\n display: grid;\r\n place-items: center;\r\n flex: 1;\r\n overflow: auto;\r\n padding: 28px;\r\n}\r\n.setup-card {\r\n text-align: center;\r\n max-width: 490px;\r\n}\r\n.setup-icon {\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n width: 64px;\r\n height: 64px;\r\n border: 1px solid var(--sc-border);\r\n border-radius: 16px;\r\n color: var(--sc-accent);\r\n background: var(--sc-raised);\r\n margin-bottom: 20px;\r\n}\r\n.setup-icon svg {\r\n width: 32px;\r\n height: 32px;\r\n}\r\n.setup h2 {\r\n font-size: 23px;\r\n font-weight: 600;\r\n margin-bottom: 10px;\r\n}\r\n.setup p {\r\n color: var(--sc-muted);\r\n line-height: 1.7;\r\n}\r\n.provider-list {\r\n display: flex;\r\n flex-wrap: wrap;\r\n gap: 8px;\r\n justify-content: center;\r\n margin: 22px 0;\r\n}\r\n.provider-list span {\r\n font-size: 11px;\r\n border: 1px solid var(--sc-border);\r\n padding: 4px 9px;\r\n border-radius: 5px;\r\n color: var(--sc-muted);\r\n}\r\n.setup button {\r\n margin: 0 3px;\r\n}\r\n.setup .setup-note {\r\n font-size: 12px;\r\n margin: 24px auto 0;\r\n max-width: 360px;\r\n}\r\n.workspace {\r\n display: grid;\r\n grid-template-columns: 210px minmax(0, 1fr);\r\n min-height: 0;\r\n flex: 1;\r\n}\r\naside {\r\n background: var(--sc-inset);\r\n border-right: 1px solid var(--sc-border);\r\n padding: 16px 12px;\r\n overflow: auto;\r\n}\r\n.side-head {\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-between;\r\n gap: 10px;\r\n padding: 0 6px 12px;\r\n}\r\n.side-head h2 {\r\n color: var(--sc-muted);\r\n font-size: 11px;\r\n text-transform: uppercase;\r\n letter-spacing: 0.08em;\r\n}\r\n.icon-button {\r\n padding: 0;\r\n min-height: 28px;\r\n width: 28px;\r\n font-size: 18px;\r\n border-color: transparent;\r\n}\r\n#tree {\r\n color: var(--sc-muted);\r\n font-size: 12px;\r\n}\r\ndetails {\r\n margin: 4px 0;\r\n}\r\nsummary {\r\n padding: 5px 6px;\r\n cursor: pointer;\r\n color: var(--sc-text);\r\n}\r\ndetails p {\r\n padding: 6px 16px;\r\n}\r\n.table {\r\n display: block;\r\n border: 0;\r\n width: 100%;\r\n text-align: left;\r\n min-height: 30px;\r\n padding: 5px 12px 5px 25px;\r\n font-size: 12px;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n}\r\n.work {\r\n padding: 18px;\r\n display: flex;\r\n flex-direction: column;\r\n gap: 10px;\r\n min-width: 0;\r\n min-height: 0;\r\n}\r\n.editor-heading {\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-between;\r\n font-size: 12px;\r\n}\r\n.editor-heading span {\r\n font-size: 11px;\r\n color: var(--sc-muted);\r\n}\r\ntextarea {\r\n width: 100%;\r\n min-height: 90px;\r\n height: 150px;\r\n max-height: 50vh;\r\n flex-shrink: 0;\r\n resize: vertical;\r\n border: 1px solid var(--sc-border);\r\n border-radius: 6px;\r\n background: var(--sc-inset);\r\n color: var(--sc-text);\r\n padding: 12px 14px;\r\n font:\r\n 13px/1.7 Consolas,\r\n \"Cascadia Code\",\r\n monospace;\r\n tab-size: 2;\r\n}\r\n.toolbar,\r\n.result-head,\r\n.result-head > div,\r\n.pager {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n}\r\n.query-note {\r\n margin-left: auto;\r\n color: var(--sc-muted);\r\n font-size: 11px;\r\n}\r\n.result-head {\r\n justify-content: space-between;\r\n margin-top: 10px;\r\n}\r\n.safe {\r\n font-size: 11px;\r\n color: var(--sc-muted);\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 5px;\r\n}\r\ninput[type=\"checkbox\"] {\r\n accent-color: var(--sc-accent);\r\n}\r\n.grid-wrap {\r\n overflow: auto;\r\n min-height: 100px;\r\n flex: 1;\r\n border: 1px solid var(--sc-border);\r\n border-radius: 6px;\r\n background: var(--sc-inset);\r\n}\r\ntable {\r\n border-collapse: separate;\r\n border-spacing: 0;\r\n min-width: 100%;\r\n font:\r\n 12px/1.5 Consolas,\r\n monospace;\r\n}\r\nth,\r\ntd {\r\n padding: 8px 12px;\r\n border-right: 1px solid var(--sc-border);\r\n border-bottom: 1px solid var(--sc-border);\r\n white-space: pre;\r\n max-width: 420px;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n text-align: left;\r\n}\r\nth {\r\n position: sticky;\r\n top: 0;\r\n background: var(--sc-raised);\r\n color: var(--sc-muted);\r\n font-weight: 500;\r\n}\r\ntr:hover td {\r\n background: var(--sc-hover);\r\n}\r\n.cell-null {\r\n color: var(--sc-muted);\r\n font-style: italic;\r\n}\r\n.cell-binary {\r\n color: var(--sc-accent);\r\n}\r\n.cell-unsupported,\r\n#warnings {\r\n color: var(--sc-warning);\r\n}\r\n#warnings {\r\n font-size: 12px;\r\n}\r\n#empty {\r\n padding: 28px;\r\n text-align: center;\r\n color: var(--sc-muted);\r\n font-size: 12px;\r\n}\r\n.pager {\r\n justify-content: flex-end;\r\n font-size: 11px;\r\n color: var(--sc-muted);\r\n}\r\n.pager #page {\r\n margin-right: auto;\r\n}\r\n.pager button {\r\n font-size: 11px;\r\n min-height: 28px;\r\n padding: 4px 10px;\r\n}\r\nfooter {\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-between;\r\n gap: 12px;\r\n min-height: 32px;\r\n padding: 6px 16px;\r\n border-top: 1px solid var(--sc-border);\r\n color: var(--sc-muted);\r\n font-size: 11px;\r\n}\r\nfooter > :first-child {\r\n overflow-wrap: anywhere;\r\n}\r\nfooter > :last-child {\r\n white-space: nowrap;\r\n}\r\ndialog {\r\n color: var(--sc-text);\r\n background: var(--sc-surface);\r\n border: 1px solid var(--sc-border);\r\n border-radius: 14px;\r\n padding: 26px;\r\n width: min(520px, calc(100vw - 32px));\r\n max-height: calc(100vh - 32px);\r\n overflow: auto;\r\n box-shadow: 0 20px 70px #0006;\r\n}\r\ndialog::backdrop {\r\n background: #0007;\r\n}\r\n.dialog-heading {\r\n display: flex;\r\n justify-content: space-between;\r\n align-items: center;\r\n margin-bottom: 14px;\r\n}\r\ndialog h2 {\r\n font-size: 23px;\r\n margin-bottom: 8px;\r\n}\r\ndialog p {\r\n color: var(--sc-muted);\r\n font-size: 12px;\r\n line-height: 1.6;\r\n}\r\nfieldset {\r\n border: 0;\r\n margin: 18px 0 0;\r\n padding: 0;\r\n min-width: 0;\r\n}\r\nfieldset label {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 6px;\r\n font-size: 12px;\r\n margin-bottom: 12px;\r\n}\r\nfieldset input:not([type=\"checkbox\"]),\r\nselect {\r\n width: 100%;\r\n min-width: 0;\r\n padding: 9px 10px;\r\n border: 1px solid var(--sc-border);\r\n border-radius: 6px;\r\n background: var(--sc-raised);\r\n color: var(--sc-text);\r\n}\r\n.field-row {\r\n display: flex;\r\n gap: 12px;\r\n}\r\n.field-row label {\r\n flex: 1;\r\n min-width: 0;\r\n}\r\n.field-row .port-field {\r\n flex: 0 0 100px;\r\n}\r\nfieldset .checkbox {\r\n flex-direction: row;\r\n align-items: center;\r\n gap: 7px;\r\n margin: 10px 0;\r\n}\r\n.field-hint {\r\n margin: 10px 0;\r\n}\r\n.dialog-actions {\r\n display: flex;\r\n gap: 8px;\r\n align-items: center;\r\n margin-top: 20px;\r\n}\r\n.dialog-actions span {\r\n margin-right: auto;\r\n font-size: 11px;\r\n color: var(--sc-muted);\r\n}\r\n.error {\r\n margin-top: 14px;\r\n padding: 10px 12px;\r\n border-radius: 6px;\r\n background: var(--sc-dangerSoft);\r\n color: var(--sc-danger);\r\n}\r\n@media (max-width: 700px) {\r\n .app-header {\r\n padding: 12px;\r\n gap: 10px;\r\n }\r\n .connection-state {\r\n display: none;\r\n }\r\n .workspace {\r\n grid-template-columns: 155px minmax(0, 1fr);\r\n }\r\n .work {\r\n padding: 12px;\r\n }\r\n .query-note {\r\n display: none;\r\n }\r\n .result-head {\r\n align-items: flex-start;\r\n gap: 8px;\r\n flex-wrap: wrap;\r\n }\r\n .safe {\r\n font-size: 10px;\r\n }\r\n footer > :last-child {\r\n display: none;\r\n }\r\n}\r\n@media (max-width: 480px) {\r\n .workspace {\r\n grid-template-columns: 1fr;\r\n }\r\n aside {\r\n max-height: 145px;\r\n border-right: 0;\r\n border-bottom: 1px solid var(--sc-border);\r\n }\r\n .app-heading p {\r\n max-width: 40vw;\r\n }\r\n .app-header .app-icon {\r\n display: none;\r\n }\r\n .field-row {\r\n flex-wrap: wrap;\r\n }\r\n}\r\n"} \ No newline at end of file diff --git a/app/icon.svg b/app/icon.svg new file mode 100644 index 0000000..7028289 --- /dev/null +++ b/app/icon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/app/shellcanvas.json b/app/shellcanvas.json index b749023..29e7e94 100644 --- a/app/shellcanvas.json +++ b/app/shellcanvas.json @@ -5,6 +5,7 @@ "id": "dev.shellcanvas.database", "version": "0.1.1", "title": "ShellCanvas Database", + "icon": "icon.svg", "description": "Browse database schemas, run SQL, inspect typed results and export CSV.", "permissions": [ "services.dev.shellcanvas.database", diff --git a/shellcanvas.repo.json b/shellcanvas.repo.json index 4360aa2..555c91e 100644 --- a/shellcanvas.repo.json +++ b/shellcanvas.repo.json @@ -7,7 +7,7 @@ "description": "Query SQL Server, PostgreSQL, MySQL/MariaDB and SQLite from ShellCanvas.", "package": { "path": "app/dist/app.shellcanvas.json", - "sha256": "465c74b45a479cc82ecd2a741cac0ab604c69ee0692c66c2febe1e9cde618189" + "sha256": "528dbc86301055b66af9218eba4f93c3aa63b41d4c4497a11a2d34fa5d79095b" }, "nativeAdapter": { "id": "dev.shellcanvas.database", @@ -21,17 +21,17 @@ { "platform": "linux-x86_64", "path": "dist/adapter-linux-x86_64/adapter.json", - "sha256": "ea17f8b67da947072bbec0e28b57d39961865192d37cbfb580852b0f2da9a75d" + "sha256": "7c80efe6ec0078cbc1efef2fd5bf085c6f5dcaa88f8da2bfcf1f28f034c4257c" }, { "platform": "macos-x86_64", "path": "dist/adapter-macos-x86_64/adapter.json", - "sha256": "7c7886446f49e2b136b7e85d618ef177c52858296f32fac8975e6c78721fa2c8" + "sha256": "781e574a3f47c9e29eb88e8c15acbafaf638d80ec6cae890ef567067cf2f7a38" }, { "platform": "macos-aarch64", "path": "dist/adapter-macos-aarch64/adapter.json", - "sha256": "07b92bd4b0681ece3acd4db73f74e1e5638902324ae5e359106b2138cd861339" + "sha256": "bf394caa54a8a55e8aedd2a3ca49619d44dd386dc27cd7979654c1415586538d" } ] }