@mcptoolshop/backpropagate 1.0.4 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/bin/backpropagate.js +189 -11
  2. package/package.json +1 -1
@@ -1,20 +1,198 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
 
4
- // Linux: no prebuilt binary — libtorch_cpu.so is ~1.5GB on x86_64,
5
- // producing a 4GB+ binary that exceeds GitHub's 2GB release asset limit.
4
+ const { spawnSync } = require("child_process");
5
+ const path = require("path");
6
+ const fs = require("fs");
7
+ const os = require("os");
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Linux: bootstrap a private venv and run backpropagate from PyPI.
11
+ //
12
+ // PyTorch's libtorch_cpu.so is ~1.5 GB on x86_64 Linux. A PyInstaller
13
+ // --onefile binary lands at 4.16 GB — 2x GitHub's release asset limit.
14
+ // macOS/Windows don't hit this because Accelerate is system-provided and
15
+ // Windows torch is smaller.
16
+ //
17
+ // Instead of shipping a broken giant binary, we bootstrap a managed venv
18
+ // on first run and exec the pip-installed CLI from there.
19
+ // ---------------------------------------------------------------------------
20
+
6
21
  if (process.platform === "linux") {
7
- process.stderr.write(
8
- "\nbackpropagate: prebuilt binary not available for Linux.\n" +
9
- "PyTorch's native libraries are too large for a single-file binary.\n\n" +
10
- "Install natively instead:\n\n" +
11
- " pipx install backpropagate\n" +
12
- " # or: pip install backpropagate\n\n"
13
- );
14
- process.exit(1);
22
+ const TOOL = "backpropagate";
23
+ const VERSION = "1.0.4";
24
+
25
+ // XDG-compliant install root
26
+ const dataHome = process.env.XDG_DATA_HOME
27
+ || path.join(os.homedir(), ".local", "share");
28
+ const installRoot = process.env.BACKPROPAGATE_BOOTSTRAP_ROOT
29
+ || path.join(dataHome, TOOL);
30
+ const venvDir = path.join(installRoot, "venv");
31
+ const metaPath = path.join(installRoot, "install.json");
32
+ const venvBin = path.join(venvDir, "bin", TOOL);
33
+ const venvPython = path.join(venvDir, "bin", "python3");
34
+
35
+ // -- helpers --------------------------------------------------------------
36
+
37
+ function findPython() {
38
+ // Prefer python3, fall back to python (if it's 3.x)
39
+ for (const cmd of ["python3", "python"]) {
40
+ const r = spawnSync(cmd, ["--version"], {
41
+ encoding: "utf8",
42
+ stdio: ["ignore", "pipe", "pipe"],
43
+ });
44
+ if (r.status === 0) {
45
+ const ver = (r.stdout || r.stderr || "").trim();
46
+ const match = ver.match(/Python\s+(\d+)\.(\d+)/);
47
+ if (match && parseInt(match[1], 10) >= 3 && parseInt(match[2], 10) >= 10) {
48
+ return cmd;
49
+ }
50
+ }
51
+ }
52
+ return null;
53
+ }
54
+
55
+ function readMeta() {
56
+ try {
57
+ return JSON.parse(fs.readFileSync(metaPath, "utf8"));
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+
63
+ function writeMeta(obj) {
64
+ fs.mkdirSync(installRoot, { recursive: true });
65
+ fs.writeFileSync(metaPath, JSON.stringify(obj, null, 2) + "\n");
66
+ }
67
+
68
+ function fail(message, hint) {
69
+ process.stderr.write(`\n${TOOL}: ${message}\n`);
70
+ if (hint) process.stderr.write(`\n${hint}\n`);
71
+ process.stderr.write("\n");
72
+ process.exit(1);
73
+ }
74
+
75
+ // -- bootstrap ------------------------------------------------------------
76
+
77
+ function bootstrap() {
78
+ const forceReinstall = process.env.BACKPROPAGATE_FORCE_REINSTALL === "1";
79
+ const meta = readMeta();
80
+
81
+ const versionMatch = meta && meta.version === VERSION;
82
+ const binaryPresent = fs.existsSync(venvBin);
83
+
84
+ if (versionMatch && binaryPresent && !forceReinstall) {
85
+ return; // fast path — venv exists, version matches, no forced reinstall
86
+ }
87
+
88
+ // Decide what to tell the user
89
+ if (forceReinstall) {
90
+ process.stderr.write(`Forced reinstall requested (BACKPROPAGATE_FORCE_REINSTALL=1).\n`);
91
+ } else if (meta && !versionMatch) {
92
+ process.stderr.write(
93
+ `Updating ${TOOL}: ${meta.version} -> ${VERSION}\n`
94
+ );
95
+ } else if (meta && !binaryPresent) {
96
+ process.stderr.write(
97
+ `Repairing ${TOOL}: binary missing, reinstalling ${VERSION}...\n`
98
+ );
99
+ }
100
+
101
+ // Find system Python
102
+ const python = findPython();
103
+ if (!python) {
104
+ fail(
105
+ "Python 3.10+ is required but not found.",
106
+ "Install Python and try again:\n" +
107
+ " Ubuntu/Debian: sudo apt install python3 python3-venv\n" +
108
+ " Fedora/RHEL: sudo dnf install python3\n" +
109
+ " Arch: sudo pacman -S python\n" +
110
+ " Or use pyenv: https://github.com/pyenv/pyenv"
111
+ );
112
+ }
113
+
114
+ // Recreate venv on force-reinstall (nuke corrupted state)
115
+ if (forceReinstall && fs.existsSync(venvDir)) {
116
+ process.stderr.write("Removing existing venv...\n");
117
+ fs.rmSync(venvDir, { recursive: true, force: true });
118
+ }
119
+
120
+ // Create venv if missing
121
+ if (!fs.existsSync(venvPython)) {
122
+ if (!meta) {
123
+ process.stderr.write(
124
+ `First run on Linux: setting up local Python environment for ${TOOL}...\n`
125
+ );
126
+ }
127
+ fs.mkdirSync(installRoot, { recursive: true });
128
+ const venvResult = spawnSync(python, ["-m", "venv", venvDir], {
129
+ stdio: "inherit",
130
+ });
131
+ if (venvResult.status !== 0) {
132
+ fail(
133
+ "Failed to create Python virtual environment.",
134
+ "The venv module may be missing. Try:\n" +
135
+ " Ubuntu/Debian: sudo apt install python3-venv\n" +
136
+ " Fedora/RHEL: sudo dnf install python3-libs"
137
+ );
138
+ }
139
+ }
140
+
141
+ // Install or upgrade backpropagate
142
+ // --force-reinstall needed for repair (binary missing but pip metadata intact)
143
+ // and force-reinstall (nuked venv rebuilt). Without it, pip sees the same
144
+ // version in metadata and skips recreating the entrypoint script.
145
+ const needsForce = !binaryPresent || forceReinstall;
146
+ const pipArgs = ["-m", "pip", "install", "--quiet", "--upgrade"];
147
+ if (needsForce) pipArgs.push("--force-reinstall");
148
+ pipArgs.push(`${TOOL}==${VERSION}`);
149
+
150
+ process.stderr.write(`Installing ${TOOL} ${VERSION}${needsForce ? " (force)" : ""}...\n`);
151
+ const pipResult = spawnSync(venvPython, pipArgs, { stdio: "inherit" });
152
+ if (pipResult.status !== 0) {
153
+ fail(
154
+ `pip install failed (exit ${pipResult.status}).`,
155
+ "Check your network connection and try again.\n" +
156
+ "You can also install manually:\n" +
157
+ ` pip install ${TOOL}==${VERSION}`
158
+ );
159
+ }
160
+
161
+ // Verify the binary actually exists after install
162
+ if (!fs.existsSync(venvBin)) {
163
+ fail(
164
+ `Installation completed but ${TOOL} binary not found at expected path.`,
165
+ `Expected: ${venvBin}\n` +
166
+ "Try installing manually:\n" +
167
+ ` pipx install ${TOOL}`
168
+ );
169
+ }
170
+
171
+ // Record metadata
172
+ writeMeta({
173
+ version: VERSION,
174
+ installedAt: new Date().toISOString(),
175
+ python,
176
+ venvDir,
177
+ });
178
+
179
+ process.stderr.write("Ready.\n");
180
+ }
181
+
182
+ // -- exec -----------------------------------------------------------------
183
+
184
+ bootstrap();
185
+ const result = spawnSync(venvBin, process.argv.slice(2), { stdio: "inherit" });
186
+ if (result.error) {
187
+ fail(`Failed to execute ${TOOL}: ${result.error.message}`);
188
+ }
189
+ process.exit(result.status ?? 1);
15
190
  }
16
191
 
17
- // macOS + Windows: download and launch prebuilt binary
192
+ // ---------------------------------------------------------------------------
193
+ // macOS + Windows: download and launch prebuilt binary via npm-launcher
194
+ // ---------------------------------------------------------------------------
195
+
18
196
  process.env.MCPTOOLSHOP_LAUNCH_CONFIG = JSON.stringify({
19
197
  toolName: "backpropagate",
20
198
  owner: "mcp-tool-shop-org",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcptoolshop/backpropagate",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "Headless LLM fine-tuning CLI with smart defaults — train, export, serve",
5
5
  "type": "commonjs",
6
6
  "license": "MIT",