@termhub/agent 0.2.0 → 0.2.1

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/dist/cli.js +215 -147
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -77,7 +77,9 @@ var RPC = {
77
77
  token: z.string().min(1).max(256).regex(/^[A-Za-z0-9_-]+$/),
78
78
  claude_dirs: z.array(machinePath).max(16).optional()
79
79
  }), z.object({ home: z.string(), claude: z.enum(["installed", "skipped"]), codex: z.enum(["installed", "skipped"]), claude_dirs: z.array(z.string()).optional() }), 15e3),
80
- "hooks.uninstall": def(z.object({ claude_dirs: z.array(machinePath).max(16).optional() }), z.object({ removed: z.boolean() }), 15e3)
80
+ "hooks.uninstall": def(z.object({ claude_dirs: z.array(machinePath).max(16).optional() }), z.object({ removed: z.boolean() }), 15e3),
81
+ /** Installs `version` of @termhub/agent with npm; when the agent runs as a service it then exits so the service relaunches the new code (since agent 0.2.1). */
82
+ "agent.update": def(z.object({ version: z.string().regex(/^\d+\.\d+\.\d+$/) }), z.object({ installed_version: z.string(), restart: z.enum(["service", "manual"]) }), 18e4)
81
83
  };
82
84
  var RPC_METHODS = Object.keys(RPC);
83
85
  var rpcMethod = z.enum(RPC_METHODS);
@@ -372,7 +374,7 @@ function deleteConfig() {
372
374
  }
373
375
 
374
376
  // src/run.ts
375
- import os5 from "os";
377
+ import os6 from "os";
376
378
 
377
379
  // src/exec.ts
378
380
  import { execFile } from "child_process";
