mirror of
https://github.com/Nystik-gh/ignis.git
synced 2026-06-17 04:35:53 +00:00
headless-sync vault setup
This commit is contained in:
@@ -1,29 +1,38 @@
|
||||
const { spawn, execSync } = require("child_process");
|
||||
const os = require("os");
|
||||
|
||||
const isWindows = process.platform === "win32";
|
||||
|
||||
function checkInstalled() {
|
||||
try {
|
||||
const output = execSync("ob --version", { stdio: "pipe" }).toString().trim();
|
||||
const output = execSync("ob --version", {
|
||||
stdio: "pipe",
|
||||
windowsHide: true,
|
||||
})
|
||||
.toString()
|
||||
.trim();
|
||||
|
||||
return { installed: true, version: output || "unknown" };
|
||||
} catch {
|
||||
return { installed: false, version: null };
|
||||
}
|
||||
}
|
||||
|
||||
function spawnOb(args, opts = {}) {
|
||||
return spawn("ob", args, {
|
||||
env: { ...process.env, HOME: os.homedir() },
|
||||
shell: isWindows,
|
||||
windowsHide: true,
|
||||
...opts,
|
||||
});
|
||||
}
|
||||
|
||||
function runCommand(args, opts = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const spawnOpts = {
|
||||
env: { ...process.env, HOME: os.homedir() },
|
||||
};
|
||||
|
||||
if (opts.cwd) {
|
||||
spawnOpts.cwd = opts.cwd;
|
||||
}
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
|
||||
const proc = spawn("ob", args, spawnOpts);
|
||||
const proc = spawnOb(args, opts);
|
||||
|
||||
proc.stdout.on("data", (data) => {
|
||||
stdout += data.toString();
|
||||
@@ -49,4 +58,4 @@ function runCommand(args, opts = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { checkInstalled, runCommand };
|
||||
module.exports = { checkInstalled, spawnOb, runCommand };
|
||||
|
||||
@@ -39,6 +39,10 @@ function setupSync(vaultId, remoteVault, opts = {}) {
|
||||
return post("/setup", { vaultId, remoteVault, ...opts });
|
||||
}
|
||||
|
||||
function createRemoteVault(name, encryption, password, region) {
|
||||
return post("/create-remote-vault", { name, encryption, password, region });
|
||||
}
|
||||
|
||||
function startSync(vaultId) {
|
||||
return post("/start", { vaultId });
|
||||
}
|
||||
@@ -47,6 +51,10 @@ function stopSync(vaultId) {
|
||||
return post("/stop", { vaultId });
|
||||
}
|
||||
|
||||
function unlinkVault(vaultId) {
|
||||
return post("/unlink", { vaultId });
|
||||
}
|
||||
|
||||
function getVaults() {
|
||||
return fetchJson("/vaults");
|
||||
}
|
||||
@@ -61,8 +69,10 @@ module.exports = {
|
||||
logout,
|
||||
getRemoteVaults,
|
||||
setupSync,
|
||||
createRemoteVault,
|
||||
startSync,
|
||||
stopSync,
|
||||
unlinkVault,
|
||||
getVaults,
|
||||
getLogs,
|
||||
};
|
||||
|
||||
@@ -12,8 +12,6 @@ class HeadlessSyncSettingTab extends PluginSettingTab {
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
|
||||
containerEl.createEl("h2", { text: "Headless Sync" });
|
||||
|
||||
let serverStatus;
|
||||
|
||||
try {
|
||||
@@ -35,10 +33,7 @@ class HeadlessSyncSettingTab extends PluginSettingTab {
|
||||
}
|
||||
|
||||
this.renderAuthSection(containerEl, serverStatus);
|
||||
|
||||
if (serverStatus.authenticated) {
|
||||
await this.renderSyncSection(containerEl);
|
||||
}
|
||||
await this.renderSyncSection(containerEl, serverStatus.authenticated);
|
||||
}
|
||||
|
||||
renderAuthSection(containerEl, serverStatus) {
|
||||
@@ -52,18 +47,17 @@ class HeadlessSyncSettingTab extends PluginSettingTab {
|
||||
`Signed in as ${serverStatus.name || "unknown"} (${serverStatus.email || "unknown"})`,
|
||||
)
|
||||
.addButton((btn) => {
|
||||
btn
|
||||
.setButtonText("Disconnect")
|
||||
.setWarning()
|
||||
.onClick(async () => {
|
||||
try {
|
||||
await api.logout();
|
||||
new Notice("Disconnected from Headless Sync");
|
||||
this.display();
|
||||
} catch (e) {
|
||||
new Notice(`Failed to disconnect: ${e.message}`);
|
||||
}
|
||||
});
|
||||
btn.setButtonText("Disconnect");
|
||||
btn.buttonEl.addClass("mod-destructive");
|
||||
btn.onClick(async () => {
|
||||
try {
|
||||
await api.logout();
|
||||
new Notice("Disconnected from Headless Sync");
|
||||
this.display();
|
||||
} catch (e) {
|
||||
new Notice(`Failed to disconnect: ${e.message}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
} else if (localToken) {
|
||||
// State B: signed into Obsidian, not connected to server
|
||||
@@ -111,7 +105,21 @@ class HeadlessSyncSettingTab extends PluginSettingTab {
|
||||
}
|
||||
}
|
||||
|
||||
async renderSyncSection(containerEl) {
|
||||
async renderSyncSection(containerEl, authenticated) {
|
||||
containerEl.createEl("h3", { text: "Vault sync" });
|
||||
|
||||
if (!authenticated) {
|
||||
new Setting(containerEl)
|
||||
.setName("Sync not configured")
|
||||
.setDesc("Sign in to your Obsidian Sync account to set up sync.")
|
||||
.addButton((btn) => {
|
||||
btn.setButtonText("Set up sync");
|
||||
btn.buttonEl.disabled = true;
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const vaultId = this.app.vault.getName();
|
||||
|
||||
let vaultsData;
|
||||
@@ -128,8 +136,6 @@ class HeadlessSyncSettingTab extends PluginSettingTab {
|
||||
|
||||
const vaultState = vaultsData.vaults.find((v) => v.vaultId === vaultId);
|
||||
|
||||
containerEl.createEl("h3", { text: "Vault sync" });
|
||||
|
||||
if (!vaultState) {
|
||||
new Setting(containerEl)
|
||||
.setName("Sync not configured")
|
||||
@@ -139,7 +145,30 @@ class HeadlessSyncSettingTab extends PluginSettingTab {
|
||||
.setButtonText("Set up sync")
|
||||
.setCta()
|
||||
.onClick(() => {
|
||||
new Notice("Vault picker coming soon.");
|
||||
const scope = this.app.setting.scope;
|
||||
const prevFocusContainer = scope.tabFocusContainerEl;
|
||||
scope.tabFocusContainerEl = null;
|
||||
|
||||
const cleanup = () => {
|
||||
scope.tabFocusContainerEl = prevFocusContainer;
|
||||
};
|
||||
|
||||
const modal = new window.IgnisUI.SyncSetupModal({
|
||||
target: document.body,
|
||||
props: {
|
||||
vaultId,
|
||||
onSuccess: () => {
|
||||
cleanup();
|
||||
modal.$destroy();
|
||||
this.display();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
modal.$on("close", () => {
|
||||
cleanup();
|
||||
modal.$destroy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -149,7 +178,20 @@ class HeadlessSyncSettingTab extends PluginSettingTab {
|
||||
// Show current sync config
|
||||
new Setting(containerEl)
|
||||
.setName("Remote vault")
|
||||
.setDesc(vaultState.remoteVault || "unknown");
|
||||
.setDesc(vaultState.remoteVaultName || vaultState.remoteVault || "unknown")
|
||||
.addButton((btn) => {
|
||||
btn.setButtonText("Unlink");
|
||||
btn.buttonEl.addClass("mod-destructive");
|
||||
btn.onClick(async () => {
|
||||
try {
|
||||
await api.unlinkVault(vaultId);
|
||||
new Notice("Vault unlinked");
|
||||
this.display();
|
||||
} catch (e) {
|
||||
new Notice(`Failed to unlink: ${e.message}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Sync mode")
|
||||
@@ -168,7 +210,9 @@ class HeadlessSyncSettingTab extends PluginSettingTab {
|
||||
.setDesc(statusText)
|
||||
.addButton((btn) => {
|
||||
if (vaultState.status === "running") {
|
||||
btn.setButtonText("Stop sync").setWarning().onClick(async () => {
|
||||
btn.setButtonText("Stop sync");
|
||||
btn.buttonEl.addClass("mod-destructive");
|
||||
btn.onClick(async () => {
|
||||
try {
|
||||
await api.stopSync(vaultId);
|
||||
new Notice("Sync stopped");
|
||||
|
||||
40
server/plugins/headless-sync/plugin/styles.css
Normal file
40
server/plugins/headless-sync/plugin/styles.css
Normal file
@@ -0,0 +1,40 @@
|
||||
.ignis-vault-list {
|
||||
margin: 8px 0 16px;
|
||||
}
|
||||
|
||||
.ignis-vault-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-s);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.ignis-vault-row-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ignis-vault-row-name {
|
||||
font-weight: var(--font-semibold);
|
||||
font-size: var(--font-ui-medium);
|
||||
}
|
||||
|
||||
.ignis-vault-row-region {
|
||||
font-size: var(--font-ui-small);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.ignis-vault-connect-options {
|
||||
padding: 8px 16px 16px;
|
||||
margin-bottom: 8px;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-top: none;
|
||||
border-radius: 0 0 var(--radius-s) var(--radius-s);
|
||||
margin-top: -8px;
|
||||
}
|
||||
|
||||
.ignis-vault-connect-options .setting-item {
|
||||
border-top: none;
|
||||
}
|
||||
@@ -49,7 +49,7 @@ function mountRoutes(router, plugin) {
|
||||
router.post("/setup", async (req, res) => {
|
||||
const ctx = plugin.getCtx();
|
||||
const syncManager = plugin.getSyncManager();
|
||||
const { vaultId, remoteVault, vaultPassword, deviceName, mode } = req.body;
|
||||
const { vaultId, remoteVault, remoteVaultName, vaultPassword, deviceName, mode } = req.body;
|
||||
|
||||
if (!vaultId || !remoteVault) {
|
||||
return res.status(400).json({ error: "vaultId and remoteVault are required" });
|
||||
@@ -67,6 +67,7 @@ function mountRoutes(router, plugin) {
|
||||
|
||||
try {
|
||||
const state = await syncManager.setupSync(vaultId, vaultPath, remoteVault, {
|
||||
remoteVaultName,
|
||||
vaultPassword,
|
||||
deviceName,
|
||||
mode,
|
||||
@@ -115,6 +116,24 @@ function mountRoutes(router, plugin) {
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/unlink", (req, res) => {
|
||||
const ctx = plugin.getCtx();
|
||||
const syncManager = plugin.getSyncManager();
|
||||
const { vaultId } = req.body;
|
||||
|
||||
if (!vaultId) {
|
||||
return res.status(400).json({ error: "vaultId is required" });
|
||||
}
|
||||
|
||||
try {
|
||||
syncManager.unlinkVault(vaultId);
|
||||
res.json({ success: true });
|
||||
} catch (e) {
|
||||
ctx.log(`Failed to unlink vault: ${e.message}`);
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/logs", (req, res) => {
|
||||
const syncManager = plugin.getSyncManager();
|
||||
const { vaultId, limit } = req.query;
|
||||
@@ -132,6 +151,42 @@ function mountRoutes(router, plugin) {
|
||||
res.json({ vaults: syncManager.getAllStates() });
|
||||
});
|
||||
|
||||
router.post("/create-remote-vault", async (req, res) => {
|
||||
const ctx = plugin.getCtx();
|
||||
const { name, encryption, password, region } = req.body;
|
||||
|
||||
if (!name) {
|
||||
return res.status(400).json({ error: "name is required" });
|
||||
}
|
||||
|
||||
if (!auth.isAuthenticated(ctx.dataDir)) {
|
||||
return res.status(401).json({ error: "Not authenticated" });
|
||||
}
|
||||
|
||||
const args = ["sync-create-remote", "--name", name];
|
||||
|
||||
if (encryption) {
|
||||
args.push("--encryption", encryption);
|
||||
}
|
||||
|
||||
if (password) {
|
||||
args.push("--password", password);
|
||||
}
|
||||
|
||||
if (region) {
|
||||
args.push("--region", region);
|
||||
}
|
||||
|
||||
try {
|
||||
await obCli.runCommand(args);
|
||||
ctx.log(`Created remote vault: ${name}`);
|
||||
res.json({ success: true });
|
||||
} catch (e) {
|
||||
ctx.log(`Failed to create remote vault: ${e.message}`);
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/remote-vaults", async (req, res) => {
|
||||
const ctx = plugin.getCtx();
|
||||
|
||||
@@ -162,10 +217,10 @@ function parseRemoteVaults(stdout) {
|
||||
}
|
||||
|
||||
// Format: [vaultId] "[vaultName]" ([region])
|
||||
const match = trimmed.match(/^([a-f0-9]+)\s+"([^"]+)"/);
|
||||
const match = trimmed.match(/^([a-f0-9]+)\s+"([^"]+)"\s+\(([^)]+)\)/);
|
||||
|
||||
if (match) {
|
||||
vaults.push({ id: match[1], name: match[2] });
|
||||
vaults.push({ id: match[1], name: match[2], region: match[3] });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
const { spawn } = require("child_process");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const os = require("os");
|
||||
const { spawnOb } = require("./ob-cli");
|
||||
|
||||
const MAX_LOG_ENTRIES = 200;
|
||||
|
||||
@@ -28,6 +27,7 @@ class SyncManager {
|
||||
vaultId: entry.vaultId,
|
||||
vaultPath,
|
||||
remoteVault: entry.remoteVault,
|
||||
remoteVaultName: entry.remoteVaultName || null,
|
||||
status: "stopped",
|
||||
pid: null,
|
||||
lastActivity: new Date().toISOString(),
|
||||
@@ -56,6 +56,7 @@ class SyncManager {
|
||||
vaultId: state.vaultId,
|
||||
vaultPath: state.vaultPath,
|
||||
remoteVault: state.remoteVault,
|
||||
remoteVaultName: state.remoteVaultName,
|
||||
config: state.config,
|
||||
autoStart: state.autoStart,
|
||||
});
|
||||
@@ -83,6 +84,7 @@ class SyncManager {
|
||||
vaultId,
|
||||
vaultPath,
|
||||
remoteVault,
|
||||
remoteVaultName: options.remoteVaultName || null,
|
||||
status: "stopped",
|
||||
pid: null,
|
||||
lastActivity: new Date().toISOString(),
|
||||
@@ -123,10 +125,7 @@ class SyncManager {
|
||||
args.push("--mirror-remote");
|
||||
}
|
||||
|
||||
const proc = spawn("ob", args, {
|
||||
cwd: state.vaultPath,
|
||||
env: { ...process.env, HOME: os.homedir() },
|
||||
});
|
||||
const proc = spawnOb(args, { cwd: state.vaultPath });
|
||||
|
||||
state.status = "running";
|
||||
state.pid = proc.pid;
|
||||
@@ -214,6 +213,22 @@ class SyncManager {
|
||||
return this.getState(vaultId);
|
||||
}
|
||||
|
||||
unlinkVault(vaultId) {
|
||||
const state = this.states.get(vaultId);
|
||||
|
||||
if (!state) {
|
||||
throw new Error(`No sync configuration for vault: ${vaultId}`);
|
||||
}
|
||||
|
||||
if (state._process) {
|
||||
state._process.kill("SIGTERM");
|
||||
}
|
||||
|
||||
this.states.delete(vaultId);
|
||||
this.saveStates();
|
||||
this.ctx.log(`Unlinked vault ${vaultId}`);
|
||||
}
|
||||
|
||||
getState(vaultId) {
|
||||
const state = this.states.get(vaultId);
|
||||
|
||||
@@ -224,6 +239,7 @@ class SyncManager {
|
||||
return {
|
||||
vaultId: state.vaultId,
|
||||
remoteVault: state.remoteVault,
|
||||
remoteVaultName: state.remoteVaultName,
|
||||
status: state.status,
|
||||
pid: state.pid,
|
||||
lastActivity: state.lastActivity,
|
||||
|
||||
Reference in New Issue
Block a user