@tarfa/cli 0.1.0 → 0.1.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.
- package/dist/cli.js +86 -55
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -542,7 +542,7 @@ function findMise(home, pathVar, exists2 = existsSync) {
|
|
|
542
542
|
return candidates.find(exists2) ?? null;
|
|
543
543
|
}
|
|
544
544
|
function miseCli(home) {
|
|
545
|
-
const
|
|
545
|
+
const run4 = async (mise, args, timeout) => {
|
|
546
546
|
try {
|
|
547
547
|
return (await exec(mise, ["--cd", home, ...args], { timeout })).stdout.trim();
|
|
548
548
|
} catch (err) {
|
|
@@ -551,10 +551,10 @@ function miseCli(home) {
|
|
|
551
551
|
}
|
|
552
552
|
};
|
|
553
553
|
return {
|
|
554
|
-
install: async (mise, version) => void await
|
|
554
|
+
install: async (mise, version) => void await run4(mise, ["use", "-g", `npm:@tarfa/cli@${version}`], 6e5),
|
|
555
555
|
async locate(mise, version) {
|
|
556
|
-
const install = await
|
|
557
|
-
const nodeHome = await
|
|
556
|
+
const install = await run4(mise, ["where", `npm:@tarfa/cli@${version}`], 6e4);
|
|
557
|
+
const nodeHome = await run4(mise, ["where", "node"], 6e4);
|
|
558
558
|
const cli = join2(install, "bin", "tarfa");
|
|
559
559
|
const node = join2(nodeHome, "bin", "node");
|
|
560
560
|
if (!install || !existsSync(cli)) throw new Error(`mise knows no install of @tarfa/cli ${version} with a bin/tarfa`);
|
|
@@ -702,29 +702,54 @@ async function backupFile(path) {
|
|
|
702
702
|
await chmod4(copy, (await stat2(path)).mode & 511);
|
|
703
703
|
return copy;
|
|
704
704
|
}
|
|
705
|
+
var LABEL_GONE_MS = 1e4;
|
|
706
|
+
var BOOTSTRAP_TRIES = 5;
|
|
707
|
+
var POLL_MS = 250;
|
|
708
|
+
var RETRY_MS = 1e3;
|
|
705
709
|
function serviceInstall(options) {
|
|
706
|
-
const
|
|
710
|
+
const run4 = options.run ?? systemRun;
|
|
711
|
+
const sleep = options.sleep ?? ((ms) => new Promise((resolve2) => setTimeout(resolve2, ms)));
|
|
707
712
|
const templates = options.templates ?? join4(packageRoot(), "templates");
|
|
708
713
|
if ((options.platform ?? process.platform) === "darwin") {
|
|
709
714
|
const plist = join4(options.home, "Library", "LaunchAgents", "app.tarfa.agent.plist");
|
|
710
715
|
const domain = `gui/${options.uid}`;
|
|
711
716
|
const legacyPlist = join4(options.home, "Library", "LaunchAgents", `${LEGACY_LABEL}.plist`);
|
|
717
|
+
const loaded = (label) => run4("launchctl", ["print", `${domain}/${label}`]).then(() => true, () => false);
|
|
718
|
+
const untilGone = async (label) => {
|
|
719
|
+
for (let waited = 0; waited < LABEL_GONE_MS && await loaded(label); waited += POLL_MS) await sleep(POLL_MS);
|
|
720
|
+
};
|
|
712
721
|
return {
|
|
713
722
|
kind: "launchd",
|
|
714
723
|
template: () => readFile3(join4(templates, "app.tarfa.agent.plist"), "utf8"),
|
|
715
724
|
installed: () => readOrNull2(plist),
|
|
716
725
|
backup: () => backupFile(plist),
|
|
717
|
-
enabled: () =>
|
|
726
|
+
enabled: () => loaded("app.tarfa.agent"),
|
|
718
727
|
async install(text) {
|
|
719
728
|
await atomicWrite(plist, text, 420);
|
|
720
|
-
await
|
|
721
|
-
await
|
|
722
|
-
|
|
729
|
+
await run4("launchctl", ["bootout", `${domain}/app.tarfa.agent`]).catch(() => void 0);
|
|
730
|
+
await untilGone("app.tarfa.agent");
|
|
731
|
+
let failure;
|
|
732
|
+
for (let attempt2 = 1; attempt2 <= BOOTSTRAP_TRIES; attempt2++) {
|
|
733
|
+
try {
|
|
734
|
+
await run4("launchctl", ["bootstrap", domain, plist]);
|
|
735
|
+
return [`wrote ${plist}`, `launchctl bootstrap ${domain}${attempt2 > 1 ? ` (try ${attempt2})` : ""}`];
|
|
736
|
+
} catch (err) {
|
|
737
|
+
failure = err;
|
|
738
|
+
if (attempt2 < BOOTSTRAP_TRIES) await sleep(RETRY_MS);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
const reason = failure instanceof Error ? failure.message : String(failure);
|
|
742
|
+
if (await loaded("app.tarfa.agent")) {
|
|
743
|
+
throw new Error(`the agent already loaded keeps running its old definition: ${reason} (${BOOTSTRAP_TRIES} tries). The new ${plist} is in place: run \`launchctl bootout ${domain}/app.tarfa.agent\`, wait until \`launchctl print ${domain}/app.tarfa.agent\` fails, then \`launchctl bootstrap ${domain} ${plist}\``);
|
|
744
|
+
}
|
|
745
|
+
throw new Error(`no agent is loaded: ${reason} (${BOOTSTRAP_TRIES} tries). The new ${plist} is in place: load it with \`launchctl bootstrap ${domain} ${plist}\`, then run tarfa setup again`);
|
|
723
746
|
},
|
|
724
747
|
legacyInstalled: async () => await readOrNull2(legacyPlist) !== null,
|
|
725
748
|
legacyText: () => readOrNull2(legacyPlist),
|
|
749
|
+
// The old agent holds the hooks and phone ports until launchd has stopped it.
|
|
726
750
|
async stopLegacy() {
|
|
727
|
-
await
|
|
751
|
+
await run4("launchctl", ["bootout", `${domain}/${LEGACY_LABEL}`]).catch(() => void 0);
|
|
752
|
+
await untilGone(LEGACY_LABEL);
|
|
728
753
|
return [`launchctl bootout ${domain}/${LEGACY_LABEL}`];
|
|
729
754
|
},
|
|
730
755
|
async removeLegacy() {
|
|
@@ -743,7 +768,7 @@ function serviceInstall(options) {
|
|
|
743
768
|
installed: () => readOrNull2(unit),
|
|
744
769
|
backup: () => backupFile(unit),
|
|
745
770
|
async enabled() {
|
|
746
|
-
const enabled = await
|
|
771
|
+
const enabled = await run4("systemctl", ["--user", "is-enabled", "tarfa-agent"]).then(
|
|
747
772
|
() => true,
|
|
748
773
|
(err) => {
|
|
749
774
|
if (NO_USER_MANAGER.test(String(err instanceof Error ? err.message : err))) {
|
|
@@ -752,7 +777,7 @@ function serviceInstall(options) {
|
|
|
752
777
|
return false;
|
|
753
778
|
}
|
|
754
779
|
);
|
|
755
|
-
const linger = await
|
|
780
|
+
const linger = await run4("loginctl", ["show-user", options.user, "-p", "Linger", "--value"]).then((out) => out.trim() === "yes", () => false);
|
|
756
781
|
return enabled && linger;
|
|
757
782
|
},
|
|
758
783
|
async install(text) {
|
|
@@ -765,7 +790,7 @@ function serviceInstall(options) {
|
|
|
765
790
|
// Without linger the user's units stop at logout and do not start at boot.
|
|
766
791
|
["loginctl", ["enable-linger", options.user]]
|
|
767
792
|
];
|
|
768
|
-
for (const [command, args] of ran) await
|
|
793
|
+
for (const [command, args] of ran) await run4(command, args);
|
|
769
794
|
return [`wrote ${unit}`, ...copied ? [`copied ${legacyDropIns} to ${dropIns}`] : [], ...ran.map(([command, args]) => `${command} ${args.join(" ")}`)];
|
|
770
795
|
},
|
|
771
796
|
legacyInstalled: async () => await readOrNull2(legacyUnit) !== null,
|
|
@@ -776,14 +801,14 @@ function serviceInstall(options) {
|
|
|
776
801
|
["systemctl", ["--user", "stop", LEGACY_UNIT]],
|
|
777
802
|
["systemctl", ["--user", "disable", LEGACY_UNIT]]
|
|
778
803
|
];
|
|
779
|
-
for (const [command, args] of ran) await
|
|
804
|
+
for (const [command, args] of ran) await run4(command, args);
|
|
780
805
|
return ran.map(([command, args]) => `${command} ${args.join(" ")}`);
|
|
781
806
|
},
|
|
782
807
|
async removeLegacy() {
|
|
783
808
|
await rm3(legacyUnit, { force: true });
|
|
784
809
|
if (await exists(legacyDropIns) && !await exists(dropIns)) await cp(legacyDropIns, dropIns, { recursive: true });
|
|
785
810
|
await rm3(legacyDropIns, { recursive: true, force: true });
|
|
786
|
-
await
|
|
811
|
+
await run4("systemctl", ["--user", "daemon-reload"]);
|
|
787
812
|
return [`removed ${legacyUnit} and its drop-ins`, "systemctl --user daemon-reload"];
|
|
788
813
|
}
|
|
789
814
|
};
|
|
@@ -807,12 +832,12 @@ async function readFrom(path, offset) {
|
|
|
807
832
|
}
|
|
808
833
|
}
|
|
809
834
|
function agentLog(options) {
|
|
810
|
-
const
|
|
835
|
+
const run4 = options.run ?? systemRun;
|
|
811
836
|
const now = options.now ?? Date.now;
|
|
812
837
|
if ((options.platform ?? process.platform) !== "darwin") {
|
|
813
838
|
return {
|
|
814
839
|
mark: async () => Math.floor(now() / 1e3),
|
|
815
|
-
since: (mark) =>
|
|
840
|
+
since: (mark) => run4("journalctl", ["--user", "-u", "tarfa-agent", `--since=@${String(mark)}`, "--no-pager", "-o", "cat"])
|
|
816
841
|
};
|
|
817
842
|
}
|
|
818
843
|
const plist = join5(options.home, "Library", "LaunchAgents", "app.tarfa.agent.plist");
|
|
@@ -844,18 +869,18 @@ import { promisify as promisify3 } from "node:util";
|
|
|
844
869
|
var run = promisify3(execFile3);
|
|
845
870
|
var systemExec = (args) => run("tailscale", args, { env: childEnv(), timeout: 3e4 });
|
|
846
871
|
var NEEDS_OPERATOR = /access denied|--operator|permission denied/i;
|
|
847
|
-
function tailscaleCli(
|
|
872
|
+
function tailscaleCli(exec4 = systemExec) {
|
|
848
873
|
return {
|
|
849
874
|
async status() {
|
|
850
875
|
try {
|
|
851
|
-
return JSON.parse((await
|
|
876
|
+
return JSON.parse((await exec4(["status", "--json"])).stdout);
|
|
852
877
|
} catch {
|
|
853
878
|
return null;
|
|
854
879
|
}
|
|
855
880
|
},
|
|
856
881
|
async serveStatus() {
|
|
857
882
|
try {
|
|
858
|
-
const { stdout } = await
|
|
883
|
+
const { stdout } = await exec4(["serve", "status", "--json"]);
|
|
859
884
|
return stdout.trim() === "" ? {} : JSON.parse(stdout);
|
|
860
885
|
} catch {
|
|
861
886
|
return null;
|
|
@@ -864,7 +889,7 @@ function tailscaleCli(exec5 = systemExec) {
|
|
|
864
889
|
async serve({ port, path, target, funnel }) {
|
|
865
890
|
const args = [funnel ? "funnel" : "serve", "--bg", `--https=${port}`, ...path === "/" ? [] : [`--set-path=${path}`], target];
|
|
866
891
|
try {
|
|
867
|
-
await
|
|
892
|
+
await exec4(args);
|
|
868
893
|
} catch (err) {
|
|
869
894
|
const stderr = String(err.stderr ?? "").trim();
|
|
870
895
|
if (NEEDS_OPERATOR.test(stderr)) throw new TailscaleOperatorError(stderr);
|
|
@@ -879,7 +904,12 @@ import { execFile as execFile4 } from "node:child_process";
|
|
|
879
904
|
import { readFile as readFile5 } from "node:fs/promises";
|
|
880
905
|
import { createServer } from "node:net";
|
|
881
906
|
import { promisify as promisify4 } from "node:util";
|
|
882
|
-
var
|
|
907
|
+
var run2 = promisify4(execFile4);
|
|
908
|
+
var systemDeps = {
|
|
909
|
+
exec: async (command, args) => (await run2(command, args, { env: childEnv() })).stdout,
|
|
910
|
+
readFile: (path) => readFile5(path, "utf8")
|
|
911
|
+
};
|
|
912
|
+
var AGENT_COMMAND = /\bsrc\/serve\.ts\b|(^|[\s/])tarfa serve(\s|$)/;
|
|
883
913
|
function isFree(port) {
|
|
884
914
|
return new Promise((resolve2, reject) => {
|
|
885
915
|
const server = createServer();
|
|
@@ -887,35 +917,34 @@ function isFree(port) {
|
|
|
887
917
|
server.listen(port, "127.0.0.1", () => server.close(() => resolve2(true)));
|
|
888
918
|
});
|
|
889
919
|
}
|
|
890
|
-
async function listenerPid(port, platform) {
|
|
920
|
+
async function listenerPid(port, platform, deps) {
|
|
891
921
|
try {
|
|
892
922
|
if (platform === "darwin") {
|
|
893
|
-
const
|
|
923
|
+
const stdout2 = await deps.exec("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"]);
|
|
894
924
|
const pid2 = Number(stdout2.trim().split("\n")[0]);
|
|
895
925
|
return pid2 > 0 ? pid2 : null;
|
|
896
926
|
}
|
|
897
|
-
const
|
|
927
|
+
const stdout = await deps.exec("ss", ["-ltnpH", `sport = :${port}`]);
|
|
898
928
|
const pid = /pid=(\d+)/.exec(stdout)?.[1];
|
|
899
929
|
return pid ? Number(pid) : null;
|
|
900
930
|
} catch {
|
|
901
931
|
return null;
|
|
902
932
|
}
|
|
903
933
|
}
|
|
904
|
-
async function isAgent(pid, platform) {
|
|
934
|
+
async function isAgent(pid, platform, deps) {
|
|
905
935
|
try {
|
|
906
936
|
if (platform === "darwin") {
|
|
907
|
-
|
|
908
|
-
return /\bsrc\/serve\.ts\b/.test(stdout);
|
|
937
|
+
return AGENT_COMMAND.test((await deps.exec("ps", ["-o", "command=", "-p", String(pid)])).trim());
|
|
909
938
|
}
|
|
910
|
-
return /\/(tarfa|perch)-agent\.service\b/.test(await
|
|
939
|
+
return /\/(tarfa|perch)-agent\.service\b/.test(await deps.readFile(`/proc/${pid}/cgroup`));
|
|
911
940
|
} catch {
|
|
912
941
|
return false;
|
|
913
942
|
}
|
|
914
943
|
}
|
|
915
|
-
async function portOwner(port, platform = process.platform) {
|
|
944
|
+
async function portOwner(port, platform = process.platform, deps = systemDeps) {
|
|
916
945
|
if (await isFree(port)) return "free";
|
|
917
|
-
const pid = await listenerPid(port, platform);
|
|
918
|
-
return pid !== null && await isAgent(pid, platform) ? "ours" : "other";
|
|
946
|
+
const pid = await listenerPid(port, platform, deps);
|
|
947
|
+
return pid !== null && await isAgent(pid, platform, deps) ? "ours" : "other";
|
|
919
948
|
}
|
|
920
949
|
|
|
921
950
|
// src/infrastructure/listener-check.ts
|
|
@@ -971,13 +1000,13 @@ async function mintPairingLink(deps, input) {
|
|
|
971
1000
|
// src/infrastructure/agent-service.ts
|
|
972
1001
|
import { execFile as execFile5 } from "node:child_process";
|
|
973
1002
|
import { promisify as promisify5 } from "node:util";
|
|
974
|
-
var
|
|
1003
|
+
var exec3 = promisify5(execFile5);
|
|
975
1004
|
function agentService(platform = process.platform, uid = process.getuid?.() ?? 0) {
|
|
976
1005
|
return {
|
|
977
1006
|
async restart() {
|
|
978
1007
|
const [command, args] = platform === "darwin" ? ["launchctl", ["kickstart", "-k", `gui/${uid}/app.tarfa.agent`]] : ["systemctl", ["--user", "restart", "tarfa-agent"]];
|
|
979
1008
|
try {
|
|
980
|
-
await
|
|
1009
|
+
await exec3(command, args, { env: childEnv() });
|
|
981
1010
|
} catch (err) {
|
|
982
1011
|
const stderr = err.stderr?.trim();
|
|
983
1012
|
throw new Error(`${command} ${args.join(" ")} failed${stderr ? `: ${stderr}` : ""}`);
|
|
@@ -1107,13 +1136,13 @@ function loginShell(shell = process.env.SHELL || "/bin/sh") {
|
|
|
1107
1136
|
execFile6(shell, ["-lc", script], { env: childEnv(), timeout: 15e3 }, (err, stdout) => err ? reject(err) : resolve2(stdout));
|
|
1108
1137
|
});
|
|
1109
1138
|
}
|
|
1110
|
-
function codexVersions(
|
|
1139
|
+
function codexVersions(run4 = loginShell()) {
|
|
1111
1140
|
return async () => {
|
|
1112
1141
|
const [cliOut, daemonOut] = await Promise.all([
|
|
1113
|
-
|
|
1142
|
+
run4("codex --version").catch((err) => {
|
|
1114
1143
|
throw new Error(`cannot run codex --version: ${err.message}`);
|
|
1115
1144
|
}),
|
|
1116
|
-
|
|
1145
|
+
run4("codex app-server daemon version").catch((err) => {
|
|
1117
1146
|
throw new Error(`cannot read the Codex app server's version: ${err.message}`);
|
|
1118
1147
|
})
|
|
1119
1148
|
]);
|
|
@@ -1166,7 +1195,7 @@ async function cliState(home, env) {
|
|
|
1166
1195
|
};
|
|
1167
1196
|
}
|
|
1168
1197
|
var CODEX_NEXT_STEP = "open Codex and trust the Tarfa hooks in /hooks; repeat after any hook change";
|
|
1169
|
-
async function
|
|
1198
|
+
async function run3(argv, rawEnv, home, io = {}) {
|
|
1170
1199
|
const env = withLegacyEnv(rawEnv);
|
|
1171
1200
|
if (argv[0] === "--version" || argv[0] === "-v") return packageVersion();
|
|
1172
1201
|
if (argv[0] === "setup") return setup(argv.slice(1), env, home, io);
|
|
@@ -1605,8 +1634,9 @@ Sets this machine up as a Tarfa agent, without prompting: agent.env (backed up f
|
|
|
1605
1634
|
for an installed package), the launcher the service runs (~/.tarfa/bin/tarfa-agent), the
|
|
1606
1635
|
agent key, a new device roster that admits pairing codes only (an existing one is kept), the service started at boot (systemd
|
|
1607
1636
|
user unit with linger, or launchd), the Claude Code hooks, and optionally a Tailscale address for
|
|
1608
|
-
phones. Rerunning it with the same flags
|
|
1609
|
-
|
|
1637
|
+
phones. Rerunning it with the same flags, or with none, changes nothing: --machine-id, --project-roots,
|
|
1638
|
+
--phone-port and --default-path default to what the agent.env it finds holds. It refuses rather than
|
|
1639
|
+
replace anything it did not make (an agent key, a token, another tailscale serve entry).
|
|
1610
1640
|
|
|
1611
1641
|
--machine-id <id> the name phones show for this machine
|
|
1612
1642
|
--project-roots <dir,dir> absolute folders new workspaces may be made in
|
|
@@ -1689,22 +1719,23 @@ async function setup(argv, env, home, io) {
|
|
|
1689
1719
|
} else {
|
|
1690
1720
|
envText = await readText(envPath);
|
|
1691
1721
|
}
|
|
1692
|
-
const
|
|
1693
|
-
const
|
|
1694
|
-
const
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1722
|
+
const given = withLegacyEnv(parseEnvFile(envText ?? ""));
|
|
1723
|
+
const filled = (name, value) => value !== void 0 && value !== "" && !argv.includes(name) ? [name, value] : [];
|
|
1724
|
+
const flags = parseSetupFlags([
|
|
1725
|
+
...argv,
|
|
1726
|
+
...filled("--machine-id", given.TARFA_MACHINE_ID),
|
|
1727
|
+
...filled("--project-roots", given.TARFA_PROJECT_ROOTS?.split(":").join(",")),
|
|
1728
|
+
...filled("--phone-port", given.TARFA_PHONE_PORT),
|
|
1729
|
+
...filled("--default-path", given.TARFA_DEFAULT_PATH)
|
|
1730
|
+
]);
|
|
1731
|
+
if ("error" in flags) {
|
|
1732
|
+
const read = cloneRepo !== null && copyEnv ? joinPath(cloneRepo, "deploy", "agent.env") : envPath;
|
|
1733
|
+
const source = envText === null ? `there is no ${read} yet to read them from` : `a value not given as a flag is read from ${read}`;
|
|
1734
|
+
throw new UsageError(`${flags.error} (${source})
|
|
1705
1735
|
|
|
1706
1736
|
${SETUP_USAGE.split("\n")[0]}
|
|
1707
1737
|
(tarfa setup --help for every option)`);
|
|
1738
|
+
}
|
|
1708
1739
|
const repo = await mainCheckout(kind);
|
|
1709
1740
|
const config = loadConfig({ ...withLegacyEnv(parseEnvFile(envText ?? "")), TARFA_MACHINE_ID: flags.machineId }, home);
|
|
1710
1741
|
const tailscale = fx.tailscale ?? tailscaleCli();
|
|
@@ -1950,7 +1981,7 @@ function formatRoster(roster) {
|
|
|
1950
1981
|
}
|
|
1951
1982
|
if (isEntrypoint(import.meta.url, process.argv[1])) {
|
|
1952
1983
|
const argv = process.argv.slice(2);
|
|
1953
|
-
const program = argv[0] === "serve" ? import("./serve.js").then((agent) => agent.main()) :
|
|
1984
|
+
const program = argv[0] === "serve" ? import("./serve.js").then((agent) => agent.main()) : run3(argv, process.env, homedir()).then((out) => {
|
|
1954
1985
|
process.stdout.write(`${out}
|
|
1955
1986
|
`);
|
|
1956
1987
|
});
|
|
@@ -1966,5 +1997,5 @@ export {
|
|
|
1966
1997
|
UPDATE_USAGE,
|
|
1967
1998
|
UsageError,
|
|
1968
1999
|
WaitTimeout,
|
|
1969
|
-
|
|
2000
|
+
run3 as run
|
|
1970
2001
|
};
|