mirror of
https://github.com/lllin000/PaperForge.git
synced 2026-07-22 06:50:53 +00:00
- New services/skill_deploy.py: single source of truth for agent skill deployment - AGENT_CONFIGS with 9 platforms, all vault-local - deploy_skills() with install/update mode (overwrite flag) - Used by both setup wizard and update worker - update.py: _deploy_all_skills() after pip/git/zip update - setup_wizard.py: delegates skill deploy to shared service - config.py: add agent_platform default - main.js: Copy Context + Copy Collection Context now pure JS - Uses in-memory _cachedItems / _currentPaperEntry - No subprocess spawn, no timeout, no JSON parse errors - testable.js: remove needsKey/needsFilter from context actions - commands.test.mjs: update assertions for removed flags
232 lines
8.4 KiB
JavaScript
232 lines
8.4 KiB
JavaScript
/**
|
|
* Testable functions extracted from main.js for Vitest.
|
|
* No obsidian dependency — safe to import in Node test environment.
|
|
*/
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { execFile, spawn } = require('node:child_process');
|
|
|
|
// ── Runtime helpers ──
|
|
|
|
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 ──
|
|
|
|
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 ──
|
|
|
|
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",
|
|
},
|
|
{
|
|
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",
|
|
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",
|
|
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 });
|
|
});
|
|
});
|
|
}
|
|
|
|
module.exports = {
|
|
resolvePythonExecutable,
|
|
getPluginVersion,
|
|
checkRuntimeVersion,
|
|
classifyError,
|
|
buildRuntimeInstallCommand,
|
|
parseRuntimeStatus,
|
|
ACTIONS,
|
|
buildCommandArgs,
|
|
runSubprocess,
|
|
};
|