Merge branch 'main' into filewatcher

This commit is contained in:
Nystik
2026-03-24 01:09:54 +01:00
29 changed files with 1468 additions and 283 deletions

View File

@@ -48,6 +48,8 @@ export const vaultService = {
body: JSON.stringify({ name }),
});
this._setVaultTrust(name);
return this.listVaults();
},
@@ -121,6 +123,10 @@ export const vaultService = {
target.location.href = "/?vault=" + encodeURIComponent(id);
},
_setVaultTrust(vaultId, trusted = true) {
localStorage.setItem("enable-plugin-" + vaultId, String(trusted));
},
_migrateLocalStorage(oldId, newId) {
const pluginKey = "enable-plugin-";

View File

@@ -1,9 +1,11 @@
import { randomBytes } from "./random-bytes.js";
import { createHash } from "./create-hash.js";
import { scrypt } from "./scrypt.js";
import { randomUUID } from "./random-uuid.js";
export const cryptoShim = {
randomBytes,
createHash,
scrypt,
randomUUID,
};

View File

@@ -0,0 +1,3 @@
export function randomUUID() {
return crypto.randomUUID();
}

47
src/shims/debug.js Normal file
View File

@@ -0,0 +1,47 @@
const DEBUG = true;
const _accessLog = new Map(); // "module.property" -> count
export function wrapWithProxy(obj, name) {
if (!DEBUG || !obj || typeof obj !== "object") {
return obj;
}
return new Proxy(obj, {
get(target, prop) {
if (
typeof prop === "string" &&
prop !== "then" &&
prop !== "toJSON" &&
!prop.startsWith("_")
) {
const key = `${name}.${prop}`;
_accessLog.set(key, (_accessLog.get(key) || 0) + 1);
if (!(prop in target)) {
console.warn(`[shim:MISS] ${key} - property not found on shim`);
}
}
return target[prop];
},
});
}
export function installDebugHelpers(rawRegistry) {
window.__shimLog = function () {
const sorted = [..._accessLog.entries()].sort((a, b) => b[1] - a[1]);
console.table(sorted.map(([k, v]) => ({ api: k, calls: v })));
};
window.__shimMisses = function () {
const sorted = [..._accessLog.entries()]
.filter(([k]) => {
const [mod, prop] = k.split(".");
const shim = rawRegistry[mod];
return shim && !(prop in shim);
})
.sort((a, b) => b[1] - a[1]);
console.table(sorted.map(([k, v]) => ({ api: k, calls: v })));
};
}

View File

@@ -3,12 +3,158 @@ import {
showConfirmDialog,
showPromptDialog,
} from "../../../ui/bootstrap.js";
import { transport } from "../../fs/transport.js";
const IMPORTS_DIR = ".obsidian/imports";
let stagedFiles = [];
function buildAcceptString(filters) {
if (!filters || filters.length === 0) {
return "";
}
const extensions = filters.flatMap((f) => f.extensions || []);
if (extensions.includes("*")) {
return "";
}
return extensions.map((ext) => "." + ext).join(",");
}
function pickFiles(accept, multiple) {
return new Promise((resolve) => {
const input = document.createElement("input");
input.type = "file";
input.multiple = multiple;
input.style.display = "none";
if (accept) {
input.accept = accept;
}
input.addEventListener("change", () => {
const files = Array.from(input.files || []);
input.remove();
resolve(files);
});
// User closed the picker without selecting
input.addEventListener("cancel", () => {
input.remove();
resolve([]);
});
document.body.appendChild(input);
input.click();
});
}
async function uploadToImports(file) {
const arrayBuffer = await file.arrayBuffer();
const bytes = new Uint8Array(arrayBuffer);
const targetPath = IMPORTS_DIR + "/" + file.name;
await transport.writeFile(targetPath, bytes);
return "/" + targetPath;
}
async function startWorkaroundFlow(options) {
const properties = options?.properties || [];
const multiple = properties.includes("multiSelections");
const accept = buildAcceptString(options?.filters);
const files = await pickFiles(accept, multiple);
if (files.length === 0) {
return;
}
const paths = [];
for (const file of files) {
const vaultPath = await uploadToImports(file);
paths.push(vaultPath);
}
stagedFiles = paths;
const names = paths.map((p) => p.split("/").pop()).join(", ");
console.log("[shim:dialog] Files staged for next sync call:", paths);
await showMessageDialog(
"Files Ready",
`Uploaded: ${names}\n\nPlease retry the action that brought you here. ` +
"The files will be provided automatically.",
);
}
export const dialogShim = {
async showOpenDialog(browserWindow, options) {
// TODO: implement custom modal with server-side file listing
console.log("[shim:dialog] showOpenDialog (stub):", options);
return { canceled: true, filePaths: [] };
if (typeof browserWindow === "object" && !options) {
options = browserWindow;
}
const properties = options?.properties || [];
const multiple = properties.includes("multiSelections");
const accept = buildAcceptString(options?.filters);
console.log("[shim:dialog] showOpenDialog - opening browser file picker");
const files = await pickFiles(accept, multiple);
if (files.length === 0) {
return { canceled: true, filePaths: [] };
}
const filePaths = [];
for (const file of files) {
const vaultPath = await uploadToImports(file);
filePaths.push(vaultPath);
}
console.log("[shim:dialog] showOpenDialog - uploaded:", filePaths);
return { canceled: false, filePaths };
},
showOpenDialogSync(browserWindow, options) {
if (typeof browserWindow === "object" && !options) {
options = browserWindow;
}
// If files were staged from a previous workaround, return them immediately
if (stagedFiles.length > 0) {
const paths = stagedFiles;
stagedFiles = [];
console.log(
"[shim:dialog] showOpenDialogSync - returning staged files:",
paths,
);
return paths;
}
console.warn(
"[shim:dialog] showOpenDialogSync requires workaround in browser context",
);
// Fire-and-forget: show warning, then optionally start workaround flow
showConfirmDialog(
"Feature Not Available",
"This action requires a native file picker which is not available in the browser.",
"A workaround is available: upload your file first, then retry the action. " +
"Would you like to proceed?",
"Upload File",
).then((confirmed) => {
if (confirmed) {
startWorkaroundFlow(options);
}
});
return undefined;
},
async showSaveDialog(browserWindow, options) {

153
src/shims/fs/fd.js Normal file
View File

@@ -0,0 +1,153 @@
// File descriptor shim - maps fake integer fds to in-memory file buffers.
// Enables libraries like yauzl that use fs.open/fs.read/fs.close to seek
// around files without loading them via readFileSync upfront.
let nextFd = 100;
const openFiles = new Map();
export function createFdOps(metadataCache, contentCache, transport) {
function ensureData(path) {
const cached = contentCache.get(path);
if (cached !== null) {
if (typeof cached === "string") {
return new TextEncoder().encode(cached);
}
return cached;
}
// Synchronous fetch fallback
console.warn("[shim:fs] fd open cache miss, using sync XHR:", path);
const data = transport.readFileSync(path);
contentCache.set(path, data);
return data;
}
function getEntry(fd) {
const entry = openFiles.get(fd);
if (!entry) {
const err = new Error(`EBADF: bad file descriptor, fd ${fd}`);
err.code = "EBADF";
throw err;
}
return entry;
}
// --- Sync ---
function openSync(path, flags, mode) {
if (!metadataCache.has(path)) {
const err = new Error(
`ENOENT: no such file or directory, open '${path}'`,
);
err.code = "ENOENT";
throw err;
}
const data = ensureData(path);
const fd = nextFd++;
openFiles.set(fd, { path, data });
return fd;
}
function readSync(fd, buffer, offset, length, position) {
const entry = getEntry(fd);
const available = Math.min(length, entry.data.length - position);
if (available <= 0) {
return 0;
}
const slice = entry.data.subarray(position, position + available);
buffer.set(slice, offset);
return available;
}
function closeSync(fd) {
openFiles.delete(fd);
}
function fstatSync(fd) {
const entry = getEntry(fd);
const stat = metadataCache.toStat(entry.path);
if (stat) {
return stat;
}
// Fallback: construct minimal stat from the buffer
return {
size: entry.data.length,
isFile: () => true,
isDirectory: () => false,
};
}
// --- Async (callback style) ---
function open(path, flags, modeOrCb, cb) {
if (typeof modeOrCb === "function") {
cb = modeOrCb;
}
try {
const fd = openSync(path, flags);
queueMicrotask(() => cb(null, fd));
} catch (e) {
queueMicrotask(() => cb(e));
}
}
function read(fd, buffer, offset, length, position, cb) {
try {
const bytesRead = readSync(fd, buffer, offset, length, position);
queueMicrotask(() => cb(null, bytesRead, buffer));
} catch (e) {
queueMicrotask(() => cb(e));
}
}
function close(fd, cb) {
try {
closeSync(fd);
if (cb) {
queueMicrotask(() => cb(null));
}
} catch (e) {
if (cb) {
queueMicrotask(() => cb(e));
}
}
}
function fstat(fd, optionsOrCb, cb) {
if (typeof optionsOrCb === "function") {
cb = optionsOrCb;
}
try {
const stat = fstatSync(fd);
queueMicrotask(() => cb(null, stat));
} catch (e) {
queueMicrotask(() => cb(e));
}
}
return {
openSync,
readSync,
closeSync,
fstatSync,
open,
read,
close,
fstat,
};
}

View File

@@ -5,6 +5,7 @@ import { createFsPromises } from "./promises.js";
import { createFsSync } from "./sync.js";
import { createFsWatch } from "./watch.js";
import { createWatcherClient } from "./watcher-client.js";
import { createFdOps } from "./fd.js";
import { constants } from "./constants.js";
const metadataCache = new MetadataCache();
@@ -14,6 +15,7 @@ const fsPromises = createFsPromises(metadataCache, contentCache, transport);
const fsSync = createFsSync(metadataCache, contentCache, transport);
const fsWatch = createFsWatch(transport);
const watcherClient = createWatcherClient(metadataCache, contentCache, fsWatch);
const fdOps = createFdOps(metadataCache, contentCache, transport);
export const fsShim = {
promises: fsPromises,
@@ -26,6 +28,15 @@ export const fsShim = {
statSync: fsSync.statSync,
readdirSync: fsSync.readdirSync,
open: fdOps.open,
openSync: fdOps.openSync,
read: fdOps.read,
readSync: fdOps.readSync,
close: fdOps.close,
closeSync: fdOps.closeSync,
fstat: fdOps.fstat,
fstatSync: fdOps.fstatSync,
watch: fsWatch.watch,
constants,

View File

@@ -208,5 +208,48 @@ export function createFsPromises(metadataCache, contentCache, transport) {
metadataCache.set(path, meta);
}
},
async open(path, flags) {
if (!metadataCache.has(path)) {
const err = new Error(
`ENOENT: no such file or directory, open '${path}'`,
);
err.code = "ENOENT";
throw err;
}
const data = await this.readFile(path);
const fileData =
typeof data === "string" ? new TextEncoder().encode(data) : data;
const fileStat = metadataCache.toStat(path) || {
size: fileData.length,
isFile: () => true,
isDirectory: () => false,
};
return {
async stat() {
return fileStat;
},
async read(buffer, offset, length, position) {
const available = Math.min(length, fileData.length - position);
if (available <= 0) {
return { bytesRead: 0, buffer };
}
const slice = fileData.subarray(position, position + available);
buffer.set(slice, offset);
return { bytesRead: available, buffer };
},
async close() {
// Nothing to clean up - data is in memory
},
};
},
};
}

275
src/shims/globals.js Normal file
View File

@@ -0,0 +1,275 @@
import { processShim } from "./process.js";
import {
registerPopupWindow,
unregisterPopupWindow,
} from "./electron/remote/window.js";
import { showVaultManager } from "../ui/bootstrap.js";
function installProcess() {
window.process = processShim;
}
function installBuffer() {
if (typeof window.Buffer !== "undefined") return;
window.Buffer = {
from: function (data, encoding) {
if (typeof data === "string") {
return new TextEncoder().encode(data);
}
if (data instanceof ArrayBuffer) {
return new Uint8Array(data);
}
return new Uint8Array(data);
},
alloc: function (size, fill, encoding) {
const buf = new Uint8Array(size);
if (fill !== undefined) {
buf.fill(typeof fill === "string" ? fill.charCodeAt(0) : fill);
}
return buf;
},
allocUnsafe: function (size) {
return new Uint8Array(size);
},
concat: function (arrays) {
const total = arrays.reduce((sum, a) => sum + a.length, 0);
const result = new Uint8Array(total);
let offset = 0;
for (const arr of arrays) {
result.set(arr, offset);
offset += arr.length;
}
return result;
},
isBuffer: function (obj) {
return obj instanceof Uint8Array;
},
byteLength: function (str, encoding) {
return new TextEncoder().encode(str).length;
},
isEncoding: function (encoding) {
return [
"utf8",
"utf-8",
"ascii",
"binary",
"base64",
"hex",
"latin1",
].includes((encoding || "").toLowerCase());
},
};
}
function installWindowClose() {
window.close = function () {
console.log("[ignis] window.close() blocked");
if (!window.__vaultConfig) {
showVaultManager();
}
};
}
function installWindowOpen() {
window.__popupIframe = null;
const _originalOpen = window.open;
window.open = function (url, target, features) {
if (url === "about:blank" || (features && features.includes("popup"))) {
console.log("[ignis] intercepted popup:", url, features);
registerPopupWindow();
const iframe = document.createElement("iframe");
iframe.style.cssText =
"position:fixed;left:-9999px;width:0;height:0;border:none;";
document.body.appendChild(iframe);
window.__popupIframe = iframe;
const iframeWin = iframe.contentWindow;
iframeWin.require = window.require;
iframeWin.module = window.module;
iframeWin.Buffer = window.Buffer;
iframeWin.process = window.process;
iframeWin.global = iframeWin;
iframeWin.globalEnhance = window.globalEnhance;
iframeWin.close = function () {
unregisterPopupWindow();
iframe.remove();
window.__popupIframe = null;
};
return iframeWin;
}
return _originalOpen.call(window, url, target, features);
};
}
function arrayBufferToBase64(buf) {
const bytes = new Uint8Array(buf);
let binary = "";
const chunk = 8192;
for (let i = 0; i < bytes.length; i += chunk) {
binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk));
}
return btoa(binary);
}
function base64ToArrayBuffer(base64) {
const binary = atob(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes.buffer;
}
function isSameOrigin(url) {
if (
!url ||
url.startsWith("/") ||
url.startsWith("./") ||
url.startsWith("../")
) {
return true;
}
if (url.startsWith("data:") || url.startsWith("blob:")) {
return true;
}
try {
const parsed = new URL(url, window.location.origin);
return parsed.origin === window.location.origin;
} catch {
return true;
}
}
function installFetchShim() {
const originalFetch = window.fetch.bind(window);
window.__originalFetch = originalFetch;
window.fetch = async function (input, init) {
let url;
if (typeof input === "string") {
url = input;
} else if (input instanceof URL) {
url = input.href;
} else if (input instanceof Request) {
url = input.url;
} else {
url = String(input);
}
if (isSameOrigin(url)) {
return originalFetch(input, init);
}
// Cross-origin - route through server proxy
const method = (
init?.method || (input instanceof Request ? input.method : "GET")
).toUpperCase();
const headers = {};
if (init?.headers) {
const h =
init.headers instanceof Headers
? init.headers
: new Headers(init.headers);
h.forEach((val, key) => {
headers[key] = val;
});
} else if (input instanceof Request) {
input.headers.forEach((val, key) => {
headers[key] = val;
});
}
// Mimic the real Obsidian desktop app headers for cross-origin requests
if (!headers["user-agent"] && !headers["User-Agent"]) {
headers["user-agent"] = navigator.userAgent;
}
if (!headers["origin"] && !headers["Origin"]) {
headers["origin"] = "app://obsidian.md";
}
let body = null;
let binary = false;
if (init?.body && method !== "GET" && method !== "HEAD") {
if (typeof init.body === "string") {
body = init.body;
} else if (init.body instanceof ArrayBuffer) {
body = arrayBufferToBase64(init.body);
binary = true;
} else if (init.body instanceof Uint8Array) {
body = arrayBufferToBase64(init.body.buffer);
binary = true;
} else if (typeof init.body === "object") {
body = JSON.stringify(init.body);
} else {
body = String(init.body);
}
}
console.log("[shim:fetch] Proxying cross-origin:", method, url);
const proxyRes = await originalFetch("/api/proxy", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url, method, headers, body, binary }),
});
if (!proxyRes.ok) {
const err = await proxyRes
.json()
.catch(() => ({ error: "Proxy request failed" }));
throw new TypeError(err.error || "Failed to fetch");
}
const result = await proxyRes.json();
const respBody = base64ToArrayBuffer(result.body);
return new Response(respBody, {
status: result.status,
headers: result.headers,
});
};
}
function installContextMenuFix() {
// hacky fix to prevent browser from showing context menu while allowing obsidian context menu
window.addEventListener(
"contextmenu",
(e) => {
e.preventDefault();
Object.defineProperty(e, "defaultPrevented", { get: () => false });
},
true,
);
}
export function installGlobals() {
installProcess();
installBuffer();
installFetchShim();
installWindowClose();
installWindowOpen();
installContextMenuFix();
}

117
src/shims/init.js Normal file
View File

@@ -0,0 +1,117 @@
import { fsShim } from "./fs/index.js";
import { installRequestUrlShim } from "./request-url.js";
import { vaultService } from "../services/vault-service.js";
import { showPluginInstallDialog } from "../ui/bootstrap.js";
function resolveVaultId() {
const urlParams = new URLSearchParams(window.location.search);
window.__currentVaultId =
urlParams.get("vault") || localStorage.getItem("last-vault") || "";
}
function initVaultConfig() {
try {
const vaultParam = window.__currentVaultId
? "?vault=" + encodeURIComponent(window.__currentVaultId)
: "";
const xhr = new XMLHttpRequest();
xhr.open("GET", "/api/vault/info" + vaultParam, false);
xhr.send();
if (xhr.status === 200) {
const info = JSON.parse(xhr.responseText);
window.__currentVaultId = info.id;
localStorage.setItem("last-vault", info.id);
window.__obsidianVersion = info.version || "0.0.0";
window.__vaultConfig = {
id: info.id,
path: "/",
};
window.__ignisPlugin = info.ignisPlugin || null;
console.log("[ignis] Vault:", window.__vaultConfig);
console.log("[ignis] Obsidian version:", window.__obsidianVersion);
} else {
console.warn("[ignis] No vault found, will show manager");
}
} catch (e) {
console.error("[ignis] Failed to fetch vault config:", e);
}
}
function initVaultList() {
try {
vaultService.listVaultsSync();
} catch (e) {
window.__vaultList = [];
}
}
function initMetadataCache() {
try {
const vaultParam = window.__currentVaultId
? "?vault=" + encodeURIComponent(window.__currentVaultId)
: "";
const xhr = new XMLHttpRequest();
xhr.open("GET", "/api/fs/tree" + vaultParam, false);
xhr.send();
if (xhr.status === 200) {
const tree = JSON.parse(xhr.responseText);
fsShim._metadataCache.populate(tree);
fsShim._metadataCache.set("", { type: "directory" });
fsShim._metadataCache.set("/", { type: "directory" });
console.log(
"[ignis] Metadata cache populated:",
fsShim._metadataCache.size,
"entries",
);
} else {
console.error("[ignis] Failed to fetch metadata tree:", xhr.status);
}
} catch (e) {
console.error("[ignis] Failed to init metadata cache:", e);
}
}
function initPluginPrompt() {
if (
!window.__ignisPlugin ||
window.__ignisPlugin.installed ||
window.__ignisPlugin.prompted
) {
return;
}
const vaultId = window.__currentVaultId;
const observer = new MutationObserver(() => {
if (document.querySelector(".workspace")) {
observer.disconnect();
showPluginInstallDialog(vaultId);
}
});
observer.observe(document.documentElement, {
childList: true,
subtree: true,
});
}
export function initialize() {
resolveVaultId();
initVaultConfig();
initVaultList();
initMetadataCache();
installRequestUrlShim();
initPluginPrompt();
}

