@termhub/agent 0.1.1 → 0.1.2

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 (3) hide show
  1. package/README.md +7 -0
  2. package/dist/cli.js +71 -30
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -52,3 +52,10 @@ macOS's TCC (Transparency, Consent and Control) can block the agent from listing
52
52
  special privileges. If `doctor` or `service install` reports this, grant the agent's Node binary
53
53
  Full Disk Access: **Ajustes → Privacidade e Segurança → Acesso Total ao Disco**, then add the
54
54
  `node` binary path `doctor` printed.
55
+
56
+ ## Troubleshooting
57
+
58
+ - **Terminals fail to open with `posix_spawnp failed.`** (visible in `~/.termhub/agent.log`): node-pty's
59
+ prebuilt `spawn-helper` lost its exec bit — npm 11 skips the postinstall that sets it unless the
60
+ package is approved. The agent repairs this itself on startup and in `termhub-agent doctor`; if the
61
+ file is not writable by your user, `doctor` prints the exact `chmod +x` to run.
package/dist/cli.js CHANGED
@@ -529,10 +529,10 @@ function ptyEnv(base, shell) {
529
529
  // src/exec.ts
530
530
  var DEFAULT_TIMEOUT_MS = 8e3;
531
531
  var RpcFailure = class extends Error {
532
- constructor(code, message, path8) {
532
+ constructor(code, message, path9) {
533
533
  super(message);
534
534
  this.code = code;
535
- this.path = path8;
535
+ this.path = path9;
536
536
  }
537
537
  code;
538
538
  path;
@@ -771,6 +771,37 @@ function createPtyManager(deps) {
771
771
  };
772
772
  }
773
773
 
774
+ // src/pty-health.ts
775
+ import fs3 from "fs";
776
+ import path3 from "path";
777
+ import { createRequire } from "module";
778
+ function findSpawnHelper(resolveFrom = import.meta.url) {
779
+ let pkgJson;
780
+ try {
781
+ pkgJson = createRequire(resolveFrom).resolve("node-pty/package.json");
782
+ } catch {
783
+ return null;
784
+ }
785
+ const root = path3.dirname(pkgJson);
786
+ const candidates = [
787
+ path3.join(root, "prebuilds", `${process.platform}-${process.arch}`, "spawn-helper"),
788
+ path3.join(root, "build", "Release", "spawn-helper")
789
+ ];
790
+ return candidates.find((p) => fs3.existsSync(p)) ?? null;
791
+ }
792
+ var EXEC_BITS = 73;
793
+ function ensureSpawnHelperExecutable(helper = findSpawnHelper()) {
794
+ if (!helper) return { path: null, executable: true, repaired: false };
795
+ try {
796
+ const mode = fs3.statSync(helper).mode;
797
+ if (mode & EXEC_BITS) return { path: helper, executable: true, repaired: false };
798
+ fs3.chmodSync(helper, (mode | 493) & 4095);
799
+ return { path: helper, executable: true, repaired: true };
800
+ } catch (err) {
801
+ return { path: helper, executable: false, repaired: false, error: err instanceof Error ? err.message : String(err) };
802
+ }
803
+ }
804
+
774
805
  // src/rpc/ai.ts
775
806
  async function credential(params) {
776
807
  let prefix;
@@ -821,9 +852,9 @@ async function pasteFile(params) {
821
852
  if (r.timedOut) throw new RpcFailure("timeout", "file.paste timed out");
822
853
  if (r.code !== 0) throw new RpcFailure("internal", `file.paste exited with code ${r.code}`);
823
854
  const lines = r.stdout.split("\n").map((l) => l.trim()).filter(Boolean);
824
- const path8 = lines[lines.length - 1] ?? "";
825
- if (!path8.startsWith("/")) throw new RpcFailure("internal", "unexpected paste output");
826
- return { path: path8 };
855
+ const path9 = lines[lines.length - 1] ?? "";
856
+ if (!path9.startsWith("/")) throw new RpcFailure("internal", "unexpected paste output");
857
+ return { path: path9 };
827
858
  }
828
859
 
829
860
  // src/rpc/tmux.ts
@@ -878,9 +909,9 @@ var handlers = {
878
909
  };
879
910
 
880
911
  // src/service/launchd.ts
881
- import fs3 from "fs";
912
+ import fs4 from "fs";
882
913
  import os3 from "os";
883
- import path3 from "path";
914
+ import path4 from "path";
884
915
  var LABEL = "dev.termhub.agent";
885
916
  function renderPlist({ label, node, script, logPath }) {
886
917
  const escape = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -919,7 +950,7 @@ function renderPlist({ label, node, script, logPath }) {
919
950
  `;
920
951
  }
921
952
  function plistPath() {
922
- return path3.join(os3.homedir(), "Library", "LaunchAgents", `${LABEL}.plist`);
953
+ return path4.join(os3.homedir(), "Library", "LaunchAgents", `${LABEL}.plist`);
923
954
  }
924
955
  function gui() {
925
956
  return `gui/${process.getuid ? process.getuid() : 0}`;
@@ -928,8 +959,8 @@ async function install(opts, deps = {}) {
928
959
  const runFn = deps.run ?? run;
929
960
  const file = plistPath();
930
961
  const plist = renderPlist({ label: LABEL, node: opts.node, script: opts.script, logPath: opts.logPath });
931
- fs3.mkdirSync(path3.dirname(file), { recursive: true });
932
- fs3.writeFileSync(file, plist, "utf8");
962
+ fs4.mkdirSync(path4.dirname(file), { recursive: true });
963
+ fs4.writeFileSync(file, plist, "utf8");
933
964
  await runFn("launchctl", ["bootout", gui(), file]);
934
965
  const result = await runFn("launchctl", ["bootstrap", gui(), file]);
935
966
  if (result.code !== 0) {
@@ -941,7 +972,7 @@ async function uninstall(deps = {}) {
941
972
  const file = plistPath();
942
973
  await runFn("launchctl", ["bootout", gui(), file]);
943
974
  try {
944
- fs3.unlinkSync(file);
975
+ fs4.unlinkSync(file);
945
976
  } catch (err) {
946
977
  if (err.code !== "ENOENT") throw err;
947
978
  }
@@ -962,7 +993,7 @@ async function status(deps = {}) {
962
993
  }
963
994
 
964
995
  // src/version.ts
965
- var AGENT_VERSION = "0.1.1";
996
+ var AGENT_VERSION = "0.1.2";
966
997
 
967
998
  // src/run.ts
968
999
  function detectOs(platform = process.platform) {
@@ -1040,6 +1071,9 @@ async function runAgent(config, opts) {
1040
1071
  process.exit(1);
1041
1072
  }
1042
1073
  const hello = await buildHello(osName);
1074
+ const helper = ensureSpawnHelperExecutable();
1075
+ if (helper.repaired) opts.log("spawn-helper exec bit repaired", { path: helper.path });
1076
+ else if (!helper.executable) opts.log("spawn-helper is not executable and could not be fixed", { path: helper.path, error: helper.error });
1043
1077
  const pty = createPtyManager({ log: opts.log });
1044
1078
  const dispatch = createDispatcher({ handlers, pty, log: opts.log });
1045
1079
  try {
@@ -1149,17 +1183,17 @@ function disconnectCommand() {
1149
1183
  }
1150
1184
 
1151
1185
  // src/doctor.ts
1152
- import fs4 from "fs";
1186
+ import fs5 from "fs";
1153
1187
  import os5 from "os";
1154
- import path4 from "path";
1188
+ import path5 from "path";
1155
1189
  var SERVER_CHECK_TIMEOUT_MS = 5e3;
1156
1190
  function defaultDoctorPaths() {
1157
1191
  const home = os5.homedir();
1158
- const paths = [home, path4.join(home, "Documents"), path4.join(home, "Desktop")];
1192
+ const paths = [home, path5.join(home, "Documents"), path5.join(home, "Desktop")];
1159
1193
  if (process.platform === "darwin") {
1160
1194
  try {
1161
- for (const entry of fs4.readdirSync("/Volumes", { withFileTypes: true })) {
1162
- if (entry.isDirectory()) paths.push(path4.join("/Volumes", entry.name));
1195
+ for (const entry of fs5.readdirSync("/Volumes", { withFileTypes: true })) {
1196
+ if (entry.isDirectory()) paths.push(path5.join("/Volumes", entry.name));
1163
1197
  }
1164
1198
  } catch {
1165
1199
  }
@@ -1172,7 +1206,7 @@ function pathErrorCode(err) {
1172
1206
  if (e?.code) return e.code;
1173
1207
  return err instanceof Error ? err.message : String(err);
1174
1208
  }
1175
- function checkPathAccess(p, fsImpl = fs4) {
1209
+ function checkPathAccess(p, fsImpl = fs5) {
1176
1210
  try {
1177
1211
  fsImpl.readdirSync(p);
1178
1212
  return { path: p, ok: true };
@@ -1184,7 +1218,7 @@ function fullDiskAccessNote(execPath = process.execPath) {
1184
1218
  return `Conceda Acesso Total ao Disco a ${execPath} em Ajustes \u2192 Privacidade e Seguran\xE7a \u2192 Acesso Total ao Disco`;
1185
1219
  }
1186
1220
  async function runDoctor(paths, deps = {}) {
1187
- const fsImpl = deps.fs ?? fs4;
1221
+ const fsImpl = deps.fs ?? fs5;
1188
1222
  const connect = deps.connect ?? checkServerConnection;
1189
1223
  const config = readConfig();
1190
1224
  const configReport = { ok: config !== null, path: configPath() };
@@ -1199,7 +1233,9 @@ async function runDoctor(paths, deps = {}) {
1199
1233
  nodePty = { ok: false, error: err instanceof Error ? err.message : String(err) };
1200
1234
  }
1201
1235
  const pathsReport = paths.map((p) => checkPathAccess(p, fsImpl));
1202
- return { config: configReport, server, tmux, nodePty, paths: pathsReport };
1236
+ const helper = ensureSpawnHelperExecutable();
1237
+ const spawnHelper = { ok: helper.executable, path: helper.path, repaired: helper.repaired, ...helper.error ? { error: helper.error } : {} };
1238
+ return { config: configReport, server, tmux, nodePty, spawnHelper, paths: pathsReport };
1203
1239
  }
1204
1240
  function mark(ok) {
1205
1241
  return ok ? "\u2713" : "\u2717";
@@ -1212,6 +1248,11 @@ function formatDoctor(report, deps = {}) {
1212
1248
  lines.push(`${mark(report.server.ok)} Servidor${report.server.ok ? "" : report.server.error ? `: ${report.server.error}` : ""}`);
1213
1249
  lines.push(`${mark(report.tmux.ok)} tmux${report.tmux.path ? ` (${report.tmux.path})` : ""}`);
1214
1250
  lines.push(`${mark(report.nodePty.ok)} node-pty${report.nodePty.ok ? "" : report.nodePty.error ? `: ${report.nodePty.error}` : ""}`);
1251
+ const sh2 = report.spawnHelper;
1252
+ if (sh2.path) {
1253
+ lines.push(`${mark(sh2.ok)} spawn-helper do node-pty ${sh2.ok ? sh2.repaired ? "(permiss\xE3o de execu\xE7\xE3o corrigida agora)" : "execut\xE1vel" : `sem permiss\xE3o de execu\xE7\xE3o${sh2.error ? `: ${sh2.error}` : ""}`}`);
1254
+ if (!sh2.ok) lines.push(` \u2192 rode: chmod +x "${sh2.path}"`);
1255
+ }
1215
1256
  for (const p of report.paths) {
1216
1257
  lines.push(`${mark(p.ok)} ${p.path}${p.ok ? "" : p.error ? `: ${p.error}` : ""}`);
1217
1258
  if (!p.ok && p.error === "eperm" && platform === "darwin") {
@@ -1247,18 +1288,18 @@ async function runCommand(log2) {
1247
1288
  import os7 from "os";
1248
1289
 
1249
1290
  // src/service/index.ts
1250
- import path7 from "path";
1291
+ import path8 from "path";
1251
1292
 
1252
1293
  // src/paths.ts
1253
1294
  import { realpathSync } from "fs";
1254
- import path5 from "path";
1295
+ import path6 from "path";
1255
1296
  import { pathToFileURL } from "url";
1256
1297
  function resolveScriptPath(argv1) {
1257
1298
  if (!argv1) return "";
1258
1299
  try {
1259
1300
  return realpathSync(argv1);
1260
1301
  } catch {
1261
- return path5.resolve(argv1);
1302
+ return path6.resolve(argv1);
1262
1303
  }
1263
1304
  }
1264
1305
  function isMainModule(importMetaUrl, argv1) {
@@ -1267,9 +1308,9 @@ function isMainModule(importMetaUrl, argv1) {
1267
1308
  }
1268
1309
 
1269
1310
  // src/service/systemd.ts
1270
- import fs5 from "fs";
1311
+ import fs6 from "fs";
1271
1312
  import os6 from "os";
1272
- import path6 from "path";
1313
+ import path7 from "path";
1273
1314
  var UNIT_NAME = "termhub-agent";
1274
1315
  function renderUnit({ node, script, logPath }) {
1275
1316
  const lines = [
@@ -1291,14 +1332,14 @@ function renderUnit({ node, script, logPath }) {
1291
1332
  return lines.join("\n");
1292
1333
  }
1293
1334
  function unitPath() {
1294
- return path6.join(os6.homedir(), ".config", "systemd", "user", `${UNIT_NAME}.service`);
1335
+ return path7.join(os6.homedir(), ".config", "systemd", "user", `${UNIT_NAME}.service`);
1295
1336
  }
1296
1337
  async function install2(opts, deps = {}) {
1297
1338
  const runFn = deps.run ?? run;
1298
1339
  const file = unitPath();
1299
1340
  const unit = renderUnit({ node: opts.node, script: opts.script, logPath: opts.logPath });
1300
- fs5.mkdirSync(path6.dirname(file), { recursive: true });
1301
- fs5.writeFileSync(file, unit, "utf8");
1341
+ fs6.mkdirSync(path7.dirname(file), { recursive: true });
1342
+ fs6.writeFileSync(file, unit, "utf8");
1302
1343
  const reload = await runFn("systemctl", ["--user", "daemon-reload"]);
1303
1344
  if (reload.code !== 0) throw new Error(`systemctl daemon-reload failed (code ${reload.code}): ${reload.stderr.trim()}`);
1304
1345
  const enable = await runFn("systemctl", ["--user", "enable", "--now", UNIT_NAME]);
@@ -1309,7 +1350,7 @@ async function uninstall2(deps = {}) {
1309
1350
  const runFn = deps.run ?? run;
1310
1351
  await runFn("systemctl", ["--user", "disable", "--now", UNIT_NAME]);
1311
1352
  try {
1312
- fs5.unlinkSync(unitPath());
1353
+ fs6.unlinkSync(unitPath());
1313
1354
  } catch (err) {
1314
1355
  if (err.code !== "ENOENT") throw err;
1315
1356
  }
@@ -1326,7 +1367,7 @@ function serviceFileOptions() {
1326
1367
  return {
1327
1368
  node: process.execPath,
1328
1369
  script: resolveScriptPath(process.argv[1]),
1329
- logPath: path7.join(agentHome(), "agent.log")
1370
+ logPath: path8.join(agentHome(), "agent.log")
1330
1371
  };
1331
1372
  }
1332
1373
  function assertSupported(platform) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@termhub/agent",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Agente do termhub: conecta esta máquina ao servidor por WebSocket de saída (sem SSH) e expõe os terminais tmux no navegador.",
5
5
  "license": "MIT",
6
6
  "private": false,