import { execFile } from "node:child_process" ;
import { constants as fsConstants } from "node:fs" ;
import fs from "node:fs/promises" ;
import path from "node:path" ;
import { promisify } from "node:util" ;
import type { ResolvedMemoryWikiConfig } from "./config.js" ;
const execFileAsync = promisify(execFile);
export type ObsidianCliProbe = {
available: boolean ;
command: string | null ;
};
export type ObsidianCliResult = {
command: string;
argv: string[];
stdout: string;
stderr: string;
};
type ObsidianCliDeps = {
exec?: typeof execFileAsync;
resolveCommand?: (command: string) => Promise<string | null >;
};
async function isExecutableFile(inputPath: string): Promise<boolean > {
try {
await fs.access(inputPath, process.platform === "win32" ? fsConstants.F_OK : fsConstants.X_OK);
return true ;
} catch {
return false ;
}
}
export async function resolveCommandOnPath(command: string): Promise<string | null > {
const pathValue = process.env.PATH ?? "" ;
const pathEntries = pathValue.split(path.delimiter).filter(Boolean );
const windowsExts =
process.platform === "win32"
? (process.env.PATHEXT?.split(";" ).filter(Boolean ) ?? [".EXE" , ".CMD" , ".BAT" ])
: ["" ];
if (command.includes(path.sep)) {
return (await isExecutableFile(command)) ? command : null ;
}
for (const dir of pathEntries) {
for (const extension of windowsExts) {
const candidate = path.join(dir, extension ? `${command}${extension}` : command);
if (await isExecutableFile(candidate)) {
return candidate;
}
}
}
return null ;
}
function buildVaultPrefix(config: ResolvedMemoryWikiConfig): string[] {
return config.obsidian.vaultName ? [`vault=${config.obsidian.vaultName}`] : [];
}
export async function probeObsidianCli(
deps?: Pick<ObsidianCliDeps, "resolveCommand" >,
): Promise<ObsidianCliProbe> {
const resolveCommand = deps?.resolveCommand ?? resolveCommandOnPath;
const command = await resolveCommand("obsidian" );
return {
available: command !== null ,
command,
};
}
export async function runObsidianCli(params: {
config: ResolvedMemoryWikiConfig;
subcommand: string;
args?: string[];
deps?: ObsidianCliDeps;
}): Promise<ObsidianCliResult> {
const resolveCommand = params.deps?.resolveCommand ?? resolveCommandOnPath;
const exec = params.deps?.exec ?? execFileAsync;
const probe = await probeObsidianCli({ resolveCommand });
if (!probe.command) {
throw new Error("Obsidian CLI is not available on PATH." );
}
const argv = [...buildVaultPrefix(params.config), params.subcommand, ...(params.args ?? [])];
const { stdout, stderr } = await exec(probe.command, argv, { encoding: "utf8" });
return {
command: probe.command,
argv,
stdout,
stderr,
};
}
export async function runObsidianSearch(params: {
config: ResolvedMemoryWikiConfig;
query: string;
deps?: ObsidianCliDeps;
}) {
return await runObsidianCli({
config: params.config,
subcommand: "search" ,
args: [`query=${params.query}`],
deps: params.deps,
});
}
export async function runObsidianOpen(params: {
config: ResolvedMemoryWikiConfig;
vaultPath: string;
deps?: ObsidianCliDeps;
}) {
return await runObsidianCli({
config: params.config,
subcommand: "open" ,
args: [`path=${params.vaultPath}`],
deps: params.deps,
});
}
export async function runObsidianCommand(params: {
config: ResolvedMemoryWikiConfig;
id: string;
deps?: ObsidianCliDeps;
}) {
return await runObsidianCli({
config: params.config,
subcommand: "command" ,
args: [`id=${params.id}`],
deps: params.deps,
});
}
export async function runObsidianDaily(params: {
config: ResolvedMemoryWikiConfig;
deps?: ObsidianCliDeps;
}) {
return await runObsidianCli({
config: params.config,
subcommand: "daily" ,
deps: params.deps,
});
}
Messung V0.5 in Prozent C=100 H=94 G=96
¤ Dauer der Verarbeitung: 0.10 Sekunden
(vorverarbeitet am 2026-06-04)
¤
*© Formatika GbR, Deutschland