4cb568c326
Use .venv/bin/python3 by default, with PYTHON_PATH as an override, and update the setup script and docs to match the new virtualenv-based workflow.
110 lines
2.8 KiB
TypeScript
110 lines
2.8 KiB
TypeScript
import { spawn } from "node:child_process";
|
|
import { fileURLToPath } from "node:url";
|
|
import type {
|
|
KeePassCommand,
|
|
KeePassEntry,
|
|
KeePassFindQuery,
|
|
KeePassGroup,
|
|
KeePassOpenOptions,
|
|
KeePassResponse,
|
|
} from "./types";
|
|
|
|
export class KeePassDatabase {
|
|
constructor(
|
|
private readonly path: string,
|
|
private readonly options: KeePassOpenOptions,
|
|
private readonly pythonPath = process.env.PYTHON_PATH ?? ".venv/bin/python3",
|
|
private readonly bridgePath = new URL("./python/bridge.py", import.meta.url)
|
|
) {}
|
|
|
|
async listEntries(): Promise<KeePassEntry[]> {
|
|
const response = await this.run<KeePassEntry[]>({ command: "list-entries" });
|
|
return response;
|
|
}
|
|
|
|
async findEntries(query: KeePassFindQuery): Promise<KeePassEntry[]> {
|
|
const response = await this.run<KeePassEntry[]>({ command: "find-entries", query });
|
|
return response;
|
|
}
|
|
|
|
async listGroups(): Promise<KeePassGroup[]> {
|
|
const response = await this.run<KeePassGroup[]>({ command: "list-groups" });
|
|
return response;
|
|
}
|
|
|
|
async close(): Promise<void> {
|
|
// No persistent process is kept alive yet.
|
|
return;
|
|
}
|
|
|
|
private async run<T>(command: KeePassCommand): Promise<T> {
|
|
const payload = JSON.stringify({
|
|
...command,
|
|
path: this.path,
|
|
password: this.options.password,
|
|
keyFile: this.options.keyFile,
|
|
});
|
|
|
|
const bridgeFile = fileURLToPath(this.bridgePath);
|
|
const result = await new Promise<{ stdout: string; stderr: string; code: number }>((resolve, reject) => {
|
|
const child = spawn(this.pythonPath, [bridgeFile], {
|
|
stdio: ["pipe", "pipe", "pipe"],
|
|
});
|
|
|
|
let stdout = "";
|
|
let stderr = "";
|
|
let settled = false;
|
|
|
|
try {
|
|
child.stdin.write(payload);
|
|
child.stdin.end();
|
|
} catch (error) {
|
|
reject(error);
|
|
}
|
|
|
|
child.stdout.on("data", (chunk) => {
|
|
stdout += chunk.toString();
|
|
});
|
|
|
|
child.stderr.on("data", (chunk) => {
|
|
stderr += chunk.toString();
|
|
});
|
|
|
|
child.on("error", (error) => {
|
|
if (!settled) {
|
|
settled = true;
|
|
reject(error);
|
|
}
|
|
});
|
|
|
|
child.on("close", (code) => {
|
|
if (!settled) {
|
|
settled = true;
|
|
resolve({ stdout, stderr, code: code ?? 1 });
|
|
}
|
|
});
|
|
});
|
|
|
|
const output = result.stdout.trim();
|
|
if (!output) {
|
|
throw new Error(result.stderr || `Empty response from Python bridge (exit code ${result.code})`);
|
|
}
|
|
|
|
let parsed: KeePassResponse<T>;
|
|
try {
|
|
parsed = JSON.parse(output) as KeePassResponse<T>;
|
|
} catch (error) {
|
|
throw new Error(`Invalid JSON from Python bridge: ${error instanceof Error ? error.message : String(error)}`);
|
|
}
|
|
|
|
if (!parsed.ok) {
|
|
throw new Error(parsed.error || result.stderr || `KeePass bridge error (exit code ${result.code})`);
|
|
}
|
|
|
|
return parsed.data;
|
|
}
|
|
}
|
|
|
|
export function openKeePassDatabase(path: string, options: KeePassOpenOptions) {
|
|
return new KeePassDatabase(path, options);
|
|
} |