@termhub/agent 0.1.3 → 0.1.5
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/dist/cli.js +285 -51
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -40,7 +40,8 @@ var machinePath = z.string().min(1).max(4096).refine((p) => (p === "~" || p.star
|
|
|
40
40
|
var pasteName = z.string().min(1).max(255).regex(/^[A-Za-z0-9._-]+$/);
|
|
41
41
|
var aiProvider = z.enum(["claude", "chatgpt", "gemini", "antigravity"]);
|
|
42
42
|
var rpcErrorSchema = z.object({
|
|
43
|
-
|
|
43
|
+
/** `failed`: the operation ran on the machine and `message` says why it failed, in words meant for the user. */
|
|
44
|
+
code: z.enum(["eperm", "notfound", "no_tmux", "timeout", "invalid", "internal", "failed"]),
|
|
44
45
|
message: z.string().max(2e3),
|
|
45
46
|
path: z.string().max(4096).optional()
|
|
46
47
|
});
|
|
@@ -60,7 +61,15 @@ var RPC = {
|
|
|
60
61
|
recursive: z.boolean().optional()
|
|
61
62
|
}), z.object({ stdout: z.string() })),
|
|
62
63
|
"ai.credential": def(z.object({ provider: aiProvider, config_dir: machinePath.nullable() }), z.object({ stdout: z.string() }), 1e4),
|
|
63
|
-
"file.paste": def(z.object({ name: pasteName, data_b64: z.string().min(1).max(28 * 1024 * 1024) }), z.object({ path: z.string() }), 6e4)
|
|
64
|
+
"file.paste": def(z.object({ name: pasteName, data_b64: z.string().min(1).max(28 * 1024 * 1024) }), z.object({ path: z.string() }), 6e4),
|
|
65
|
+
/** Monitor hooks (see @termhub/machine-ops hooks.ts): the agent writes the script, env and config entries under its own $HOME. */
|
|
66
|
+
/** `claude_dirs`: Claude config dirs besides ~/.claude (accounts with CLAUDE_CONFIG_DIR), hooked when they exist; since agent 0.1.5. */
|
|
67
|
+
"hooks.install": def(z.object({
|
|
68
|
+
hooks_url: z.string().min(1).max(2048).regex(/^https?:\/\/[^\s'"]+$/),
|
|
69
|
+
token: z.string().min(1).max(256).regex(/^[A-Za-z0-9_-]+$/),
|
|
70
|
+
claude_dirs: z.array(machinePath).max(16).optional()
|
|
71
|
+
}), z.object({ home: z.string(), claude: z.enum(["installed", "skipped"]), codex: z.enum(["installed", "skipped"]), claude_dirs: z.array(z.string()).optional() }), 15e3),
|
|
72
|
+
"hooks.uninstall": def(z.object({ claude_dirs: z.array(machinePath).max(16).optional() }), z.object({ removed: z.boolean() }), 15e3)
|
|
64
73
|
};
|
|
65
74
|
var RPC_METHODS = Object.keys(RPC);
|
|
66
75
|
var rpcMethod = z.enum(RPC_METHODS);
|
|
@@ -355,7 +364,7 @@ function deleteConfig() {
|
|
|
355
364
|
}
|
|
356
365
|
|
|
357
366
|
// src/run.ts
|
|
358
|
-
import
|
|
367
|
+
import os5 from "os";
|
|
359
368
|
|
|
360
369
|
// src/exec.ts
|
|
361
370
|
import { execFile } from "child_process";
|
|
@@ -372,16 +381,16 @@ var DETECT_TOOLS = ["tmux", "claude", "gh", "git", "node", "pnpm", "xcodebuild",
|
|
|
372
381
|
var WDA_RUNNER_APP = "$HOME/.termhub/WebDriverAgent/DerivedData/Build/Products/Debug-iphonesimulator/WebDriverAgentRunner-Runner.app";
|
|
373
382
|
var DETECT_SCRIPT = `echo OS:$(uname -s); for t in ${DETECT_TOOLS.join(" ")}; do command -v $t >/dev/null 2>&1 && echo CAP:$t; done; [ -d "${WDA_RUNNER_APP}" ] && echo CAP:wda; exit 0`;
|
|
374
383
|
function parseDetect(stdout) {
|
|
375
|
-
let
|
|
384
|
+
let os9 = null;
|
|
376
385
|
const caps = [];
|
|
377
386
|
for (const line of stdout.split("\n")) {
|
|
378
387
|
if (line.startsWith("OS:")) {
|
|
379
388
|
const raw = line.slice(3).trim().toLowerCase();
|
|
380
|
-
|
|
389
|
+
os9 = raw === "darwin" ? "macos" : raw || null;
|
|
381
390
|
} else if (line.startsWith("CAP:"))
|
|
382
391
|
caps.push(line.slice(4).trim());
|
|
383
392
|
}
|
|
384
|
-
return { os:
|
|
393
|
+
return { os: os9, capabilities: caps };
|
|
385
394
|
}
|
|
386
395
|
|
|
387
396
|
// ../../packages/machine-ops/dist/hardware-script.js
|
|
@@ -526,13 +535,121 @@ function ptyEnv(base, shell) {
|
|
|
526
535
|
};
|
|
527
536
|
}
|
|
528
537
|
|
|
538
|
+
// ../../packages/machine-ops/dist/hooks.js
|
|
539
|
+
var HOOK_SCRIPT_REL = ".termhub/bin/termhub-hook";
|
|
540
|
+
var HOOK_ENV_REL = ".termhub/hook.env";
|
|
541
|
+
var HOOK_MARK = "termhub-hook";
|
|
542
|
+
var CLAUDE_DEFAULT_DIR = "~/.claude";
|
|
543
|
+
function claudeConfigDirs(accountDirs) {
|
|
544
|
+
const out = [CLAUDE_DEFAULT_DIR];
|
|
545
|
+
for (const raw of accountDirs) {
|
|
546
|
+
let d = (raw ?? "").trim().replace(/\/+$/, "");
|
|
547
|
+
if (!d || d === "~" || /[\0\n\r]/.test(d))
|
|
548
|
+
continue;
|
|
549
|
+
if (!d.startsWith("~/") && !d.startsWith("/"))
|
|
550
|
+
d = `~/${d}`;
|
|
551
|
+
if (!out.includes(d))
|
|
552
|
+
out.push(d);
|
|
553
|
+
}
|
|
554
|
+
return out;
|
|
555
|
+
}
|
|
556
|
+
function expandHome(dir, home) {
|
|
557
|
+
return dir.startsWith("~/") ? `${home}/${dir.slice(2)}` : dir;
|
|
558
|
+
}
|
|
559
|
+
var CLAUDE_HOOK_EVENTS = ["SessionStart", "UserPromptSubmit", "Notification", "Stop", "SessionEnd"];
|
|
560
|
+
var HOOK_SCRIPT = `#!/bin/sh
|
|
561
|
+
# termhub monitor hook \u2014 installed by termhub; forwards Claude Code / Codex hook events to
|
|
562
|
+
# termhub tagged with the tmux session, so the app knows which tab is waiting for you.
|
|
563
|
+
# Safe to delete (also remove the entries in ~/.claude/settings.json and ~/.codex/config.toml).
|
|
564
|
+
TOOL="\${1:-claude}"
|
|
565
|
+
[ -f "$HOME/${HOOK_ENV_REL}" ] || exit 0
|
|
566
|
+
. "$HOME/${HOOK_ENV_REL}"
|
|
567
|
+
[ -n "$TERMHUB_HOOK_URL" ] && [ -n "$TERMHUB_HOOK_TOKEN" ] || exit 0
|
|
568
|
+
[ -n "$TMUX_PANE" ] || exit 0
|
|
569
|
+
PATH="$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:$PATH"
|
|
570
|
+
SESSION=$(tmux display-message -p -t "$TMUX_PANE" '#{session_name}' 2>/dev/null) || exit 0
|
|
571
|
+
[ -n "$SESSION" ] || exit 0
|
|
572
|
+
if [ "$TOOL" = codex ]; then EVENT="$2"; else EVENT=$(cat 2>/dev/null); fi
|
|
573
|
+
[ -n "$EVENT" ] || EVENT='{}'
|
|
574
|
+
{ printf '{"tool":"%s","session":"%s","event":' "$TOOL" "$SESSION"; printf '%s' "$EVENT"; printf '}'; } |
|
|
575
|
+
curl -s -m 5 -o /dev/null -X POST "$TERMHUB_HOOK_URL" \\
|
|
576
|
+
-H "authorization: Bearer $TERMHUB_HOOK_TOKEN" -H 'content-type: application/json' --data-binary @- >/dev/null 2>&1 &
|
|
577
|
+
exit 0
|
|
578
|
+
`;
|
|
579
|
+
function hookEnvFile(hooksUrl, token) {
|
|
580
|
+
return `TERMHUB_HOOK_URL=${shellQuote(hooksUrl)}
|
|
581
|
+
TERMHUB_HOOK_TOKEN=${shellQuote(token)}
|
|
582
|
+
`;
|
|
583
|
+
}
|
|
584
|
+
var isOurs = (e) => !!e && typeof e === "object" && Array.isArray(e.hooks) && e.hooks.some((h) => typeof h?.command === "string" && h.command.includes(HOOK_MARK));
|
|
585
|
+
function mergeClaudeSettings(current, scriptPath) {
|
|
586
|
+
let settings = {};
|
|
587
|
+
if (current.trim()) {
|
|
588
|
+
const parsed = JSON.parse(current);
|
|
589
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
590
|
+
throw new Error("~/.claude/settings.json n\xE3o \xE9 um objeto JSON");
|
|
591
|
+
settings = parsed;
|
|
592
|
+
}
|
|
593
|
+
const hooks = settings.hooks && typeof settings.hooks === "object" && !Array.isArray(settings.hooks) ? settings.hooks : {};
|
|
594
|
+
for (const event of CLAUDE_HOOK_EVENTS) {
|
|
595
|
+
const list3 = Array.isArray(hooks[event]) ? hooks[event] : [];
|
|
596
|
+
const others = list3.filter((e) => !isOurs(e));
|
|
597
|
+
others.push({ hooks: [{ type: "command", command: `${scriptPath} claude`, timeout: 10 }] });
|
|
598
|
+
hooks[event] = others;
|
|
599
|
+
}
|
|
600
|
+
settings.hooks = hooks;
|
|
601
|
+
return `${JSON.stringify(settings, null, 2)}
|
|
602
|
+
`;
|
|
603
|
+
}
|
|
604
|
+
function stripClaudeSettings(current) {
|
|
605
|
+
if (!current.trim())
|
|
606
|
+
return current;
|
|
607
|
+
const parsed = JSON.parse(current);
|
|
608
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
609
|
+
return current;
|
|
610
|
+
const settings = parsed;
|
|
611
|
+
const hooks = settings.hooks;
|
|
612
|
+
if (hooks && typeof hooks === "object" && !Array.isArray(hooks)) {
|
|
613
|
+
const h = hooks;
|
|
614
|
+
for (const key of Object.keys(h)) {
|
|
615
|
+
if (!Array.isArray(h[key]))
|
|
616
|
+
continue;
|
|
617
|
+
const kept = h[key].filter((e) => !isOurs(e));
|
|
618
|
+
if (kept.length)
|
|
619
|
+
h[key] = kept;
|
|
620
|
+
else
|
|
621
|
+
delete h[key];
|
|
622
|
+
}
|
|
623
|
+
if (Object.keys(h).length === 0)
|
|
624
|
+
delete settings.hooks;
|
|
625
|
+
}
|
|
626
|
+
return `${JSON.stringify(settings, null, 2)}
|
|
627
|
+
`;
|
|
628
|
+
}
|
|
629
|
+
function mergeCodexConfig(current, scriptPath) {
|
|
630
|
+
const line = `notify = [${JSON.stringify(scriptPath)}, "codex"]`;
|
|
631
|
+
const lines = current.split("\n");
|
|
632
|
+
const idx = lines.findIndex((l) => /^\s*notify\s*=/.test(l));
|
|
633
|
+
if (idx !== -1)
|
|
634
|
+
lines[idx] = line;
|
|
635
|
+
else
|
|
636
|
+
lines.unshift(line);
|
|
637
|
+
const out = lines.join("\n");
|
|
638
|
+
return out.endsWith("\n") ? out : `${out}
|
|
639
|
+
`;
|
|
640
|
+
}
|
|
641
|
+
function stripCodexConfig(current) {
|
|
642
|
+
const lines = current.split("\n").filter((l) => !(/^\s*notify\s*=/.test(l) && l.includes(HOOK_MARK)));
|
|
643
|
+
return lines.join("\n");
|
|
644
|
+
}
|
|
645
|
+
|
|
529
646
|
// src/exec.ts
|
|
530
647
|
var DEFAULT_TIMEOUT_MS = 8e3;
|
|
531
648
|
var RpcFailure = class extends Error {
|
|
532
|
-
constructor(code, message,
|
|
649
|
+
constructor(code, message, path10) {
|
|
533
650
|
super(message);
|
|
534
651
|
this.code = code;
|
|
535
|
-
this.path =
|
|
652
|
+
this.path = path10;
|
|
536
653
|
}
|
|
537
654
|
code;
|
|
538
655
|
path;
|
|
@@ -635,7 +752,7 @@ function createDispatcher(deps) {
|
|
|
635
752
|
// src/pty.ts
|
|
636
753
|
import fs2 from "fs";
|
|
637
754
|
import path2 from "path";
|
|
638
|
-
function
|
|
755
|
+
function expandHome2(rawCwd, home) {
|
|
639
756
|
if (rawCwd === "~") return home;
|
|
640
757
|
if (rawCwd.startsWith("~/")) return path2.join(home, rawCwd.slice(2));
|
|
641
758
|
return rawCwd;
|
|
@@ -649,7 +766,7 @@ function existsDir(p) {
|
|
|
649
766
|
}
|
|
650
767
|
function resolveCwd(rawCwd) {
|
|
651
768
|
const home = process.env.HOME || "/";
|
|
652
|
-
const expanded =
|
|
769
|
+
const expanded = expandHome2(rawCwd, home);
|
|
653
770
|
return existsDir(expanded) ? expanded : home;
|
|
654
771
|
}
|
|
655
772
|
function isEnoent(err) {
|
|
@@ -836,6 +953,121 @@ async function mkdir(params) {
|
|
|
836
953
|
return { stdout: r.stdout };
|
|
837
954
|
}
|
|
838
955
|
|
|
956
|
+
// src/rpc/hooks.ts
|
|
957
|
+
import { chmod, mkdir as mkdir2, readFile, rename, rm, stat, writeFile } from "fs/promises";
|
|
958
|
+
import os3 from "os";
|
|
959
|
+
import path4 from "path";
|
|
960
|
+
var CODEX_DIR_REL = ".codex";
|
|
961
|
+
var CODEX_CONFIG_REL = ".codex/config.toml";
|
|
962
|
+
var isEnoent2 = (err) => err?.code === "ENOENT";
|
|
963
|
+
async function readOrEmpty(file) {
|
|
964
|
+
try {
|
|
965
|
+
return await readFile(file, "utf8");
|
|
966
|
+
} catch (err) {
|
|
967
|
+
if (isEnoent2(err)) return "";
|
|
968
|
+
throw err;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
async function isDir(dir) {
|
|
972
|
+
try {
|
|
973
|
+
return (await stat(dir)).isDirectory();
|
|
974
|
+
} catch {
|
|
975
|
+
return false;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
async function writeAtomic(file, body, mode) {
|
|
979
|
+
const tmp = `${file}.termhub-new`;
|
|
980
|
+
await writeFile(tmp, body, { encoding: "utf8", mode });
|
|
981
|
+
await chmod(tmp, mode);
|
|
982
|
+
await rename(tmp, file);
|
|
983
|
+
}
|
|
984
|
+
var relPath = (shown) => shown.startsWith("~/") ? shown.slice(2) : shown;
|
|
985
|
+
function fsFailure(err, shown) {
|
|
986
|
+
const code = err?.code;
|
|
987
|
+
if (code === "EACCES" || code === "EPERM") return new RpcFailure("eperm", `sem permiss\xE3o em ${shown}`, relPath(shown));
|
|
988
|
+
return new RpcFailure("failed", `n\xE3o foi poss\xEDvel escrever ${shown}: ${err instanceof Error ? err.message : String(err)}`, relPath(shown));
|
|
989
|
+
}
|
|
990
|
+
async function claudeTargets(dirs, home) {
|
|
991
|
+
const out = [];
|
|
992
|
+
for (const d of claudeConfigDirs(dirs ?? [])) {
|
|
993
|
+
const dir = expandHome(d, home);
|
|
994
|
+
if (d !== CLAUDE_DEFAULT_DIR && !await isDir(dir)) continue;
|
|
995
|
+
out.push({ dir, file: path4.join(dir, "settings.json"), shown: `${d}/settings.json` });
|
|
996
|
+
}
|
|
997
|
+
return out;
|
|
998
|
+
}
|
|
999
|
+
async function install(params, home = os3.homedir()) {
|
|
1000
|
+
const scriptPath = path4.join(home, HOOK_SCRIPT_REL);
|
|
1001
|
+
const codexFile = path4.join(home, CODEX_CONFIG_REL);
|
|
1002
|
+
const targets = await claudeTargets(params.claude_dirs, home);
|
|
1003
|
+
const merged = [];
|
|
1004
|
+
for (const target of targets) {
|
|
1005
|
+
try {
|
|
1006
|
+
merged.push({ target, body: mergeClaudeSettings(await readOrEmpty(target.file), scriptPath) });
|
|
1007
|
+
} catch (err) {
|
|
1008
|
+
const message = err instanceof SyntaxError || err instanceof Error && err.message.includes("n\xE3o \xE9 um objeto JSON") ? `${target.shown} n\xE3o \xE9 JSON v\xE1lido` : err instanceof Error ? err.message : String(err);
|
|
1009
|
+
throw new RpcFailure("failed", message, relPath(target.shown));
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
const hasCodex = await isDir(path4.join(home, CODEX_DIR_REL));
|
|
1013
|
+
const mergedCodex = hasCodex ? mergeCodexConfig(await readOrEmpty(codexFile), scriptPath) : null;
|
|
1014
|
+
let current = `~/${HOOK_SCRIPT_REL}`;
|
|
1015
|
+
try {
|
|
1016
|
+
await mkdir2(path4.dirname(scriptPath), { recursive: true });
|
|
1017
|
+
current = `~/${HOOK_ENV_REL}`;
|
|
1018
|
+
await writeAtomic(path4.join(home, HOOK_ENV_REL), hookEnvFile(params.hooks_url, params.token), 384);
|
|
1019
|
+
current = `~/${HOOK_SCRIPT_REL}`;
|
|
1020
|
+
await writeAtomic(scriptPath, HOOK_SCRIPT, 493);
|
|
1021
|
+
for (const { target, body } of merged) {
|
|
1022
|
+
current = target.shown;
|
|
1023
|
+
await mkdir2(target.dir, { recursive: true });
|
|
1024
|
+
await writeAtomic(target.file, body, 420);
|
|
1025
|
+
}
|
|
1026
|
+
if (mergedCodex !== null) {
|
|
1027
|
+
current = `~/${CODEX_CONFIG_REL}`;
|
|
1028
|
+
await writeAtomic(codexFile, mergedCodex, 420);
|
|
1029
|
+
}
|
|
1030
|
+
} catch (err) {
|
|
1031
|
+
throw fsFailure(err, current);
|
|
1032
|
+
}
|
|
1033
|
+
return {
|
|
1034
|
+
home,
|
|
1035
|
+
claude: "installed",
|
|
1036
|
+
codex: mergedCodex !== null ? "installed" : "skipped",
|
|
1037
|
+
claude_dirs: merged.map(({ target }) => target.shown.replace(/\/settings\.json$/, ""))
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
async function uninstall(params, home = os3.homedir()) {
|
|
1041
|
+
const codexFile = path4.join(home, CODEX_CONFIG_REL);
|
|
1042
|
+
const stripped = [];
|
|
1043
|
+
for (const target of await claudeTargets(params.claude_dirs, home)) {
|
|
1044
|
+
const current2 = await readOrEmpty(target.file);
|
|
1045
|
+
try {
|
|
1046
|
+
const body = current2.trim() ? stripClaudeSettings(current2) : current2;
|
|
1047
|
+
if (body !== current2) stripped.push({ target, body });
|
|
1048
|
+
} catch {
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
const codexConfig = await isDir(path4.join(home, CODEX_DIR_REL)) ? await readOrEmpty(codexFile) : "";
|
|
1052
|
+
let current = `~/${HOOK_SCRIPT_REL}`;
|
|
1053
|
+
try {
|
|
1054
|
+
await rm(path4.join(home, HOOK_SCRIPT_REL), { force: true });
|
|
1055
|
+
current = `~/${HOOK_ENV_REL}`;
|
|
1056
|
+
await rm(path4.join(home, HOOK_ENV_REL), { force: true });
|
|
1057
|
+
for (const { target, body } of stripped) {
|
|
1058
|
+
current = target.shown;
|
|
1059
|
+
await writeAtomic(target.file, body, 420);
|
|
1060
|
+
}
|
|
1061
|
+
if (codexConfig.includes(HOOK_MARK)) {
|
|
1062
|
+
current = `~/${CODEX_CONFIG_REL}`;
|
|
1063
|
+
await writeAtomic(codexFile, stripCodexConfig(codexConfig), 420);
|
|
1064
|
+
}
|
|
1065
|
+
} catch (err) {
|
|
1066
|
+
throw fsFailure(err, current);
|
|
1067
|
+
}
|
|
1068
|
+
return { removed: true };
|
|
1069
|
+
}
|
|
1070
|
+
|
|
839
1071
|
// src/rpc/hw.ts
|
|
840
1072
|
async function probe(_params) {
|
|
841
1073
|
const r = await sh(HARDWARE_SCRIPT, { timeoutMs: 14e3 });
|
|
@@ -852,9 +1084,9 @@ async function pasteFile(params) {
|
|
|
852
1084
|
if (r.timedOut) throw new RpcFailure("timeout", "file.paste timed out");
|
|
853
1085
|
if (r.code !== 0) throw new RpcFailure("internal", `file.paste exited with code ${r.code}`);
|
|
854
1086
|
const lines = r.stdout.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
855
|
-
const
|
|
856
|
-
if (!
|
|
857
|
-
return { path:
|
|
1087
|
+
const path10 = lines[lines.length - 1] ?? "";
|
|
1088
|
+
if (!path10.startsWith("/")) throw new RpcFailure("internal", "unexpected paste output");
|
|
1089
|
+
return { path: path10 };
|
|
858
1090
|
}
|
|
859
1091
|
|
|
860
1092
|
// src/rpc/tmux.ts
|
|
@@ -891,8 +1123,8 @@ async function detect(_params) {
|
|
|
891
1123
|
const r = await sh(DETECT_SCRIPT);
|
|
892
1124
|
if (r.timedOut) throw new RpcFailure("timeout", "tools.detect timed out");
|
|
893
1125
|
if (r.code !== 0) throw new RpcFailure("internal", `tools.detect exited with code ${r.code}`);
|
|
894
|
-
const { os:
|
|
895
|
-
return { os:
|
|
1126
|
+
const { os: os9, capabilities } = parseDetect(r.stdout);
|
|
1127
|
+
return { os: os9, tools: capabilities };
|
|
896
1128
|
}
|
|
897
1129
|
|
|
898
1130
|
// src/rpc/index.ts
|
|
@@ -905,13 +1137,15 @@ var handlers = {
|
|
|
905
1137
|
"fs.list": list,
|
|
906
1138
|
"fs.mkdir": mkdir,
|
|
907
1139
|
"ai.credential": credential,
|
|
908
|
-
"file.paste": pasteFile
|
|
1140
|
+
"file.paste": pasteFile,
|
|
1141
|
+
"hooks.install": install,
|
|
1142
|
+
"hooks.uninstall": uninstall
|
|
909
1143
|
};
|
|
910
1144
|
|
|
911
1145
|
// src/service/launchd.ts
|
|
912
1146
|
import fs4 from "fs";
|
|
913
|
-
import
|
|
914
|
-
import
|
|
1147
|
+
import os4 from "os";
|
|
1148
|
+
import path5 from "path";
|
|
915
1149
|
var LABEL = "dev.termhub.agent";
|
|
916
1150
|
function renderPlist({ label, node, script, logPath }) {
|
|
917
1151
|
const escape = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
@@ -944,22 +1178,22 @@ function renderPlist({ label, node, script, logPath }) {
|
|
|
944
1178
|
<string>${escape(agentEnv().PATH ?? "")}</string>
|
|
945
1179
|
</dict>
|
|
946
1180
|
<key>WorkingDirectory</key>
|
|
947
|
-
<string>${escape(
|
|
1181
|
+
<string>${escape(os4.homedir())}</string>
|
|
948
1182
|
</dict>
|
|
949
1183
|
</plist>
|
|
950
1184
|
`;
|
|
951
1185
|
}
|
|
952
1186
|
function plistPath() {
|
|
953
|
-
return
|
|
1187
|
+
return path5.join(os4.homedir(), "Library", "LaunchAgents", `${LABEL}.plist`);
|
|
954
1188
|
}
|
|
955
1189
|
function gui() {
|
|
956
1190
|
return `gui/${process.getuid ? process.getuid() : 0}`;
|
|
957
1191
|
}
|
|
958
|
-
async function
|
|
1192
|
+
async function install2(opts, deps = {}) {
|
|
959
1193
|
const runFn = deps.run ?? run;
|
|
960
1194
|
const file = plistPath();
|
|
961
1195
|
const plist = renderPlist({ label: LABEL, node: opts.node, script: opts.script, logPath: opts.logPath });
|
|
962
|
-
fs4.mkdirSync(
|
|
1196
|
+
fs4.mkdirSync(path5.dirname(file), { recursive: true });
|
|
963
1197
|
fs4.writeFileSync(file, plist, "utf8");
|
|
964
1198
|
await runFn("launchctl", ["bootout", gui(), file]);
|
|
965
1199
|
const result = await runFn("launchctl", ["bootstrap", gui(), file]);
|
|
@@ -967,7 +1201,7 @@ async function install(opts, deps = {}) {
|
|
|
967
1201
|
throw new Error(`launchctl bootstrap failed (code ${result.code}): ${result.stderr.trim() || result.stdout.trim()}`);
|
|
968
1202
|
}
|
|
969
1203
|
}
|
|
970
|
-
async function
|
|
1204
|
+
async function uninstall2(deps = {}) {
|
|
971
1205
|
const runFn = deps.run ?? run;
|
|
972
1206
|
const file = plistPath();
|
|
973
1207
|
await runFn("launchctl", ["bootout", gui(), file]);
|
|
@@ -993,7 +1227,7 @@ async function status(deps = {}) {
|
|
|
993
1227
|
}
|
|
994
1228
|
|
|
995
1229
|
// src/version.ts
|
|
996
|
-
var AGENT_VERSION = "0.1.
|
|
1230
|
+
var AGENT_VERSION = "0.1.5";
|
|
997
1231
|
|
|
998
1232
|
// src/run.ts
|
|
999
1233
|
function detectOs(platform = process.platform) {
|
|
@@ -1013,7 +1247,7 @@ async function buildHello(osName) {
|
|
|
1013
1247
|
agent_version: AGENT_VERSION,
|
|
1014
1248
|
os: osName,
|
|
1015
1249
|
arch: process.arch,
|
|
1016
|
-
hostname:
|
|
1250
|
+
hostname: os5.hostname(),
|
|
1017
1251
|
tmux: tools.includes("tmux"),
|
|
1018
1252
|
tools
|
|
1019
1253
|
};
|
|
@@ -1033,7 +1267,7 @@ async function checkServerConnection(config, timeoutMs = 5e3) {
|
|
|
1033
1267
|
{
|
|
1034
1268
|
url: config.url,
|
|
1035
1269
|
token: config.token,
|
|
1036
|
-
hello: { agent_version: AGENT_VERSION, os: osName, arch: process.arch, hostname:
|
|
1270
|
+
hello: { agent_version: AGENT_VERSION, os: osName, arch: process.arch, hostname: os5.hostname(), tmux: false, tools: [], probe: true },
|
|
1037
1271
|
onServerMessage: () => {
|
|
1038
1272
|
},
|
|
1039
1273
|
onStream: () => {
|
|
@@ -1184,16 +1418,16 @@ function disconnectCommand() {
|
|
|
1184
1418
|
|
|
1185
1419
|
// src/doctor.ts
|
|
1186
1420
|
import fs5 from "fs";
|
|
1187
|
-
import
|
|
1188
|
-
import
|
|
1421
|
+
import os6 from "os";
|
|
1422
|
+
import path6 from "path";
|
|
1189
1423
|
var SERVER_CHECK_TIMEOUT_MS = 5e3;
|
|
1190
1424
|
function defaultDoctorPaths() {
|
|
1191
|
-
const home =
|
|
1192
|
-
const paths = [home,
|
|
1425
|
+
const home = os6.homedir();
|
|
1426
|
+
const paths = [home, path6.join(home, "Documents"), path6.join(home, "Desktop")];
|
|
1193
1427
|
if (process.platform === "darwin") {
|
|
1194
1428
|
try {
|
|
1195
1429
|
for (const entry of fs5.readdirSync("/Volumes", { withFileTypes: true })) {
|
|
1196
|
-
if (entry.isDirectory()) paths.push(
|
|
1430
|
+
if (entry.isDirectory()) paths.push(path6.join("/Volumes", entry.name));
|
|
1197
1431
|
}
|
|
1198
1432
|
} catch {
|
|
1199
1433
|
}
|
|
@@ -1285,21 +1519,21 @@ async function runCommand(log2) {
|
|
|
1285
1519
|
}
|
|
1286
1520
|
|
|
1287
1521
|
// src/commands/service.ts
|
|
1288
|
-
import
|
|
1522
|
+
import os8 from "os";
|
|
1289
1523
|
|
|
1290
1524
|
// src/service/index.ts
|
|
1291
|
-
import
|
|
1525
|
+
import path9 from "path";
|
|
1292
1526
|
|
|
1293
1527
|
// src/paths.ts
|
|
1294
1528
|
import { realpathSync } from "fs";
|
|
1295
|
-
import
|
|
1529
|
+
import path7 from "path";
|
|
1296
1530
|
import { pathToFileURL } from "url";
|
|
1297
1531
|
function resolveScriptPath(argv1) {
|
|
1298
1532
|
if (!argv1) return "";
|
|
1299
1533
|
try {
|
|
1300
1534
|
return realpathSync(argv1);
|
|
1301
1535
|
} catch {
|
|
1302
|
-
return
|
|
1536
|
+
return path7.resolve(argv1);
|
|
1303
1537
|
}
|
|
1304
1538
|
}
|
|
1305
1539
|
function isMainModule(importMetaUrl, argv1) {
|
|
@@ -1309,8 +1543,8 @@ function isMainModule(importMetaUrl, argv1) {
|
|
|
1309
1543
|
|
|
1310
1544
|
// src/service/systemd.ts
|
|
1311
1545
|
import fs6 from "fs";
|
|
1312
|
-
import
|
|
1313
|
-
import
|
|
1546
|
+
import os7 from "os";
|
|
1547
|
+
import path8 from "path";
|
|
1314
1548
|
var UNIT_NAME = "termhub-agent";
|
|
1315
1549
|
function renderUnit({ node, script, logPath }) {
|
|
1316
1550
|
const lines = [
|
|
@@ -1332,13 +1566,13 @@ function renderUnit({ node, script, logPath }) {
|
|
|
1332
1566
|
return lines.join("\n");
|
|
1333
1567
|
}
|
|
1334
1568
|
function unitPath() {
|
|
1335
|
-
return
|
|
1569
|
+
return path8.join(os7.homedir(), ".config", "systemd", "user", `${UNIT_NAME}.service`);
|
|
1336
1570
|
}
|
|
1337
|
-
async function
|
|
1571
|
+
async function install3(opts, deps = {}) {
|
|
1338
1572
|
const runFn = deps.run ?? run;
|
|
1339
1573
|
const file = unitPath();
|
|
1340
1574
|
const unit = renderUnit({ node: opts.node, script: opts.script, logPath: opts.logPath });
|
|
1341
|
-
fs6.mkdirSync(
|
|
1575
|
+
fs6.mkdirSync(path8.dirname(file), { recursive: true });
|
|
1342
1576
|
fs6.writeFileSync(file, unit, "utf8");
|
|
1343
1577
|
const reload = await runFn("systemctl", ["--user", "daemon-reload"]);
|
|
1344
1578
|
if (reload.code !== 0) throw new Error(`systemctl daemon-reload failed (code ${reload.code}): ${reload.stderr.trim()}`);
|
|
@@ -1346,7 +1580,7 @@ async function install2(opts, deps = {}) {
|
|
|
1346
1580
|
if (enable.code !== 0) throw new Error(`systemctl enable --now failed (code ${enable.code}): ${enable.stderr.trim()}`);
|
|
1347
1581
|
console.log("Para o agente continuar ap\xF3s o logout: loginctl enable-linger $USER");
|
|
1348
1582
|
}
|
|
1349
|
-
async function
|
|
1583
|
+
async function uninstall3(deps = {}) {
|
|
1350
1584
|
const runFn = deps.run ?? run;
|
|
1351
1585
|
await runFn("systemctl", ["--user", "disable", "--now", UNIT_NAME]);
|
|
1352
1586
|
try {
|
|
@@ -1367,24 +1601,24 @@ function serviceFileOptions() {
|
|
|
1367
1601
|
return {
|
|
1368
1602
|
node: process.execPath,
|
|
1369
1603
|
script: resolveScriptPath(process.argv[1]),
|
|
1370
|
-
logPath:
|
|
1604
|
+
logPath: path9.join(agentHome(), "agent.log")
|
|
1371
1605
|
};
|
|
1372
1606
|
}
|
|
1373
1607
|
function assertSupported(platform) {
|
|
1374
1608
|
if (platform !== "darwin" && platform !== "linux") throw new Error("Sistema n\xE3o suportado");
|
|
1375
1609
|
}
|
|
1376
|
-
async function
|
|
1610
|
+
async function install4() {
|
|
1377
1611
|
const platform = process.platform;
|
|
1378
1612
|
assertSupported(platform);
|
|
1379
1613
|
const opts = serviceFileOptions();
|
|
1380
|
-
if (platform === "darwin") await
|
|
1381
|
-
else await
|
|
1614
|
+
if (platform === "darwin") await install2(opts);
|
|
1615
|
+
else await install3(opts);
|
|
1382
1616
|
}
|
|
1383
|
-
async function
|
|
1617
|
+
async function uninstall4() {
|
|
1384
1618
|
const platform = process.platform;
|
|
1385
1619
|
assertSupported(platform);
|
|
1386
|
-
if (platform === "darwin") await
|
|
1387
|
-
else await
|
|
1620
|
+
if (platform === "darwin") await uninstall2();
|
|
1621
|
+
else await uninstall3();
|
|
1388
1622
|
}
|
|
1389
1623
|
async function status3() {
|
|
1390
1624
|
const platform = process.platform;
|
|
@@ -1397,16 +1631,16 @@ async function status3() {
|
|
|
1397
1631
|
async function serviceCommand(sub) {
|
|
1398
1632
|
switch (sub) {
|
|
1399
1633
|
case "install": {
|
|
1400
|
-
const homeCheck = checkPathAccess(
|
|
1634
|
+
const homeCheck = checkPathAccess(os8.homedir());
|
|
1401
1635
|
if (!homeCheck.ok && homeCheck.error === "eperm" && process.platform === "darwin") {
|
|
1402
1636
|
console.error(fullDiskAccessNote());
|
|
1403
1637
|
}
|
|
1404
|
-
await
|
|
1638
|
+
await install4();
|
|
1405
1639
|
console.log("Servi\xE7o instalado.");
|
|
1406
1640
|
return;
|
|
1407
1641
|
}
|
|
1408
1642
|
case "uninstall":
|
|
1409
|
-
await
|
|
1643
|
+
await uninstall4();
|
|
1410
1644
|
console.log("Servi\xE7o removido.");
|
|
1411
1645
|
return;
|
|
1412
1646
|
case "status": {
|
package/package.json
CHANGED