mdinterface 0.1.1 → 0.2.0

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/package.json +4 -2
  2. package/server.js +81 -15
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mdinterface",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "A live markdown canvas wired to Claude Code. Highlight a passage to set both the context and the scope, ask, and Claude edits exactly that and leaves the rest alone.",
5
5
  "license": "MIT",
6
6
  "author": "Kevin Sundstrom",
@@ -51,9 +51,11 @@
51
51
  ],
52
52
  "dependencies": {
53
53
  "express": "^4.19.0",
54
- "node-pty": "^1.0.0",
55
54
  "ws": "^8.17.0"
56
55
  },
56
+ "optionalDependencies": {
57
+ "node-pty": "^1.0.0"
58
+ },
57
59
  "devDependencies": {
58
60
  "@biomejs/biome": "^2.0.0",
59
61
  "@types/express": "^4.17.21",
package/server.js CHANGED
@@ -16,7 +16,15 @@ const path = require("node:path");
16
16
  const os = require("node:os");
17
17
  const crypto = require("node:crypto");
18
18
  const { WebSocketServer } = require("ws");
19
- const pty = require("node-pty");
19
+ // node-pty powers the embedded terminal pane. It's an OPTIONAL native dependency (see
20
+ // optionalDependencies in package.json): if it didn't install or build, the rendered canvas,
21
+ // the file watcher, and the selection bridge all still come up — only the in-window terminal
22
+ // is disabled, and you run `claude` in your own terminal instead (the hooks still feed it the
23
+ // selection off disk). The specific reason is surfaced to the user at spawn time.
24
+ let pty = null;
25
+ try {
26
+ pty = require("node-pty");
27
+ } catch {}
20
28
 
21
29
  // ---------- args ----------
22
30
  const args = process.argv.slice(2);
@@ -405,13 +413,65 @@ function onPtyData(d) {
405
413
  flushTerm();
406
414
  }, TERM_FLUSH_MS);
407
415
  }
