Redo connection manager: Stacks, More options, drag & drop, WinSCP import
Folders become Stacks (connections.json v2, no v1 migration): collapsible stack-first list, open-stack-in-tabs (first reuses a pristine tab), drag connections between stacks. More... reveals a per-connection Local directory applied to the other panel when local and untouched. Import reads a WinSCP.ini export incl. deobfuscated passwords. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,13 +1,14 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { ConnFolder, ConnectionsFile, SavedConnection } from "../../shared/types";
|
||||
import type { ConnectionsFile, SavedConnection, Stack } from "../../shared/types";
|
||||
import { appDataDir } from "./paths";
|
||||
|
||||
const EMPTY: ConnectionsFile = { version: 1, folders: [], connections: [] };
|
||||
const EMPTY: ConnectionsFile = { version: 2, stacks: [], connections: [] };
|
||||
|
||||
/**
|
||||
* Plain-JSON store for saved connections (passwords included — the user's
|
||||
* explicit v1 choice). Kept at 0600 and written atomically via tmp+rename.
|
||||
* Only the current version 2 format is read; anything else starts empty.
|
||||
*/
|
||||
export class ConnectionStore {
|
||||
private readonly file = path.join(appDataDir(), "connections.json");
|
||||
@@ -16,7 +17,8 @@ export class ConnectionStore {
|
||||
async load(): Promise<ConnectionsFile> {
|
||||
if (!this.data) {
|
||||
try {
|
||||
this.data = JSON.parse(await fs.readFile(this.file, "utf8")) as ConnectionsFile;
|
||||
const parsed = JSON.parse(await fs.readFile(this.file, "utf8")) as ConnectionsFile;
|
||||
this.data = parsed.version === 2 ? parsed : structuredClone(EMPTY);
|
||||
} catch {
|
||||
this.data = structuredClone(EMPTY);
|
||||
}
|
||||
@@ -40,22 +42,22 @@ export class ConnectionStore {
|
||||
await this.save();
|
||||
}
|
||||
|
||||
async upsertFolder(folder: ConnFolder): Promise<string> {
|
||||
async upsertStack(stack: Stack): Promise<string> {
|
||||
const data = await this.load();
|
||||
const saved = { ...folder, id: folder.id || crypto.randomUUID() };
|
||||
const index = data.folders.findIndex((f) => f.id === saved.id);
|
||||
if (index >= 0) data.folders[index] = saved;
|
||||
else data.folders.push(saved);
|
||||
const saved = { ...stack, id: stack.id || crypto.randomUUID() };
|
||||
const index = data.stacks.findIndex((s) => s.id === saved.id);
|
||||
if (index >= 0) data.stacks[index] = saved;
|
||||
else data.stacks.push(saved);
|
||||
await this.save();
|
||||
return saved.id;
|
||||
}
|
||||
|
||||
/** Deleting a folder moves its connections to the root. */
|
||||
async deleteFolder(id: string): Promise<void> {
|
||||
/** Deleting a stack moves its connections to the loose list. */
|
||||
async deleteStack(id: string): Promise<void> {
|
||||
const data = await this.load();
|
||||
data.folders = data.folders.filter((f) => f.id !== id);
|
||||
data.stacks = data.stacks.filter((s) => s.id !== id);
|
||||
for (const connection of data.connections) {
|
||||
if (connection.folderId === id) connection.folderId = null;
|
||||
if (connection.stackId === id) connection.stackId = null;
|
||||
}
|
||||
await this.save();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user