View File

@@ -1,263 +1,10 @@
import { electronShim } from "./electron/index.js";
import { remoteShim } from "./electron/remote/index.js";
import { fsShim } from "./fs/index.js";
import { pathShim } from "./path.js";
import { urlShim } from "./url.js";
import { cryptoShim } from "./crypto/index.js";
import { processShim } from "./process.js";
import { installRequestUrlShim } from "./request-url.js";
import {
registerPopupWindow,
unregisterPopupWindow,
} from "./electron/remote/window.js";
import * as childProcessShim from "./node/child_process.js";
import * as eventsShim from "./node/events.js";
import * as osShim from "./node/os.js";
import * as netShim from "./node/net.js";
import * as httpShim from "./node/http.js";
import { vaultService } from "../services/vault-service.js";
import { installRequire } from "./require.js";
import { installGlobals } from "./globals.js";
import { initialize } from "./init.js";
const DEBUG = true;
const _accessLog = new Map(); // "module.property" -> count
function wrapWithProxy(obj, name) {
if (!DEBUG || !obj || typeof obj !== "object") {
return obj;
}
return new Proxy(obj, {
get(target, prop) {
if (
typeof prop === "string" &&
prop !== "then" &&
prop !== "toJSON" &&
!prop.startsWith("_")
) {
const key = `${name}.${prop}`;
_accessLog.set(key, (_accessLog.get(key) || 0) + 1);
if (!(prop in target)) {
console.warn(`[shim:MISS] ${key} - property not found on shim`);
}
}
return target[prop];
},
});
}
window.__shimLog = function () {
const sorted = [..._accessLog.entries()].sort((a, b) => b[1] - a[1]);
console.table(sorted.map(([k, v]) => ({ api: k, calls: v })));
};
window.__shimMisses = function () {
const sorted = [..._accessLog.entries()]
.filter(([k]) => {
const [mod, prop] = k.split(".");
const shim = rawRegistry[mod];
return shim && !(prop in shim);
})
.sort((a, b) => b[1] - a[1]);
console.table(sorted.map(([k, v]) => ({ api: k, calls: v })));
};
const rawRegistry = {
electron: electronShim,
"@electron/remote": remoteShim,
"original-fs": fsShim,
fs: fsShim,
path: pathShim,
url: urlShim,
crypto: cryptoShim,
child_process: childProcessShim,
events: eventsShim,
os: osShim,
net: netShim,
http: httpShim,
https: httpShim,
};
const shimRegistry = {};
for (const [name, shim] of Object.entries(rawRegistry)) {
shimRegistry[name] = wrapWithProxy(shim, name);
}
const throwOnRequire = new Set(["btime", "get-fonts", "vibrancy-win"]);
window.require = function (moduleName) {
if (throwOnRequire.has(moduleName)) {
throw new Error(`Cannot find module '${moduleName}'`);
}
if (shimRegistry[moduleName]) {
return shimRegistry[moduleName];
}
console.warn("[ignis] Unshimmed require:", moduleName);
return wrapWithProxy({}, `UNKNOWN(${moduleName})`);
};
window.process = processShim;
if (typeof window.Buffer === "undefined") {
window.Buffer = {
from: function (data, encoding) {
if (typeof data === "string") {
return new TextEncoder().encode(data);
}
if (data instanceof ArrayBuffer) {
return new Uint8Array(data);
}
return new Uint8Array(data);
},
concat: function (arrays) {
const total = arrays.reduce((sum, a) => sum + a.length, 0);
const result = new Uint8Array(total);
let offset = 0;
for (const arr of arrays) {
result.set(arr, offset);
offset += arr.length;
}
return result;
},
isBuffer: function (obj) {
return obj instanceof Uint8Array;
},
};
}
window.close = function () {
console.log("[ignis] window.close() blocked");
};
window.__popupIframe = null;
const _originalOpen = window.open;
window.open = function (url, target, features) {
if (url === "about:blank" || (features && features.includes("popup"))) {
console.log("[ignis] intercepted popup:", url, features);
registerPopupWindow();
const iframe = document.createElement("iframe");
iframe.style.cssText =
"position:fixed;left:-9999px;width:0;height:0;border:none;";
document.body.appendChild(iframe);
window.__popupIframe = iframe;
const iframeWin = iframe.contentWindow;
iframeWin.require = window.require;
iframeWin.module = window.module;
iframeWin.Buffer = window.Buffer;
iframeWin.process = window.process;
iframeWin.global = iframeWin;
iframeWin.globalEnhance = window.globalEnhance;
iframeWin.close = function () {
unregisterPopupWindow();
iframe.remove();
window.__popupIframe = null;
};
return iframeWin;
}
return _originalOpen.call(window, url, target, features);
};
// hacky fix to prevent browser from showing context menu while allowing obsidian context menu
window.addEventListener(
"contextmenu",
(e) => {
e.preventDefault();
Object.defineProperty(e, "defaultPrevented", { get: () => false });
},
true,
);
const _urlParams = new URLSearchParams(window.location.search);
window.__currentVaultId =
_urlParams.get("vault") || localStorage.getItem("last-vault") || "";
(function initVaultConfig() {
try {
const vaultParam = window.__currentVaultId
? "?vault=" + encodeURIComponent(window.__currentVaultId)
: "";
const xhr = new XMLHttpRequest();
xhr.open("GET", "/api/vault/info" + vaultParam, false);
xhr.send();
if (xhr.status === 200) {
const info = JSON.parse(xhr.responseText);
window.__currentVaultId = info.id;
localStorage.setItem("last-vault", info.id);
window.__obsidianVersion = info.version || "0.0.0";
window.__vaultConfig = {
id: info.id,
path: "/",
};
console.log("[ignis] Vault:", window.__vaultConfig);
console.log("[ignis] Obsidian version:", window.__obsidianVersion);
} else {
console.warn("[ignis] No vault found, will show manager");
}
} catch (e) {
console.error("[ignis] Failed to fetch vault config:", e);
}
})();
(function initVaultList() {
try {
vaultService.listVaultsSync();
} catch (e) {
window.__vaultList = [];
}
})();
(function initMetadataCache() {
try {
const vaultParam = window.__currentVaultId
? "?vault=" + encodeURIComponent(window.__currentVaultId)
: "";
const xhr = new XMLHttpRequest();
xhr.open("GET", "/api/fs/tree" + vaultParam, false);
xhr.send();
if (xhr.status === 200) {
const tree = JSON.parse(xhr.responseText);
fsShim._metadataCache.populate(tree);
fsShim._metadataCache.set("", { type: "directory" });
fsShim._metadataCache.set("/", { type: "directory" });
console.log(
"[ignis] Metadata cache populated:",
fsShim._metadataCache.size,
"entries",
);
} else {
console.error("[ignis] Failed to fetch metadata tree:", xhr.status);
}
} catch (e) {
console.error("[ignis] Failed to init metadata cache:", e);
}
})();
installRequestUrlShim();
installGlobals(); // process, Buffer, window overrides (before require so Buffer is available)
installRequire(); // shim registry, window.require
initialize(); // vault config, metadata cache, plugin prompt
// Connect file watcher WebSocket after everything is initialized
if (window.__currentVaultId) {

138
src/shims/node/zlib.js Normal file
View File

@@ -0,0 +1,138 @@
// Zlib shim using pako for browser-side deflate/inflate/gzip/gunzip.
// Implements Node's zlib convenience functions (async callback + sync variants).
// Streaming classes (createDeflate, createGzip, etc.) are NOT implemented yet.
import pako from "pako";
// --- Constants ---
export const constants = {
Z_NO_FLUSH: 0,
Z_PARTIAL_FLUSH: 1,
Z_SYNC_FLUSH: 2,
Z_FULL_FLUSH: 3,
Z_FINISH: 4,
Z_BLOCK: 5,
Z_TREES: 6,
Z_OK: 0,
Z_STREAM_END: 1,
Z_NEED_DICT: 2,
Z_ERRNO: -1,
Z_STREAM_ERROR: -2,
Z_DATA_ERROR: -3,
Z_MEM_ERROR: -4,
Z_BUF_ERROR: -5,
Z_VERSION_ERROR: -6,
Z_NO_COMPRESSION: 0,
Z_BEST_SPEED: 1,
Z_BEST_COMPRESSION: 9,
Z_DEFAULT_COMPRESSION: -1,
Z_FILTERED: 1,
Z_HUFFMAN_ONLY: 2,
Z_RLE: 3,
Z_FIXED: 4,
Z_DEFAULT_STRATEGY: 0,
Z_DEFAULT_WINDOWBITS: 15,
Z_DEFAULT_MEMLEVEL: 8,
};
// --- Helpers ---
function toUint8Array(buf) {
if (buf instanceof Uint8Array) {
return buf;
}
if (typeof buf === "string") {
return new TextEncoder().encode(buf);
}
if (buf instanceof ArrayBuffer) {
return new Uint8Array(buf);
}
if (ArrayBuffer.isView(buf)) {
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
}
return new Uint8Array(buf);
}
function wrapAsync(syncFn) {
return function (buf, optionsOrCb, cb) {
if (typeof optionsOrCb === "function") {
cb = optionsOrCb;
optionsOrCb = {};
}
try {
const result = syncFn(buf, optionsOrCb || {});
if (cb) {
queueMicrotask(() => cb(null, result));
}
} catch (e) {
if (cb) {
queueMicrotask(() => cb(e));
}
}
};
}
// --- Sync functions ---
export function deflateSync(buf, options) {
return pako.deflate(toUint8Array(buf), options);
}
export function inflateSync(buf, options) {
return pako.inflate(toUint8Array(buf), options);
}
export function deflateRawSync(buf, options) {
return pako.deflateRaw(toUint8Array(buf), options);
}
export function inflateRawSync(buf, options) {
return pako.inflateRaw(toUint8Array(buf), options);
}
export function gzipSync(buf, options) {
return pako.gzip(toUint8Array(buf), options);
}
export function gunzipSync(buf, options) {
return pako.ungzip(toUint8Array(buf), options);
}
export function unzipSync(buf, options) {
return pako.ungzip(toUint8Array(buf), options);
}
// --- Async functions (callback style) ---
export const deflate = wrapAsync(deflateSync);
export const inflate = wrapAsync(inflateSync);
export const deflateRaw = wrapAsync(deflateRawSync);
export const inflateRaw = wrapAsync(inflateRawSync);
export const gzip = wrapAsync(gzipSync);
export const gunzip = wrapAsync(gunzipSync);
export const unzip = wrapAsync(unzipSync);
// --- Streaming stubs (not yet implemented) ---
function notImplemented(name) {
return function () {
throw new Error(
`zlib.${name}() streaming is not yet implemented. Use the sync/callback variants instead.`,
);
};
}
export const createDeflate = notImplemented("createDeflate");
export const createInflate = notImplemented("createInflate");
export const createDeflateRaw = notImplemented("createDeflateRaw");
export const createInflateRaw = notImplemented("createInflateRaw");
export const createGzip = notImplemented("createGzip");
export const createGunzip = notImplemented("createGunzip");
export const createUnzip = notImplemented("createUnzip");

67
src/shims/require.js Normal file
View File

@@ -0,0 +1,67 @@
import { electronShim } from "./electron/index.js";
import { remoteShim } from "./electron/remote/index.js";
import { fsShim } from "./fs/index.js";
import { pathShim } from "./path.js";
import { urlShim } from "./url.js";
import { cryptoShim } from "./crypto/index.js";
import * as childProcessShim from "./node/child_process.js";
import * as eventsShim from "./node/events.js";
import * as osShim from "./node/os.js";
import * as netShim from "./node/net.js";
import * as httpShim from "./node/http.js";
import * as zlibShim from "./node/zlib.js";
import { wrapWithProxy, installDebugHelpers } from "./debug.js";
const rawRegistry = {
electron: electronShim,
"@electron/remote": remoteShim,
"original-fs": fsShim,
fs: fsShim,
path: pathShim,
url: urlShim,
crypto: cryptoShim,
child_process: childProcessShim,
events: eventsShim,
os: osShim,
net: netShim,
http: httpShim,
https: httpShim,
zlib: zlibShim,
};
const shimRegistry = {};
const throwOnRequire = new Set(["btime", "get-fonts", "vibrancy-win"]);
export function installRequire() {
for (const [name, shim] of Object.entries(rawRegistry)) {
shimRegistry[name] = wrapWithProxy(shim, name);
}
// Add buffer shim (protobufjs inquire() checks for this)
if (typeof window.Buffer !== "undefined") {
shimRegistry.buffer = window.Buffer;
}
// Add empty long shim (optional protobufjs dependency, gracefully handled)
shimRegistry.long = undefined;
window.require = function (moduleName) {
// Strip node: prefix if present
const normalizedName = moduleName.startsWith("node:")
? moduleName.slice(5)
: moduleName;
if (throwOnRequire.has(normalizedName)) {
throw new Error(`Cannot find module '${moduleName}'`);
}
if (shimRegistry[normalizedName]) {
return shimRegistry[normalizedName];
}
console.warn("[ignis] Unshimmed require:", moduleName);
return wrapWithProxy({}, `UNKNOWN(${moduleName})`);
};
installDebugHelpers(rawRegistry);
}

37
src/ui/bootstrap.js vendored
View File

@@ -1,7 +1,6 @@
import { vaultService } from "../services/vault-service.js";
export function showVaultManager() {
if (!document.querySelector(".workspace")) return;
if (document.querySelector(".vault-manager-overlay")) return;
new window.IgnisUI.VaultManager({
@@ -48,6 +47,42 @@ export function showConfirmDialog(
});
}
export function showPluginInstallDialog(vaultId) {
return new Promise((resolve) => {
const dialog = new window.IgnisUI.PluginInstallDialog({
target: document.body,
});
dialog.$on("install", async () => {
try {
await fetch("/api/vault/install-plugin", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ vault: vaultId }),
});
} catch (e) {
console.error("[ignis] Failed to install plugin:", e);
}
dialog.$destroy();
resolve("install");
});
dialog.$on("dismiss", async () => {
try {
await fetch("/api/vault/install-plugin", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ vault: vaultId, dismiss: true }),
});
} catch (e) {
console.error("[ignis] Failed to dismiss plugin prompt:", e);
}
dialog.$destroy();
resolve("dismiss");
});
});
}
export function showPromptDialog(
title,
label,

View File

@@ -13,8 +13,8 @@
let modalRef;
function onConfirm() {
dispatch("confirm");
modalRef.dismiss();
dispatch("confirm");
}
function onEscape() {

View File

@@ -0,0 +1,89 @@
<script>
import { createEventDispatcher } from "svelte";
import Modal from "./Modal.svelte";
import Button from "../input/Button.svelte";
import { Puzzle, Download, X } from "lucide-svelte";
export let width = "500px";
const dispatch = createEventDispatcher();
let modalRef;
let installing = false;
function onInstall() {
installing = true;
dispatch("install");
}
function onDismiss() {
modalRef.dismiss();
dispatch("dismiss");
}
function onEscape() {
onDismiss();
}
export function dismiss() {
modalRef.dismiss();
}
</script>
<Modal title="Ignis Bridge Plugin" {width} bind:this={modalRef} on:escape={onEscape} closeOnOverlayClick={false}>
<svelte:fragment slot="icon">
<Puzzle size="1.25rem" />
</svelte:fragment>
<div class="dialog-body">
<p class="dialog-message">This vault doesn't have the Ignis Bridge plugin installed.</p>
<p class="dialog-description">
The plugin adds additional functionality such as file uploads.
Obsidian will work without it, but some features will be unavailable.
</p>
</div>
<svelte:fragment slot="footer">
<div class="dialog-footer">
<Button variant="secondary" on:click={onDismiss}>
<svelte:fragment slot="icon">
<X size="0.875rem" />
</svelte:fragment>
Not Now
</Button>
<Button variant="primary" on:click={onInstall} disabled={installing}>
<svelte:fragment slot="icon">
<Download size="0.875rem" />
</svelte:fragment>
{installing ? "Installing..." : "Install Plugin"}
</Button>
</div>
</svelte:fragment>
</Modal>
<style>
.dialog-body {
padding: 1.25rem 1.5rem;
border-bottom: 1px solid var(--background-modifier-border);
}
.dialog-message {
margin: 0 0 0.5rem;
font-size: 1.125rem;
font-weight: 600;
color: var(--text-normal);
}
.dialog-description {
margin: 0;
font-size: 0.875rem;
color: var(--text-muted);
line-height: 1.5;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
}
</style>

View File

@@ -2,3 +2,4 @@ export { default as VaultManager } from "./views/VaultManager.svelte";
export { default as MessageDialog } from "./components/layout/MessageDialog.svelte";
export { default as ConfirmDialog } from "./components/layout/ConfirmDialog.svelte";
export { default as PromptDialog } from "./components/layout/PromptDialog.svelte";
export { default as PluginInstallDialog } from "./components/layout/PluginInstallDialog.svelte";

View File

@@ -30,6 +30,7 @@
let dialogValue = "";
let errorMessage = "";
let pendingReload = false;
let version = "";
const menuItems = [
{ id: "rename", label: "Rename" },
@@ -47,6 +48,16 @@
)
: vaults;
async function fetchVersion() {
try {
const res = await fetch("/api/version");
const data = await res.json();
version = data.version;
} catch (e) {
console.warn("[VaultManager] Failed to fetch version:", e);
}
}
async function refreshVaults() {
try {
vaults = await vaultService.listVaults();
@@ -177,6 +188,7 @@
onMount(() => {
refreshVaults();
fetchVersion();
});
</script>
@@ -246,6 +258,11 @@
</div>
<svelte:fragment slot="footer">
<div class="footer-left">
{#if version}
<span class="version-info">Ignis v{version}</span>
{/if}
</div>
<div class="footer-right">
<Button variant="ghost" on:click={showCreateDialog}>
<svelte:fragment slot="icon">
@@ -400,8 +417,19 @@
white-space: nowrap;
}
.footer-left {
display: flex;
align-items: center;
}
.footer-right {
display: flex;
justify-content: flex-end;
}
.version-info {
font-size: 0.75rem;
color: var(--text-muted);
user-select: none;
}
</style>