416
+ // pnpm, Yarn PnP, CI caches, and Docker COPY frequently strip the +x bit from node-pty's
417
+ // prebuilt spawn-helper, which makes pty.spawn die — the single most common runtime failure.
418
+ // Restore it in-process, since a postinstall script can't help the people who hit this most
419
+ // (--ignore-scripts / pnpm users disable postinstall). Returns true if it actually fixed a bit.
420
+ function ensureSpawnHelperExecutable() {
421
+ let fixed = false;
422
+ try {
423
+ const root = path.dirname(require.resolve("node-pty/package.json")); // works wherever it's hoisted
424
+ for (const base of [path.join(root, "prebuilds"), path.join(root, "build", "Release")]) {
425
+ let ents;
426
+ try {
427
+ ents = fs.readdirSync(base, { withFileTypes: true });
428
+ } catch {
429
+ continue;
430
+ }
431
+ const files = [];
432
+ for (const ent of ents) {
433
+ if (ent.isDirectory()) files.push(path.join(base, ent.name, "spawn-helper"));
434
+ else if (ent.name === "spawn-helper") files.push(path.join(base, ent.name));
435
+ }
436
+ for (const f of files) {
437
+ try {
438
+ if (!(fs.statSync(f).mode & 0o111)) {
439
+ fs.chmodSync(f, 0o755);
440
+ fixed = true;
441
+ }
442
+ } catch {}
443
+ }
444
+ }
445
+ } catch {}
446
+ return fixed;
447
+ }
448
+
449
+ // Turn a spawn failure into a specific, actionable message instead of a generic one.
450
+ function diagnosePtyFailure(e) {
451
+ const msg = (e && e.message) || String(e);
452
+ let hint;
453
+ if (/NODE_MODULE_VERSION|compiled against|different Node/i.test(msg))
454
+ hint = "node-pty was built for a different Node version — rebuild it:\n npm rebuild node-pty";
455
+ else if (/spawn-helper|EACCES|ENOENT|permission/i.test(msg))
456
+ hint =
457
+ "node-pty's spawn-helper is missing or not executable:\n" +
458
+ " chmod +x node_modules/node-pty/prebuilds/*/spawn-helper\n npm rebuild node-pty";
459
+ else hint = "Reinstall the native module:\n npm rebuild node-pty";
460
+ return (
461
+ `Embedded terminal could not start (${msg}).\n${hint}\n` +
462
+ `The canvas and selection bridge still work — run \`${CLAUDE_CMD}\` in your own terminal beside this window.`
463
+ );
464
+ }
465
+
408
466
  function spawnPty(cols = 100, rows = 32) {
467
+ if (!pty) return null; // optional dependency absent — startShell shows the fallback message
468
+ ensureSpawnHelperExecutable(); // proactively fix a stripped +x bit before we spawn
409
469
  const env = { ...process.env, TERM: "xterm-256color", COLORTERM: "truecolor" };
410
470
  const cwd = path.dirname(DOC);
411
471
  const sh = process.env.SHELL || (os.platform() === "win32" ? "powershell.exe" : "/bin/bash");
412
472
  const opts = { name: "xterm-256color", cols, rows, cwd, env };
413
- // Launch through the user's interactive login shell so PATH, rc files,
414
- // and aliases apply — this is how `claude` is normally found.
473
+ // Launch through the user's interactive login shell so PATH, rc files, and aliases apply —
474
+ // this is how `claude` is normally found.
415
475
  try {
416
476
  return pty.spawn(sh, ["-ilc", CLAUDE_CMD], opts);
417
477
  } catch (e) {
@@ -419,18 +479,18 @@ function spawnPty(cols = 100, rows = 32) {
419
479
  `Could not start "${CLAUDE_CMD}" via ${sh} (${e.message}); opening a plain shell.`
420
480
  );
421
481
  }
422
- try {
423
- return pty.spawn(sh, [], opts);
424
- } catch (e) {
425
- console.error(
426
- `PTY could not start at all (${e.message}).\n` +
427
- `node-pty's prebuilt spawn-helper may have lost its executable bit. Restore it:\n` +
428
- ` chmod +x node_modules/node-pty/prebuilds/*/spawn-helper\n` +
429
- `If there's no prebuilt binary for your platform, build it instead:\n` +
430
- ` npm rebuild node-pty`
431
- );
432
- return null;
482
+ // Plain-shell fallback. If even this fails, a stripped +x bit on spawn-helper is the usual
483
+ // cause: self-heal it and retry once before giving up with a diagnosis.
484
+ for (let attempt = 0; attempt < 2; attempt++) {
485
+ try {
486
+ return pty.spawn(sh, [], opts);
487
+ } catch (e) {
488
+ if (attempt === 0 && ensureSpawnHelperExecutable()) continue; // fixed a bit → retry
489
+ console.error(diagnosePtyFailure(e));
490
+ return null;
491
+ }
433
492
  }
493
+ return null;
434
494
  }
435
495
 
436
496
  // Start the PTY and wire BOTH its data and exit handlers (the earlier version reattached
@@ -445,7 +505,13 @@ let rapidExits = 0;
445
505
  function startShell() {
446
506
  shell = spawnPty(ptyCols, ptyRows); // respawn at the current size, not the 100x32 default
447
507
  if (!shell) {
448
- broadcast({ type: "term", data: "Terminal unavailable see server console for the fix.\r\n" });
508
+ // Two cases, both non-fatal: node-pty isn't installed at all, or it is but the spawn
509
+ // failed (details already in the server console via diagnosePtyFailure). Either way the
510
+ // canvas + selection bridge work, so point the user at running claude in their own terminal.
511
+ const data = pty
512
+ ? "\r\nEmbedded terminal could not start (see the server console). The canvas and selection still work — run claude in your own terminal beside this window and it gets the same context.\r\n"
513
+ : "\r\nEmbedded terminal disabled: node-pty isn't installed. The canvas and selection still work — run claude in your own terminal beside this window (it gets the same context via the hooks). To enable the in-window terminal: npm i node-pty\r\n";
514
+ broadcast({ type: "term", data });
449
515
  return;
450
516
  }
451
517
  shellSpawnAt = Date.now();