@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.
- package/README.md +9 -1
- package/dist/cli.js +77 -32
- 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
|
|
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,
|
|
532
|
+
constructor(code, message, path9) {
|
|
533
533
|
super(message);
|
|
534
534
|
this.code = code;
|
|
535
|
-
this.path =
|
|
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
|
|
824
|
-
if (!
|
|
825
|
-
return { path:
|
|
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
|
|
912
|
+
import fs4 from "fs";
|
|
881
913
|
import os3 from "os";
|
|
882
|
-
import
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
@@ -918,7 +950,7 @@ function renderPlist({ label, node, script, logPath }) {
|
|
|
918
950
|
`;
|
|
919
951
|
}
|
|
920
952
|
function plistPath() {
|
|
921
|
-
return
|
|
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
|
-
|
|
931
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
1138
|
-
|
|
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
|
|
1186
|
+
import fs5 from "fs";
|
|
1149
1187
|
import os5 from "os";
|
|
1150
|
-
import
|
|
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,
|
|
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
|
|
1158
|
-
if (entry.isDirectory()) paths.push(
|
|
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 =
|
|
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 ??
|
|
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
|
-
|
|
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
|
|
1291
|
+
import path8 from "path";
|
|
1247
1292
|
|
|
1248
1293
|
// src/paths.ts
|
|
1249
1294
|
import { realpathSync } from "fs";
|
|
1250
|
-
import
|
|
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
|
|
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
|
|
1311
|
+
import fs6 from "fs";
|
|
1267
1312
|
import os6 from "os";
|
|
1268
|
-
import
|
|
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
|
|
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
|
-
|
|
1297
|
-
|
|
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
|
-
|
|
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:
|
|
1370
|
+
logPath: path8.join(agentHome(), "agent.log")
|
|
1326
1371
|
};
|
|
1327
1372
|
}
|
|
1328
1373
|
function assertSupported(platform) {
|
package/package.json
CHANGED