@@ -666,10 +668,10 @@ function stripCodexConfig(current) {
666
668
  // src/exec.ts
667
669
  var DEFAULT_TIMEOUT_MS = 8e3;
668
670
  var RpcFailure = class extends Error {
669
- constructor(code, message, path10) {
671
+ constructor(code, message, path11) {
670
672
  super(message);
671
673
  this.code = code;
672
- this.path = path10;
674
+ this.path = path11;
673
675
  }
674
676
  code;
675
677
  path;
@@ -1119,9 +1121,9 @@ async function pasteFile(params) {
1119
1121
  if (r.timedOut) throw new RpcFailure("timeout", "file.paste timed out");
1120
1122
  if (r.code !== 0) throw new RpcFailure("internal", `file.paste exited with code ${r.code}`);
1121
1123
  const lines = r.stdout.split("\n").map((l) => l.trim()).filter(Boolean);
1122
- const path10 = lines[lines.length - 1] ?? "";
1123
- if (!path10.startsWith("/")) throw new RpcFailure("internal", "unexpected paste output");
1124
- return { path: path10 };
1124
+ const path11 = lines[lines.length - 1] ?? "";
1125
+ if (!path11.startsWith("/")) throw new RpcFailure("internal", "unexpected paste output");
1126
+ return { path: path11 };
1125
1127
  }
1126
1128
 
1127
1129
  // src/rpc/tmux.ts
@@ -1199,28 +1201,35 @@ async function detect(_params) {
1199
1201
  return { os: os9, tools: capabilities };
1200
1202
  }
1201
1203
 
1202
- // src/rpc/index.ts
1203
- var handlers = {
1204
- "tmux.list": list2,
1205
- "tmux.kill": kill,
1206
- "tmux.capture": capture,
1207
- "tmux.ensure": ensure,
1208
- "tmux.sendText": sendText,
1209
- "tmux.sendKey": sendKey,
1210
- "tools.detect": detect,
1211
- "hw.probe": probe,
1212
- "fs.list": list,
1213
- "fs.mkdir": mkdir,
1214
- "ai.credential": credential,
1215
- "file.paste": pasteFile,
1216
- "hooks.install": install,
1217
- "hooks.uninstall": uninstall
1218
- };
1204
+ // src/rpc/update.ts
1205
+ import { existsSync, realpathSync as realpathSync2 } from "fs";
1206
+ import { readFile as readFile2 } from "fs/promises";
1207
+ import path9 from "path";
1208
+
1209
+ // src/paths.ts
1210
+ import { realpathSync } from "fs";
1211
+ import path5 from "path";
1212
+ import { pathToFileURL } from "url";
1213
+ function resolveScriptPath(argv1) {
1214
+ if (!argv1) return "";
1215
+ try {
1216
+ return realpathSync(argv1);
1217
+ } catch {
1218
+ return path5.resolve(argv1);
1219
+ }
1220
+ }
1221
+ function isMainModule(importMetaUrl, argv1) {
1222
+ if (!argv1) return false;
1223
+ return importMetaUrl === pathToFileURL(resolveScriptPath(argv1)).href;
1224
+ }
1225
+
1226
+ // src/service/index.ts
1227
+ import path8 from "path";
1219
1228
 
1220
1229
  // src/service/launchd.ts
1221
1230
  import fs4 from "fs";
1222
1231
  import os4 from "os";
1223
- import path5 from "path";
1232
+ import path6 from "path";
1224
1233
  var LABEL = "dev.termhub.agent";
1225
1234
  function renderPlist({ label, node, script, logPath }) {
1226
1235
  const escape = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -1259,7 +1268,7 @@ function renderPlist({ label, node, script, logPath }) {
1259
1268
  `;
1260
1269
  }
1261
1270
  function plistPath() {
1262
- return path5.join(os4.homedir(), "Library", "LaunchAgents", `${LABEL}.plist`);
1271
+ return path6.join(os4.homedir(), "Library", "LaunchAgents", `${LABEL}.plist`);
1263
1272
  }
1264
1273
  function gui() {
1265
1274
  return `gui/${process.getuid ? process.getuid() : 0}`;
@@ -1268,7 +1277,7 @@ async function install2(opts, deps = {}) {
1268
1277
  const runFn = deps.run ?? run;
1269
1278
  const file = plistPath();
1270
1279
  const plist = renderPlist({ label: LABEL, node: opts.node, script: opts.script, logPath: opts.logPath });
1271
- fs4.mkdirSync(path5.dirname(file), { recursive: true });
1280
+ fs4.mkdirSync(path6.dirname(file), { recursive: true });
1272
1281
  fs4.writeFileSync(file, plist, "utf8");
1273
1282
  await runFn("launchctl", ["bootout", gui(), file]);
1274
1283
  const result = await runFn("launchctl", ["bootstrap", gui(), file]);
@@ -1301,8 +1310,175 @@ async function status(deps = {}) {
1301
1310
  return result.code === 0;
1302
1311
  }
1303
1312
 
1313
+ // src/service/systemd.ts
1314
+ import fs5 from "fs";
1315
+ import os5 from "os";
1316
+ import path7 from "path";
1317
+ var UNIT_NAME = "termhub-agent";
1318
+ function renderUnit({ node, script, logPath }) {
1319
+ const lines = [
1320
+ "[Unit]",
1321
+ "Description=termhub agent",
1322
+ "After=network-online.target",
1323
+ "",
1324
+ "[Service]",
1325
+ `ExecStart=${node} ${script} run`,
1326
+ "Restart=on-failure",
1327
+ "RestartSec=2",
1328
+ "RestartPreventExitStatus=78",
1329
+ `Environment=PATH=${agentEnv().PATH ?? ""}`
1330
+ ];
1331
+ if (logPath) {
1332
+ lines.push(`StandardOutput=append:${logPath}`, `StandardError=append:${logPath}`);
1333
+ }
1334
+ lines.push("", "[Install]", "WantedBy=default.target", "");
1335
+ return lines.join("\n");
1336
+ }
1337
+ function unitPath() {
1338
+ return path7.join(os5.homedir(), ".config", "systemd", "user", `${UNIT_NAME}.service`);
1339
+ }
1340
+ async function install3(opts, deps = {}) {
1341
+ const runFn = deps.run ?? run;
1342
+ const file = unitPath();
1343
+ const unit = renderUnit({ node: opts.node, script: opts.script, logPath: opts.logPath });
1344
+ fs5.mkdirSync(path7.dirname(file), { recursive: true });
1345
+ fs5.writeFileSync(file, unit, "utf8");
1346
+ const reload = await runFn("systemctl", ["--user", "daemon-reload"]);
1347
+ if (reload.code !== 0) throw new Error(`systemctl daemon-reload failed (code ${reload.code}): ${reload.stderr.trim()}`);
1348
+ const enable = await runFn("systemctl", ["--user", "enable", "--now", UNIT_NAME]);
1349
+ if (enable.code !== 0) throw new Error(`systemctl enable --now failed (code ${enable.code}): ${enable.stderr.trim()}`);
1350
+ console.log("Para o agente continuar ap\xF3s o logout: loginctl enable-linger $USER");
1351
+ }
1352
+ async function uninstall3(deps = {}) {
1353
+ const runFn = deps.run ?? run;
1354
+ await runFn("systemctl", ["--user", "disable", "--now", UNIT_NAME]);
1355
+ try {
1356
+ fs5.unlinkSync(unitPath());
1357
+ } catch (err) {
1358
+ if (err.code !== "ENOENT") throw err;
1359
+ }
1360
+ await runFn("systemctl", ["--user", "daemon-reload"]);
1361
+ }
1362
+ async function status2(deps = {}) {
1363
+ const runFn = deps.run ?? run;
1364
+ const result = await runFn("systemctl", ["--user", "is-active", UNIT_NAME]);
1365
+ return result.code === 0;
1366
+ }
1367
+
1368
+ // src/service/index.ts
1369
+ function serviceFileOptions() {
1370
+ return {
1371
+ node: process.execPath,
1372
+ script: resolveScriptPath(process.argv[1]),
1373
+ logPath: path8.join(agentHome(), "agent.log")
1374
+ };
1375
+ }
1376
+ function assertSupported(platform) {
1377
+ if (platform !== "darwin" && platform !== "linux") throw new Error("Sistema n\xE3o suportado");
1378
+ }
1379
+ async function install4() {
1380
+ const platform = process.platform;
1381
+ assertSupported(platform);
1382
+ const opts = serviceFileOptions();
1383
+ if (platform === "darwin") await install2(opts);
1384
+ else await install3(opts);
1385
+ }
1386
+ async function uninstall4() {
1387
+ const platform = process.platform;
1388
+ assertSupported(platform);
1389
+ if (platform === "darwin") await uninstall2();
1390
+ else await uninstall3();
1391
+ }
1392
+ async function status3() {
1393
+ const platform = process.platform;
1394
+ assertSupported(platform);
1395
+ if (platform === "darwin") return status();
1396
+ return status2();
1397
+ }
1398
+
1304
1399
  // src/version.ts
1305
- var AGENT_VERSION = "0.2.0";
1400
+ var AGENT_VERSION = "0.2.1";
1401
+
1402
+ // src/rpc/update.ts
1403
+ var PACKAGE = "@termhub/agent";
1404
+ var NPM_TIMEOUT_MS = 15e4;
1405
+ var EXIT_DELAY_MS = 750;
1406
+ function npmCliBesideNode(execPath = process.execPath) {
1407
+ const dir = path9.dirname(execPath);
1408
+ const symlink = path9.join(dir, "npm");
1409
+ if (existsSync(symlink)) return realpathSync2(symlink);
1410
+ const fallback = path9.join(dir, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js");
1411
+ return existsSync(fallback) ? fallback : null;
1412
+ }
1413
+ async function installedAgentVersion(argv1 = process.argv[1]) {
1414
+ const script = resolveScriptPath(argv1);
1415
+ if (!script) return null;
1416
+ try {
1417
+ const pkg = JSON.parse(await readFile2(path9.join(path9.dirname(script), "..", "package.json"), "utf8"));
1418
+ return typeof pkg.version === "string" ? pkg.version : null;
1419
+ } catch {
1420
+ return null;
1421
+ }
1422
+ }
1423
+ var defaultDeps = {
1424
+ run,
1425
+ execPath: process.execPath,
1426
+ npmCli: npmCliBesideNode,
1427
+ installedVersion: installedAgentVersion,
1428
+ serviceInstalled: () => status3(),
1429
+ exit: (code) => process.exit(code),
1430
+ log: (msg, meta) => console.error(meta ? `[termhub-agent] ${msg} ${JSON.stringify(meta)}` : `[termhub-agent] ${msg}`)
1431
+ };
1432
+ var inFlight = null;
1433
+ async function updateAgent(params, deps = defaultDeps) {
1434
+ if (inFlight) throw new RpcFailure("failed", "update already running");
1435
+ inFlight = doUpdate(params.version, deps);
1436
+ try {
1437
+ return await inFlight;
1438
+ } finally {
1439
+ inFlight = null;
1440
+ }
1441
+ }
1442
+ async function doUpdate(version, deps) {
1443
+ deps.log("update starting", { from: AGENT_VERSION, to: version });
1444
+ const npmCli = deps.npmCli();
1445
+ if (!npmCli) throw new RpcFailure("notfound", "npm not found beside node");
1446
+ const r = await deps.run(deps.execPath, [npmCli, "install", "-g", "--no-fund", "--no-audit", `${PACKAGE}@${version}`], { timeoutMs: NPM_TIMEOUT_MS });
1447
+ if (r.error === "enoent") throw new RpcFailure("notfound", "npm not found on this machine");
1448
+ if (r.timedOut) throw new RpcFailure("timeout", "npm install timed out");
1449
+ if (r.code !== 0) {
1450
+ deps.log("update failed", { to: version, code: r.code });
1451
+ throw new RpcFailure("failed", `npm exited with code ${r.code ?? "unknown"}`);
1452
+ }
1453
+ const installed = await deps.installedVersion();
1454
+ if (installed !== version) throw new RpcFailure("failed", `installed version mismatch (${installed ?? "unknown"})`);
1455
+ const restart = await deps.serviceInstalled() ? "service" : "manual";
1456
+ deps.log("update installed", { from: AGENT_VERSION, to: version, restart });
1457
+ if (restart === "service") {
1458
+ setTimeout(() => deps.exit(1), EXIT_DELAY_MS);
1459
+ }
1460
+ return { installed_version: installed, restart };
1461
+ }
1462
+ var update = (params) => updateAgent(params);
1463
+
1464
+ // src/rpc/index.ts
1465
+ var handlers = {
1466
+ "tmux.list": list2,
1467
+ "tmux.kill": kill,
1468
+ "tmux.capture": capture,
1469
+ "tmux.ensure": ensure,
1470
+ "tmux.sendText": sendText,
1471
+ "tmux.sendKey": sendKey,
1472
+ "tools.detect": detect,
1473
+ "hw.probe": probe,
1474
+ "fs.list": list,
1475
+ "fs.mkdir": mkdir,
1476
+ "ai.credential": credential,
1477
+ "file.paste": pasteFile,
1478
+ "hooks.install": install,
1479
+ "hooks.uninstall": uninstall,
1480
+ "agent.update": update
1481
+ };
1306
1482
 
1307
1483
  // src/run.ts
1308
1484
  function detectOs(platform = process.platform) {
@@ -1322,7 +1498,7 @@ async function buildHello(osName) {
1322
1498
  agent_version: AGENT_VERSION,
1323
1499
  os: osName,
1324
1500
  arch: process.arch,
1325
- hostname: os5.hostname(),
1501
+ hostname: os6.hostname(),
1326
1502
  tmux: tools.includes("tmux"),
1327
1503
  tools
1328
1504
  };
@@ -1342,7 +1518,7 @@ async function checkServerConnection(config, timeoutMs = 5e3) {
1342
1518
  {
1343
1519
  url: config.url,
1344
1520
  token: config.token,
1345
- hello: { agent_version: AGENT_VERSION, os: osName, arch: process.arch, hostname: os5.hostname(), tmux: false, tools: [], probe: true },
1521
+ hello: { agent_version: AGENT_VERSION, os: osName, arch: process.arch, hostname: os6.hostname(), tmux: false, tools: [], probe: true },
1346
1522
  onServerMessage: () => {
1347
1523
  },
1348
1524
  onStream: () => {
@@ -1492,17 +1668,17 @@ function disconnectCommand() {
1492
1668
  }
1493
1669
 
1494
1670
  // src/doctor.ts
1495
- import fs5 from "fs";
1496
- import os6 from "os";
1497
- import path6 from "path";
1671
+ import fs6 from "fs";
1672
+ import os7 from "os";
1673
+ import path10 from "path";
1498
1674
  var SERVER_CHECK_TIMEOUT_MS = 5e3;
1499
1675
  function defaultDoctorPaths() {
1500
- const home = os6.homedir();
1501
- const paths = [home, path6.join(home, "Documents"), path6.join(home, "Desktop")];
1676
+ const home = os7.homedir();
1677
+ const paths = [home, path10.join(home, "Documents"), path10.join(home, "Desktop")];
1502
1678
  if (process.platform === "darwin") {
1503
1679
  try {
1504
- for (const entry of fs5.readdirSync("/Volumes", { withFileTypes: true })) {
1505
- if (entry.isDirectory()) paths.push(path6.join("/Volumes", entry.name));
1680
+ for (const entry of fs6.readdirSync("/Volumes", { withFileTypes: true })) {
1681
+ if (entry.isDirectory()) paths.push(path10.join("/Volumes", entry.name));
1506
1682
  }
1507
1683
  } catch {
1508
1684
  }
@@ -1515,7 +1691,7 @@ function pathErrorCode(err) {
1515
1691
  if (e?.code) return e.code;
1516
1692
  return err instanceof Error ? err.message : String(err);
1517
1693
  }
1518
- function checkPathAccess(p, fsImpl = fs5) {
1694
+ function checkPathAccess(p, fsImpl = fs6) {
1519
1695
  try {
1520
1696
  fsImpl.readdirSync(p);
1521
1697
  return { path: p, ok: true };
@@ -1527,7 +1703,7 @@ function fullDiskAccessNote(execPath = process.execPath) {
1527
1703
  return `Conceda Acesso Total ao Disco a ${execPath} em Ajustes \u2192 Privacidade e Seguran\xE7a \u2192 Acesso Total ao Disco`;
1528
1704
  }
1529
1705
  async function runDoctor(paths, deps = {}) {
1530
- const fsImpl = deps.fs ?? fs5;
1706
+ const fsImpl = deps.fs ?? fs6;
1531
1707
  const connect = deps.connect ?? checkServerConnection;
1532
1708
  const config = readConfig();
1533
1709
  const configReport = { ok: config !== null, path: configPath() };
@@ -1595,114 +1771,6 @@ async function runCommand(log2) {
1595
1771
 
1596
1772
  // src/commands/service.ts
1597
1773
  import os8 from "os";
1598
-
1599
- // src/service/index.ts
1600
- import path9 from "path";
1601
-
1602
- // src/paths.ts
1603
- import { realpathSync } from "fs";
1604
- import path7 from "path";
1605
- import { pathToFileURL } from "url";
1606
- function resolveScriptPath(argv1) {
1607
- if (!argv1) return "";
1608
- try {
1609
- return realpathSync(argv1);
1610
- } catch {
1611
- return path7.resolve(argv1);
1612
- }
1613
- }
1614
- function isMainModule(importMetaUrl, argv1) {
1615
- if (!argv1) return false;
1616
- return importMetaUrl === pathToFileURL(resolveScriptPath(argv1)).href;
1617
- }
1618
-
1619
- // src/service/systemd.ts
1620
- import fs6 from "fs";
1621
- import os7 from "os";
1622
- import path8 from "path";
1623
- var UNIT_NAME = "termhub-agent";
1624
- function renderUnit({ node, script, logPath }) {
1625
- const lines = [
1626
- "[Unit]",
1627
- "Description=termhub agent",
1628
- "After=network-online.target",
1629
- "",
1630
- "[Service]",
1631
- `ExecStart=${node} ${script} run`,
1632
- "Restart=on-failure",
1633
- "RestartSec=2",
1634
- "RestartPreventExitStatus=78",
1635
- `Environment=PATH=${agentEnv().PATH ?? ""}`
1636
- ];
1637
- if (logPath) {
1638
- lines.push(`StandardOutput=append:${logPath}`, `StandardError=append:${logPath}`);
1639
- }
1640
- lines.push("", "[Install]", "WantedBy=default.target", "");
1641
- return lines.join("\n");
1642
- }
1643
- function unitPath() {
1644
- return path8.join(os7.homedir(), ".config", "systemd", "user", `${UNIT_NAME}.service`);
1645
- }
1646
- async function install3(opts, deps = {}) {
1647
- const runFn = deps.run ?? run;
1648
- const file = unitPath();
1649
- const unit = renderUnit({ node: opts.node, script: opts.script, logPath: opts.logPath });
1650
- fs6.mkdirSync(path8.dirname(file), { recursive: true });
1651
- fs6.writeFileSync(file, unit, "utf8");
1652
- const reload = await runFn("systemctl", ["--user", "daemon-reload"]);
1653
- if (reload.code !== 0) throw new Error(`systemctl daemon-reload failed (code ${reload.code}): ${reload.stderr.trim()}`);
1654
- const enable = await runFn("systemctl", ["--user", "enable", "--now", UNIT_NAME]);
1655
- if (enable.code !== 0) throw new Error(`systemctl enable --now failed (code ${enable.code}): ${enable.stderr.trim()}`);
1656
- console.log("Para o agente continuar ap\xF3s o logout: loginctl enable-linger $USER");
1657
- }
1658
- async function uninstall3(deps = {}) {
1659
- const runFn = deps.run ?? run;
1660
- await runFn("systemctl", ["--user", "disable", "--now", UNIT_NAME]);
1661
- try {
1662
- fs6.unlinkSync(unitPath());
1663
- } catch (err) {
1664
- if (err.code !== "ENOENT") throw err;
1665
- }
1666
- await runFn("systemctl", ["--user", "daemon-reload"]);
1667
- }
1668
- async function status2(deps = {}) {
1669
- const runFn = deps.run ?? run;
1670
- const result = await runFn("systemctl", ["--user", "is-active", UNIT_NAME]);
1671
- return result.code === 0;
1672
- }
1673
-
1674
- // src/service/index.ts
1675
- function serviceFileOptions() {
1676
- return {
1677
- node: process.execPath,
1678
- script: resolveScriptPath(process.argv[1]),
1679
- logPath: path9.join(agentHome(), "agent.log")
1680
- };
1681
- }
1682
- function assertSupported(platform) {
1683
- if (platform !== "darwin" && platform !== "linux") throw new Error("Sistema n\xE3o suportado");
1684
- }
1685
- async function install4() {
1686
- const platform = process.platform;
1687
- assertSupported(platform);
1688
- const opts = serviceFileOptions();
1689
- if (platform === "darwin") await install2(opts);
1690
- else await install3(opts);
1691
- }
1692
- async function uninstall4() {
1693
- const platform = process.platform;
1694
- assertSupported(platform);
1695
- if (platform === "darwin") await uninstall2();
1696
- else await uninstall3();
1697
- }
1698
- async function status3() {
1699
- const platform = process.platform;
1700
- assertSupported(platform);
1701
- if (platform === "darwin") return status();
1702
- return status2();
1703
- }
1704
-
1705
- // src/commands/service.ts
1706
1774
  async function serviceCommand(sub) {
1707
1775
  switch (sub) {
1708
1776
  case "install": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@termhub/agent",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
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,