mirror of
https://github.com/lllin000/PaperForge.git
synced 2026-07-22 06:50:53 +00:00
refactor: inline src/ modules back into main.js — single file, no runtime require
This commit is contained in:
parent
56ee621bd7
commit
caab9ff9ee
5 changed files with 217 additions and 409 deletions
|
|
@ -1,12 +1,224 @@
|
|||
const { Plugin, Notice, ItemView, Modal, Setting, PluginSettingTab, addIcon } = require('obsidian');
|
||||
const { exec, execFile } = require('node:child_process');
|
||||
const { exec, execFile, spawn } = require('node:child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Extracted modules for testability (Plan 53-001)
|
||||
const { resolvePythonExecutable, getPluginVersion, checkRuntimeVersion } = require('./src/runtime');
|
||||
const { classifyError, buildRuntimeInstallCommand, parseRuntimeStatus } = require('./src/errors');
|
||||
const { ACTIONS, buildCommandArgs, runSubprocess } = require('./src/commands');
|
||||
// ── Runtime helpers (inlined from src/runtime.js) ──
|
||||
|
||||
function resolvePythonExecutable(vaultPath, settings, _fs, _execFileSync) {
|
||||
const f = _fs || fs;
|
||||
const execSync = _execFileSync || require("node:child_process").execFileSync;
|
||||
|
||||
if (settings && settings.python_path && settings.python_path.trim()) {
|
||||
const manualPath = settings.python_path.trim();
|
||||
if (f.existsSync(manualPath)) {
|
||||
return { path: manualPath, source: "manual", extraArgs: [] };
|
||||
}
|
||||
}
|
||||
|
||||
const venvCandidates = [
|
||||
path.join(vaultPath, ".paperforge-test-venv", "Scripts", "python.exe"),
|
||||
path.join(vaultPath, ".venv", "Scripts", "python.exe"),
|
||||
path.join(vaultPath, "venv", "Scripts", "python.exe"),
|
||||
];
|
||||
for (const candidate of venvCandidates) {
|
||||
try {
|
||||
if (f.existsSync(candidate)) {
|
||||
return { path: candidate, source: "auto-detected", extraArgs: [] };
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const systemCandidates = [
|
||||
{ path: "py", extraArgs: ["-3"] },
|
||||
{ path: "python", extraArgs: [] },
|
||||
{ path: "python3", extraArgs: [] },
|
||||
];
|
||||
for (const candidate of systemCandidates) {
|
||||
try {
|
||||
const verOut = execSync(candidate.path, [...candidate.extraArgs, "--version"], {
|
||||
encoding: "utf-8", timeout: 5000, windowsHide: true,
|
||||
});
|
||||
if (verOut && verOut.toLowerCase().includes("python")) {
|
||||
return { path: candidate.path, source: "auto-detected", extraArgs: candidate.extraArgs };
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return { path: "python", source: "auto-detected", extraArgs: [] };
|
||||
}
|
||||
|
||||
function getPluginVersion(app) {
|
||||
try {
|
||||
const manifest = app && app.plugins && app.plugins.plugins &&
|
||||
app.plugins.plugins["paperforge"] && app.plugins.plugins["paperforge"].manifest;
|
||||
return (manifest && manifest.version) || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function checkRuntimeVersion(pythonExe, pluginVersion, cwd, timeout, _execFile) {
|
||||
if (timeout === undefined) timeout = 10000;
|
||||
const exe = _execFile || execFile;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
exe(pythonExe, ["-c", "import paperforge; print(paperforge.__version__)"],
|
||||
{ cwd, timeout },
|
||||
(err, stdout) => {
|
||||
if (err) {
|
||||
resolve({ status: "not-installed", pyVersion: null, pluginVersion, error: err.message });
|
||||
return;
|
||||
}
|
||||
const pyVer = (stdout && stdout.trim()) || null;
|
||||
if (pyVer === pluginVersion) {
|
||||
resolve({ status: "match", pyVersion: pyVer, pluginVersion, error: null });
|
||||
} else {
|
||||
resolve({ status: "mismatch", pyVersion: pyVer, pluginVersion, error: null });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ── Error helpers (inlined from src/errors.js) ──
|
||||
|
||||
function classifyError(errorCode) {
|
||||
const code = String(errorCode);
|
||||
const patterns = {
|
||||
ENOENT: { type: "python_missing", message: "Python executable not found", recoverable: true },
|
||||
"python-missing": { type: "python_missing", message: "Python executable not found", recoverable: true },
|
||||
MODULE_NOT_FOUND: { type: "import_failed", message: "PaperForge package not installed", recoverable: true },
|
||||
"import-failed": { type: "import_failed", message: "PaperForge package not installed", recoverable: true },
|
||||
"version-mismatch": { type: "version_mismatch", message: "Plugin and package versions differ", recoverable: true, action: "sync-runtime" },
|
||||
"pip-failed": { type: "pip_install_failure", message: "pip install command failed", recoverable: true },
|
||||
ETIMEDOUT: { type: "timeout", message: "Subprocess timed out", recoverable: true, action: "retry" },
|
||||
timeout: { type: "timeout", message: "Subprocess timed out", recoverable: true, action: "retry" },
|
||||
};
|
||||
const match = patterns[code];
|
||||
if (match) return { ...match };
|
||||
return { type: "unknown", message: String(errorCode), recoverable: false };
|
||||
}
|
||||
|
||||
function buildRuntimeInstallCommand(pythonExe, version, extraArgs) {
|
||||
if (extraArgs === undefined) extraArgs = [];
|
||||
const url = `git+https://github.com/LLLin000/PaperForge.git@${version}`;
|
||||
const args = [...extraArgs, "-m", "pip", "install", "--upgrade", url];
|
||||
return { cmd: pythonExe, args, url, timeout: 120000 };
|
||||
}
|
||||
|
||||
function parseRuntimeStatus(err, stdout, stderr) {
|
||||
if (!err && stdout) {
|
||||
return { status: "ok", version: stdout.trim() };
|
||||
}
|
||||
if (err && err.code === "ENOENT") {
|
||||
const classified = classifyError("ENOENT");
|
||||
return { status: "error", version: null, ...classified };
|
||||
}
|
||||
if (stderr && stderr.includes("No module named paperforge")) {
|
||||
const classified = classifyError("import-failed");
|
||||
return { status: "error", version: null, ...classified };
|
||||
}
|
||||
if (err && err.killed) {
|
||||
const classified = classifyError("timeout");
|
||||
return { status: "error", version: null, ...classified };
|
||||
}
|
||||
if (stderr && stderr.includes("ModuleNotFoundError")) {
|
||||
const classified = classifyError("import-failed");
|
||||
return { status: "error", version: null, ...classified };
|
||||
}
|
||||
return { status: "error", version: null, type: "unknown",
|
||||
message: err ? err.message : String(stderr), recoverable: false };
|
||||
}
|
||||
|
||||
// ── Action definitions and subprocess dispatch (inlined from src/commands.js) ──
|
||||
|
||||
const ACTIONS = [
|
||||
{
|
||||
id: "paperforge-sync",
|
||||
title: "Sync Library",
|
||||
desc: "Pull new references from Zotero and generate literature notes",
|
||||
icon: "\u21BB",
|
||||
cmd: "sync",
|
||||
okMsg: "Sync complete",
|
||||
},
|
||||
{
|
||||
id: "paperforge-ocr",
|
||||
title: "Run OCR",
|
||||
desc: "Extract full text and figures from PDFs via PaddleOCR",
|
||||
icon: "\u229E",
|
||||
cmd: "ocr",
|
||||
okMsg: "OCR started",
|
||||
},
|
||||
{
|
||||
id: "paperforge-doctor",
|
||||
title: "Run Doctor",
|
||||
desc: "Verify PaperForge setup \u2014 check configs, Zotero, paths, and index health",
|
||||
icon: "\u2695",
|
||||
cmd: "doctor",
|
||||
okMsg: "Doctor complete",
|
||||
},
|
||||
{
|
||||
id: "paperforge-repair",
|
||||
title: "Repair Issues",
|
||||
desc: "Fix three-way state divergence, path errors, and rebuild index",
|
||||
icon: "\u21BA",
|
||||
cmd: "repair",
|
||||
args: ["--fix", "--fix-paths"],
|
||||
okMsg: "Repair complete",
|
||||
disabled: true,
|
||||
disabledMsg: "Repair Issues will be available in a future update.",
|
||||
},
|
||||
{
|
||||
id: "paperforge-copy-context",
|
||||
title: "Copy Context",
|
||||
desc: "Copy this paper\u2019s canonical index entry JSON to clipboard for AI use",
|
||||
icon: "\u2139",
|
||||
cmd: "context",
|
||||
needsKey: true,
|
||||
okMsg: "Context copied",
|
||||
},
|
||||
{
|
||||
id: "paperforge-copy-collection-context",
|
||||
title: "Copy Collection Context",
|
||||
desc: "Copy canonical index entries for all visible papers to clipboard",
|
||||
icon: "\u2261",
|
||||
cmd: "context",
|
||||
needsFilter: true,
|
||||
okMsg: "Collection context copied",
|
||||
},
|
||||
];
|
||||
|
||||
function buildCommandArgs(action, key, filter) {
|
||||
const args = Array.isArray(action.args) ? [...action.args] : [];
|
||||
if (action.needsKey && key) args.push(key);
|
||||
if (action.needsFilter || filter) args.push("--all");
|
||||
return args;
|
||||
}
|
||||
|
||||
function runSubprocess(pythonExe, args, cwd, timeout, _spawn) {
|
||||
const sp = _spawn || spawn;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const startTime = Date.now();
|
||||
const child = sp(pythonExe, args, { cwd, timeout, windowsHide: true });
|
||||
const stdoutChunks = [];
|
||||
const stderrChunks = [];
|
||||
|
||||
child.stdout.on("data", (data) => { stdoutChunks.push(data.toString("utf-8")); });
|
||||
child.stderr.on("data", (data) => { stderrChunks.push(data.toString("utf-8")); });
|
||||
|
||||
child.on("close", (code) => {
|
||||
resolve({ stdout: stdoutChunks.join(""), stderr: stderrChunks.join(""),
|
||||
exitCode: code, elapsed: Date.now() - startTime });
|
||||
});
|
||||
|
||||
child.on("error", (err) => {
|
||||
resolve({ stdout: stdoutChunks.join(""),
|
||||
stderr: stderrChunks.join("") + "\n" + err.message,
|
||||
exitCode: -1, elapsed: Date.now() - startTime });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const VIEW_TYPE_PAPERFORGE = 'paperforge-status';
|
||||
const PF_ICON_ID = 'paperforge';
|
||||
|
|
|
|||
|
|
@ -1,147 +0,0 @@
|
|||
/**
|
||||
* Action definitions and subprocess dispatch for PaperForge plugin commands.
|
||||
*
|
||||
* Extracted from main.js for testability. Provides:
|
||||
* - ACTIONS array (same as main.js lines 189-243)
|
||||
* - buildCommandArgs — resolves action arguments (key, filter flags)
|
||||
* - runSubprocess — Promisified spawn with stdout/stderr capture
|
||||
*
|
||||
* `runSubprocess` accepts an optional `_spawn` parameter for testing.
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
const { spawn } = require("node:child_process");
|
||||
|
||||
/**
|
||||
* Action definitions for the PaperForge dashboard quick-action buttons.
|
||||
*
|
||||
* Each action maps to a `paperforge <cmd>` CLI invocation.
|
||||
*/
|
||||
const ACTIONS = [
|
||||
{
|
||||
id: "paperforge-sync",
|
||||
title: "Sync Library",
|
||||
desc: "Pull new references from Zotero and generate literature notes",
|
||||
icon: "\u21BB",
|
||||
cmd: "sync",
|
||||
okMsg: "Sync complete",
|
||||
},
|
||||
{
|
||||
id: "paperforge-ocr",
|
||||
title: "Run OCR",
|
||||
desc: "Extract full text and figures from PDFs via PaddleOCR",
|
||||
icon: "\u229E",
|
||||
cmd: "ocr",
|
||||
okMsg: "OCR started",
|
||||
},
|
||||
{
|
||||
id: "paperforge-doctor",
|
||||
title: "Run Doctor",
|
||||
desc: "Verify PaperForge setup \u2014 check configs, Zotero, paths, and index health",
|
||||
icon: "\u2695",
|
||||
cmd: "doctor",
|
||||
okMsg: "Doctor complete",
|
||||
},
|
||||
{
|
||||
id: "paperforge-repair",
|
||||
title: "Repair Issues",
|
||||
desc: "Fix three-way state divergence, path errors, and rebuild index",
|
||||
icon: "\u21BA",
|
||||
cmd: "repair",
|
||||
args: ["--fix", "--fix-paths"],
|
||||
okMsg: "Repair complete",
|
||||
disabled: true,
|
||||
disabledMsg: "Repair Issues will be available in a future update.",
|
||||
},
|
||||
{
|
||||
id: "paperforge-copy-context",
|
||||
title: "Copy Context",
|
||||
desc: "Copy this paper\u2019s canonical index entry JSON to clipboard for AI use",
|
||||
icon: "\u2139",
|
||||
cmd: "context",
|
||||
needsKey: true,
|
||||
okMsg: "Context copied",
|
||||
},
|
||||
{
|
||||
id: "paperforge-copy-collection-context",
|
||||
title: "Copy Collection Context",
|
||||
desc: "Copy canonical index entries for all visible papers to clipboard",
|
||||
icon: "\u2261",
|
||||
cmd: "context",
|
||||
needsFilter: true,
|
||||
okMsg: "Collection context copied",
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Resolve extra CLI arguments for an action based on needsKey and needsFilter flags.
|
||||
*
|
||||
* @param {object} action — action object from ACTIONS array
|
||||
* @param {string|null} key — zotero_key for per-paper actions
|
||||
* @param {boolean} [filter] — whether to add --all filter
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function buildCommandArgs(action, key, filter) {
|
||||
const args = Array.isArray(action.args) ? [...action.args] : [];
|
||||
if (action.needsKey && key) {
|
||||
args.push(key);
|
||||
}
|
||||
if (action.needsFilter || filter) {
|
||||
args.push("--all");
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a subprocess with full stdout/stderr capture, wrapped in a Promise.
|
||||
*
|
||||
* @param {string} pythonExe — resolved Python executable path
|
||||
* @param {string[]} args — CLI arguments
|
||||
* @param {string} cwd — working directory (vault root)
|
||||
* @param {number} [timeout] — timeout in ms
|
||||
* @param {Function} [_spawn] — optional injected spawn (for testing)
|
||||
* @returns {Promise<{ stdout: string, stderr: string, exitCode: number|null, elapsed: number }>}
|
||||
*/
|
||||
function runSubprocess(pythonExe, args, cwd, timeout, _spawn) {
|
||||
const sp = _spawn || spawn;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const startTime = Date.now();
|
||||
const child = sp(pythonExe, args, { cwd, timeout, windowsHide: true });
|
||||
const stdoutChunks = [];
|
||||
const stderrChunks = [];
|
||||
|
||||
child.stdout.on("data", (data) => {
|
||||
stdoutChunks.push(data.toString("utf-8"));
|
||||
});
|
||||
child.stderr.on("data", (data) => {
|
||||
stderrChunks.push(data.toString("utf-8"));
|
||||
});
|
||||
|
||||
child.on("close", (code) => {
|
||||
const elapsed = Date.now() - startTime;
|
||||
resolve({
|
||||
stdout: stdoutChunks.join(""),
|
||||
stderr: stderrChunks.join(""),
|
||||
exitCode: code,
|
||||
elapsed,
|
||||
});
|
||||
});
|
||||
|
||||
child.on("error", (err) => {
|
||||
const elapsed = Date.now() - startTime;
|
||||
resolve({
|
||||
stdout: stdoutChunks.join(""),
|
||||
stderr: stderrChunks.join("") + "\n" + err.message,
|
||||
exitCode: -1,
|
||||
elapsed,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ACTIONS,
|
||||
buildCommandArgs,
|
||||
runSubprocess,
|
||||
};
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
/**
|
||||
* Error classification and runtime install command builder for PaperForge plugin.
|
||||
*
|
||||
* Extracted from main.js for testability. Covers 5 error patterns:
|
||||
* 1. Python missing — ENOENT or python-missing
|
||||
* 2. Import failed — MODULE_NOT_FOUND or import-failed
|
||||
* 3. Version mismatch — version-mismatch
|
||||
* 4. Pip install failure — pip-failed
|
||||
* 5. Timeout — ETIMEDOUT or timeout
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Classify a subprocess error code/string into a structured error object.
|
||||
*
|
||||
* @param {string|number} errorCode — error.code from subprocess, or a string label
|
||||
* @returns {{ type: string, message: string, recoverable: boolean, action?: string }}
|
||||
*/
|
||||
function classifyError(errorCode) {
|
||||
const code = String(errorCode);
|
||||
|
||||
const patterns = {
|
||||
ENOENT: { type: "python_missing", message: "Python executable not found", recoverable: true },
|
||||
"python-missing": { type: "python_missing", message: "Python executable not found", recoverable: true },
|
||||
MODULE_NOT_FOUND: { type: "import_failed", message: "PaperForge package not installed", recoverable: true },
|
||||
"import-failed": { type: "import_failed", message: "PaperForge package not installed", recoverable: true },
|
||||
"version-mismatch": { type: "version_mismatch", message: "Plugin and package versions differ", recoverable: true, action: "sync-runtime" },
|
||||
"pip-failed": { type: "pip_install_failure", message: "pip install command failed", recoverable: true },
|
||||
ETIMEDOUT: { type: "timeout", message: "Subprocess timed out", recoverable: true, action: "retry" },
|
||||
timeout: { type: "timeout", message: "Subprocess timed out", recoverable: true, action: "retry" },
|
||||
};
|
||||
|
||||
const match = patterns[code];
|
||||
if (match) {
|
||||
return { ...match };
|
||||
}
|
||||
|
||||
return { type: "unknown", message: String(errorCode), recoverable: false };
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the command and arguments for installing a specific version of paperforge via pip.
|
||||
*
|
||||
* @param {string} pythonExe — resolved Python executable
|
||||
* @param {string} version — version tag (e.g., "1.4.17rc3")
|
||||
* @param {string[]} [extraArgs=[]] — extra Python CLI args (e.g., venv activation flags)
|
||||
* @returns {{ cmd: string, args: string[], url: string, timeout: number }}
|
||||
*/
|
||||
function buildRuntimeInstallCommand(pythonExe, version, extraArgs) {
|
||||
if (extraArgs === undefined) extraArgs = [];
|
||||
const url = `git+https://github.com/LLLin000/PaperForge.git@${version}`;
|
||||
const args = [...extraArgs, "-m", "pip", "install", "--upgrade", url];
|
||||
return { cmd: pythonExe, args, url, timeout: 120000 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the result of a subprocess version check into a structured status object.
|
||||
*
|
||||
* @param {Error|null} err — subprocess error object (or null on success)
|
||||
* @param {string|null} stdout — subprocess stdout
|
||||
* @param {string|null} stderr — subprocess stderr
|
||||
* @returns {{ status: string, version: string|null, type?: string, message?: string, recoverable?: boolean, action?: string }}
|
||||
*/
|
||||
function parseRuntimeStatus(err, stdout, stderr) {
|
||||
if (!err && stdout) {
|
||||
const version = stdout.trim();
|
||||
return { status: "ok", version };
|
||||
}
|
||||
|
||||
if (err && err.code === "ENOENT") {
|
||||
const classified = classifyError("ENOENT");
|
||||
return { status: "error", version: null, ...classified };
|
||||
}
|
||||
|
||||
if (stderr && stderr.includes("No module named paperforge")) {
|
||||
const classified = classifyError("import-failed");
|
||||
return { status: "error", version: null, ...classified };
|
||||
}
|
||||
|
||||
if (err && err.killed) {
|
||||
const classified = classifyError("timeout");
|
||||
return { status: "error", version: null, ...classified };
|
||||
}
|
||||
|
||||
if (stderr && stderr.includes("ModuleNotFoundError")) {
|
||||
const classified = classifyError("import-failed");
|
||||
return { status: "error", version: null, ...classified };
|
||||
}
|
||||
|
||||
return { status: "error", version: null, type: "unknown", message: err ? err.message : String(stderr), recoverable: false };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
classifyError,
|
||||
buildRuntimeInstallCommand,
|
||||
parseRuntimeStatus,
|
||||
};
|
||||
|
|
@ -1,157 +0,0 @@
|
|||
/**
|
||||
* Runtime environment helpers — Python executable resolution and version checking.
|
||||
*
|
||||
* Extracted from main.js for testability. These functions handle:
|
||||
* - Finding a usable Python interpreter (manual, venv, system)
|
||||
* - Reading the plugin version from the Obsidian manifest
|
||||
* - Checking the installed paperforge package version against the plugin
|
||||
*
|
||||
* Dependencies are accepted as optional parameters for testability.
|
||||
* When omitted, they are eagerly required at the top level.
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { execFile } = require("node:child_process");
|
||||
|
||||
/**
|
||||
* Resolve a usable Python executable by trying candidates in priority order.
|
||||
*
|
||||
* 1. Manual override (settings.python_path)
|
||||
* 2. Venv candidates (common venv locations under vaultPath)
|
||||
* 3. System candidates (py -3, python, python3)
|
||||
* 4. Last-resort fallback
|
||||
*
|
||||
* @param {string} vaultPath — root path of the Obsidian vault
|
||||
* @param {object} settings — plugin settings (may contain python_path)
|
||||
* @param {object} [_fs] — optional injected fs module (for testing)
|
||||
* @param {Function} [_execFileSync] — optional injected execFileSync (for testing)
|
||||
* @returns {{ path: string, source: string, extraArgs: string[] }}
|
||||
*/
|
||||
function resolvePythonExecutable(vaultPath, settings, _fs, _execFileSync) {
|
||||
const f = _fs || fs;
|
||||
const execSync = _execFileSync || require("node:child_process").execFileSync;
|
||||
|
||||
// 1. Manual override — absolute source of truth
|
||||
if (settings && settings.python_path && settings.python_path.trim()) {
|
||||
const manualPath = settings.python_path.trim();
|
||||
if (f.existsSync(manualPath)) {
|
||||
return { path: manualPath, source: "manual", extraArgs: [] };
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Venv candidates
|
||||
const venvCandidates = [
|
||||
path.join(vaultPath, ".paperforge-test-venv", "Scripts", "python.exe"),
|
||||
path.join(vaultPath, ".venv", "Scripts", "python.exe"),
|
||||
path.join(vaultPath, "venv", "Scripts", "python.exe"),
|
||||
];
|
||||
for (const candidate of venvCandidates) {
|
||||
try {
|
||||
if (f.existsSync(candidate)) {
|
||||
return { path: candidate, source: "auto-detected", extraArgs: [] };
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// 3. System candidates — test each with --version, pick first that succeeds
|
||||
const systemCandidates = [
|
||||
{ path: "py", extraArgs: ["-3"] },
|
||||
{ path: "python", extraArgs: [] },
|
||||
{ path: "python3", extraArgs: [] },
|
||||
];
|
||||
for (const candidate of systemCandidates) {
|
||||
try {
|
||||
const verOut = execSync(candidate.path, [...candidate.extraArgs, "--version"], {
|
||||
encoding: "utf-8",
|
||||
timeout: 5000,
|
||||
windowsHide: true,
|
||||
});
|
||||
if (verOut && verOut.toLowerCase().includes("python")) {
|
||||
return { path: candidate.path, source: "auto-detected", extraArgs: candidate.extraArgs };
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// 4. Last-resort fallback
|
||||
return { path: "python", source: "auto-detected", extraArgs: [] };
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the plugin version from the Obsidian app manifest.
|
||||
*
|
||||
* @param {object} app — Obsidian App instance (from plugin)
|
||||
* @returns {string|null}
|
||||
*/
|
||||
function getPluginVersion(app) {
|
||||
try {
|
||||
const manifest =
|
||||
app &&
|
||||
app.plugins &&
|
||||
app.plugins.plugins &&
|
||||
app.plugins.plugins["paperforge"] &&
|
||||
app.plugins.plugins["paperforge"].manifest;
|
||||
return (manifest && manifest.version) || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the installed paperforge Python package matches the plugin version.
|
||||
*
|
||||
* Spawns: pythonExe -c "import paperforge; print(paperforge.__version__)"
|
||||
*
|
||||
* @param {string} pythonExe — resolved Python executable path
|
||||
* @param {string} pluginVersion — expected version from manifest
|
||||
* @param {string} cwd — working directory (vault root)
|
||||
* @param {number} [timeout=10000] — timeout in ms
|
||||
* @param {Function} [_execFile] — optional injected execFile (for testing)
|
||||
* @returns {Promise<{ status: string, pyVersion: string|null, pluginVersion: string, error: string|null }>}
|
||||
*/
|
||||
function checkRuntimeVersion(pythonExe, pluginVersion, cwd, timeout, _execFile) {
|
||||
if (timeout === undefined) timeout = 10000;
|
||||
const exe = _execFile || execFile;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
exe(
|
||||
pythonExe,
|
||||
["-c", "import paperforge; print(paperforge.__version__)"],
|
||||
{ cwd, timeout },
|
||||
(err, stdout) => {
|
||||
if (err) {
|
||||
resolve({
|
||||
status: "not-installed",
|
||||
pyVersion: null,
|
||||
pluginVersion,
|
||||
error: err.message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const pyVer = (stdout && stdout.trim()) || null;
|
||||
if (pyVer === pluginVersion) {
|
||||
resolve({
|
||||
status: "match",
|
||||
pyVersion: pyVer,
|
||||
pluginVersion,
|
||||
error: null,
|
||||
});
|
||||
} else {
|
||||
resolve({
|
||||
status: "mismatch",
|
||||
pyVersion: pyVer,
|
||||
pluginVersion,
|
||||
error: null,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
resolvePythonExecutable,
|
||||
getPluginVersion,
|
||||
checkRuntimeVersion,
|
||||
};
|
||||
|
|
@ -31,9 +31,6 @@ PLUGIN_FILES = [
|
|||
ROOT / "paperforge" / "plugin" / "main.js",
|
||||
ROOT / "paperforge" / "plugin" / "styles.css",
|
||||
ROOT / "paperforge" / "plugin" / "manifest.json",
|
||||
ROOT / "paperforge" / "plugin" / "src" / "runtime.js",
|
||||
ROOT / "paperforge" / "plugin" / "src" / "errors.js",
|
||||
ROOT / "paperforge" / "plugin" / "src" / "commands.js",
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue