@termhub/agent 0.1.0 → 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 +9 -1
  2. package/dist/cli.js +77 -32
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -21,7 +21,8 @@ termhub-agent connect --url https://app.termhub.dev
21
21
 
22
22
  You'll be prompted to paste an agent token generated from the termhub web app (Machines → Add
23
23
  machine). `--token <token>` and the `TERMHUB_URL`/`TERMHUB_TOKEN` environment variables work too,
24
- for non-interactive setups. On success the agent keeps running in the foreground until Ctrl-C.
24
+ for non-interactive setups. On success the pairing is saved and the command exits; start the agent
25
+ with `termhub-agent service install` (background) or `termhub-agent run` (foreground).
25
26
 
26
27
  ## Run as a background service
27
28
 
@@ -51,3 +52,10 @@ macOS's TCC (Transparency, Consent and Control) can block the agent from listing
51
52
  special privileges. If `doctor` or `service install` reports this, grant the agent's Node binary
52
53
  Full Disk Access: **Ajustes → Privacidade e Segurança → Acesso Total ao Disco**, then add the
53
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;
@@ -689,6 +689,7 @@ function createPtyManager(deps) {
689
689
  const spawn = await resolveSpawn();
690
690
  proc = spawn(tmux, ["-u", "new-session", "-A", "-s", params.session, "-c", cwd], { name: "xterm-256color", cols, rows, cwd, env });
691
691
  } catch (err) {
692
+ deps.log("pty open failed", { ch, session: params.session, tmux, cwd: params.cwd, error: err instanceof Error ? err.message : String(err) });
692
693
  socket.sendControl({
693
694
  type: "open_error",
694
695
  ch,
@@ -770,6 +771,37 @@ function createPtyManager(deps) {
770
771
  };
771
772
  }
772
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
+
773
805
  // src/rpc/ai.ts
774
806
  async function credential(params) {
775
807
  let prefix;
@@ -820,9 +852,9 @@ async function pasteFile(params) {
820
852
  if (r.timedOut) throw new RpcFailure("timeout", "file.paste timed out");
821
853
  if (r.code !== 0) throw new RpcFailure("internal", `file.paste exited with code ${r.code}`);
822
854
  const lines = r.stdout.split("\n").map((l) => l.trim()).filter(Boolean);
823
- const path8 = lines[lines.length - 1] ?? "";
824
- if (!path8.startsWith("/")) throw new RpcFailure("internal", "unexpected paste output");
825
- 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 };
826
858
  }
827
859
 
828
860
  // src/rpc/tmux.ts
@@ -877,9 +909,9 @@ var handlers = {
877
909
  };
878
910
 
879
911
  // src/service/launchd.ts
880
- import fs3 from "fs";
912
+ import fs4 from "fs";
881
913
  import os3 from "os";
882
- import path3 from "path";
914
+ import path4 from "path";
883
915
  var LABEL = "dev.termhub.agent";
884
916
  function renderPlist({ label, node, script, logPath }) {
885
917
  const escape = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -918,7 +950,7 @@ function renderPlist({ label, node, script, logPath }) {
918
950
  `;
919
951
  }
920
952
  function plistPath() {
921
- return path3.join(os3.homedir(), "Library", "LaunchAgents", `${LABEL}.plist`);
953
+ return path4.join(os3.homedir(), "Library", "LaunchAgents", `${LABEL}.plist`);
922
954
  }
923
955
  function gui() {
924
956
  return `gui/${process.getuid ? process.getuid() : 0}`;
@@ -927,8 +959,8 @@ async function install(opts, deps = {}) {
927
959
  const runFn = deps.run ?? run;
928
960
  const file = plistPath();
929
961
  const plist = renderPlist({ label: LABEL, node: opts.node, script: opts.script, logPath: opts.logPath });
930
- fs3.mkdirSync(path3.dirname(file), { recursive: true });
931
- fs3.writeFileSync(file, plist, "utf8");
962
+ fs4.mkdirSync(path4.dirname(file), { recursive: true });
963
+ fs4.writeFileSync(file, plist, "utf8");
932
964
  await runFn("launchctl", ["bootout", gui(), file]);
933
965
  const result = await runFn("launchctl", ["bootstrap", gui(), file]);
934
966
  if (result.code !== 0) {
@@ -940,7 +972,7 @@ async function uninstall(deps = {}) {
940
972
  const file = plistPath();
941
973
  await runFn("launchctl", ["bootout", gui(), file]);
942
974
  try {
943
- fs3.unlinkSync(file);
975
+ fs4.unlinkSync(file);
944
976
  } catch (err) {
945
977
  if (err.code !== "ENOENT") throw err;
946
978
  }
@@ -961,7 +993,7 @@ async function status(deps = {}) {
961
993
  }
962
994
 
963
995
  // src/version.ts
964
- var AGENT_VERSION = "0.1.0";
996
+ var AGENT_VERSION = "0.1.2";
965
997
 
966
998
  // src/run.ts
967
999
  function detectOs(platform = process.platform) {
@@ -1039,6 +1071,9 @@ async function runAgent(config, opts) {
1039
1071
  process.exit(1);
1040
1072
  }
1041
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 });
1042
1077
  const pty = createPtyManager({ log: opts.log });
1043
1078
  const dispatch = createDispatcher({ handlers, pty, log: opts.log });
1044
1079
  try {
@@ -1134,8 +1169,11 @@ async function connectCommand(values, log2) {
1134
1169
  }
1135
1170
  const config = { url, token, machine_id: "", machine_name: "", created_at: (/* @__PURE__ */ new Date()).toISOString() };
1136
1171
  writeConfig(config);
1137
- console.log("Conectado. Agora rode: termhub-agent service install");
1138
- await runForegroundUntilSignal(config, log2);
1172
+ console.log("Conectado. Configura\xE7\xE3o salva.");
1173
+ console.log("Pr\xF3ximos passos:");
1174
+ console.log(" termhub-agent service install # mant\xE9m o agente rodando em segundo plano");
1175
+ console.log(" termhub-agent doctor # confere tmux e acesso \xE0s pastas");
1176
+ console.log("(ou termhub-agent run para rodar em primeiro plano)");
1139
1177
  }
1140
1178
 
1141
1179
  // src/commands/disconnect.ts
@@ -1145,17 +1183,17 @@ function disconnectCommand() {
1145
1183
  }
1146
1184
 
1147
1185
  // src/doctor.ts
1148
- import fs4 from "fs";
1186
+ import fs5 from "fs";
1149
1187
  import os5 from "os";
1150
- import path4 from "path";
1188
+ import path5 from "path";
1151
1189
  var SERVER_CHECK_TIMEOUT_MS = 5e3;
1152
1190
  function defaultDoctorPaths() {
1153
1191
  const home = os5.homedir();
1154
- const paths = [home, path4.join(home, "Documents"), path4.join(home, "Desktop")];
1192
+ const paths = [home, path5.join(home, "Documents"), path5.join(home, "Desktop")];
1155
1193
  if (process.platform === "darwin") {
1156
1194
  try {
1157
- for (const entry of fs4.readdirSync("/Volumes", { withFileTypes: true })) {
1158
- 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));
1159
1197
  }
1160
1198
  } catch {
1161
1199
  }
@@ -1168,7 +1206,7 @@ function pathErrorCode(err) {
1168
1206
  if (e?.code) return e.code;
1169
1207
  return err instanceof Error ? err.message : String(err);
1170
1208
  }
1171
- function checkPathAccess(p, fsImpl = fs4) {
1209
+ function checkPathAccess(p, fsImpl = fs5) {
1172
1210
  try {
1173
1211
  fsImpl.readdirSync(p);
1174
1212
  return { path: p, ok: true };
@@ -1180,7 +1218,7 @@ function fullDiskAccessNote(execPath = process.execPath) {
1180
1218
  return `Conceda Acesso Total ao Disco a ${execPath} em Ajustes \u2192 Privacidade e Seguran\xE7a \u2192 Acesso Total ao Disco`;
1181
1219
  }
1182
1220
  async function runDoctor(paths, deps = {}) {
1183
- const fsImpl = deps.fs ?? fs4;
1221
+ const fsImpl = deps.fs ?? fs5;
1184
1222
  const connect = deps.connect ?? checkServerConnection;
1185
1223
  const config = readConfig();
1186
1224
  const configReport = { ok: config !== null, path: configPath() };
@@ -1195,7 +1233,9 @@ async function runDoctor(paths, deps = {}) {
1195
1233
  nodePty = { ok: false, error: err instanceof Error ? err.message : String(err) };
1196
1234
  }
1197
1235
  const pathsReport = paths.map((p) => checkPathAccess(p, fsImpl));
1198
- 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 };
1199
1239
  }
1200
1240
  function mark(ok) {
1201
1241
  return ok ? "\u2713" : "\u2717";
@@ -1208,6 +1248,11 @@ function formatDoctor(report, deps = {}) {
1208
1248
  lines.push(`${mark(report.server.ok)} Servidor${report.server.ok ? "" : report.server.error ? `: ${report.server.error}` : ""}`);
1209
1249
  lines.push(`${mark(report.tmux.ok)} tmux${report.tmux.path ? ` (${report.tmux.path})` : ""}`);
1210
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
+ }
1211
1256
  for (const p of report.paths) {
1212
1257
  lines.push(`${mark(p.ok)} ${p.path}${p.ok ? "" : p.error ? `: ${p.error}` : ""}`);
1213
1258
  if (!p.ok && p.error === "eperm" && platform === "darwin") {
@@ -1243,18 +1288,18 @@ async function runCommand(log2) {
1243
1288
  import os7 from "os";
1244
1289
 
1245
1290
  // src/service/index.ts
1246
- import path7 from "path";
1291
+ import path8 from "path";
1247
1292
 
1248
1293
  // src/paths.ts
1249
1294
  import { realpathSync } from "fs";
1250
- import path5 from "path";
1295
+ import path6 from "path";
1251
1296
  import { pathToFileURL } from "url";
1252
1297
  function resolveScriptPath(argv1) {
1253
1298
  if (!argv1) return "";
1254
1299
  try {
1255
1300
  return realpathSync(argv1);
1256
1301
  } catch {
1257
- return path5.resolve(argv1);
1302
+ return path6.resolve(argv1);
1258
1303
  }
1259
1304
  }
1260
1305
  function isMainModule(importMetaUrl, argv1) {
@@ -1263,9 +1308,9 @@ function isMainModule(importMetaUrl, argv1) {
1263
1308
  }
1264
1309
 
1265
1310
  // src/service/systemd.ts
1266
- import fs5 from "fs";
1311
+ import fs6 from "fs";
1267
1312
  import os6 from "os";
1268
- import path6 from "path";
1313
+ import path7 from "path";
1269
1314
  var UNIT_NAME = "termhub-agent";
1270
1315
  function renderUnit({ node, script, logPath }) {
1271
1316
  const lines = [
@@ -1287,14 +1332,14 @@ function renderUnit({ node, script, logPath }) {
1287
1332
  return lines.join("\n");
1288
1333
  }
1289
1334
  function unitPath() {
1290
- return path6.join(os6.homedir(), ".config", "systemd", "user", `${UNIT_NAME}.service`);
1335
+ return path7.join(os6.homedir(), ".config", "systemd", "user", `${UNIT_NAME}.service`);
1291
1336
  }
1292
1337
  async function install2(opts, deps = {}) {
1293
1338
  const runFn = deps.run ?? run;
1294
1339
  const file = unitPath();
1295
1340
  const unit = renderUnit({ node: opts.node, script: opts.script, logPath: opts.logPath });
1296
- fs5.mkdirSync(path6.dirname(file), { recursive: true });
1297
- fs5.writeFileSync(file, unit, "utf8");
1341
+ fs6.mkdirSync(path7.dirname(file), { recursive: true });
1342
+ fs6.writeFileSync(file, unit, "utf8");
1298
1343
  const reload = await runFn("systemctl", ["--user", "daemon-reload"]);
1299
1344
  if (reload.code !== 0) throw new Error(`systemctl daemon-reload failed (code ${reload.code}): ${reload.stderr.trim()}`);
1300
1345
  const enable = await runFn("systemctl", ["--user", "enable", "--now", UNIT_NAME]);
@@ -1305,7 +1350,7 @@ async function uninstall2(deps = {}) {
1305
1350
  const runFn = deps.run ?? run;
1306
1351
  await runFn("systemctl", ["--user", "disable", "--now", UNIT_NAME]);
1307
1352
  try {
1308
- fs5.unlinkSync(unitPath());
1353
+ fs6.unlinkSync(unitPath());
1309
1354
  } catch (err) {
1310
1355
  if (err.code !== "ENOENT") throw err;
1311
1356
  }
@@ -1322,7 +1367,7 @@ function serviceFileOptions() {
1322
1367
  return {
1323
1368
  node: process.execPath,
1324
1369
  script: resolveScriptPath(process.argv[1]),
1325
- logPath: path7.join(agentHome(), "agent.log")
1370
+ logPath: path8.join(agentHome(), "agent.log")
1326
1371
  };
1327
1372
  }
1328
1373
  function assertSupported(platform) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@termhub/agent",
3
- "version": "0.1.0",
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,