@tarfa/cli 0.0.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/LICENSE +21 -0
- package/dist/chunk-LXIGP77E.js +6146 -0
- package/dist/cli.js +2001 -0
- package/dist/serve.js +4293 -0
- package/package.json +25 -8
- package/src/hooks/tarfa-guard.sh +149 -0
- package/src/hooks/tarfa-hook.sh +55 -0
- package/templates/app.tarfa.agent.plist +20 -0
- package/templates/tarfa-agent.service +16 -0
- package/templates/tarfa-agent.sh +49 -0
- package/templates/tarfa-cli-wrapper.sh +48 -0
- package/README.md +0 -3
package/package.json
CHANGED
|
@@ -1,15 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tarfa/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Tarfa:
|
|
5
|
-
"license": "MIT",
|
|
6
|
-
"author": "abumalick",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Tarfa agent and CLI: serves your Claude Code terminal sessions (tmux workspaces) to the Tarfa phone app.",
|
|
7
5
|
"repository": {
|
|
8
6
|
"type": "git",
|
|
9
|
-
"url": "
|
|
7
|
+
"url": "https://github.com/abumalick/tarfa",
|
|
8
|
+
"directory": "packages/agent"
|
|
9
|
+
},
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"type": "module",
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=24"
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"tarfa": "./dist/cli.js",
|
|
17
|
+
"perch": "./dist/cli.js"
|
|
10
18
|
},
|
|
11
19
|
"files": [
|
|
12
|
-
"
|
|
20
|
+
"dist",
|
|
21
|
+
"src/hooks/tarfa-hook.sh",
|
|
22
|
+
"src/hooks/tarfa-guard.sh",
|
|
23
|
+
"templates"
|
|
13
24
|
],
|
|
14
|
-
"
|
|
15
|
-
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"node-pty": "1.2.0-beta.18",
|
|
27
|
+
"qr": "^0.7.0",
|
|
28
|
+
"ws": "^8.18.0",
|
|
29
|
+
"zod": "^3.25.76"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {}
|
|
32
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Tarfa PreToolUse guard for Claude Code and Codex: refuse Bash commands that kill processes by
|
|
3
|
+
# name or en masse from inside a workspace, and the plain commands that would post into another
|
|
4
|
+
# session's message socket or set the tmux options Tarfa reads a workspace's orchestrator and
|
|
5
|
+
# Codex thread from (docs/MESSAGING.md).
|
|
6
|
+
#
|
|
7
|
+
# The tmux server, the agent and every workspace's CLI run as the same user, so a pattern kill
|
|
8
|
+
# from any one workspace (`pkill -f x` matches nearly every command line) takes all of them down
|
|
9
|
+
# at once. Nothing at the OS level can stop a process signalling its own user's processes, so
|
|
10
|
+
# this refuses the command before it runs. It is a guard against accidents, not a security
|
|
11
|
+
# boundary: a determined command can always get past a text match.
|
|
12
|
+
#
|
|
13
|
+
# Only commands run inside a tmux pane are checked. A quoted string is one argument, so a grep
|
|
14
|
+
# pattern or a commit message that names these commands passes; a `sh -c` script is checked as
|
|
15
|
+
# a command line of its own. A heredoc body is read as commands, and an apostrophe in one can
|
|
16
|
+
# throw the quote tracking off for the rest of the line: the cost of a text match.
|
|
17
|
+
[ -n "${TMUX_PANE:-}" ] || exit 0
|
|
18
|
+
[ -t 0 ] && exit 0
|
|
19
|
+
payload=$(cat) || exit 0
|
|
20
|
+
|
|
21
|
+
# No jq on every agent machine: take tool_input.command from the JSON by hand.
|
|
22
|
+
re='"command"[[:space:]]*:[[:space:]]*"(([^"\\]|\\.)*)"'
|
|
23
|
+
[[ $payload =~ $re ]] || exit 0
|
|
24
|
+
cmd=${BASH_REMATCH[1]}
|
|
25
|
+
cmd=${cmd//\\\\/$'\x01'}
|
|
26
|
+
cmd=${cmd//\\n/$'\n'}
|
|
27
|
+
cmd=${cmd//\\t/ }
|
|
28
|
+
cmd=${cmd//\\\"/\"}
|
|
29
|
+
cmd=${cmd//$'\x01'/\\}
|
|
30
|
+
|
|
31
|
+
deny() {
|
|
32
|
+
printf 'Blocked by Tarfa: %s The tmux server, the Tarfa agent and every other workspace run as this same user, so a kill by name or en masse can end all of them at once. Look first with `pgrep -af <pattern>`, then `kill <pid>` the explicit pids that belong to this task.\n' "$1" >&2
|
|
33
|
+
exit 2
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
refuse() {
|
|
37
|
+
printf 'Blocked by Tarfa: %s Every workspace runs as this same user, so a receiving session cannot tell such a message from a real one. Message a workspace with `tarfa send <id> "…"` and start a child with `tarfa spawn --child`.\n' "$1" >&2
|
|
38
|
+
exit 2
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
word_end='([[:space:])}"'"'"'`]|$)'
|
|
42
|
+
# tmux accepts any unambiguous prefix of a command name; kill-ser is the shortest for kill-server.
|
|
43
|
+
kill_server_re="[[:space:]]kill-ser[a-z]*$word_end"
|
|
44
|
+
kill_others_re="[[:space:]]kill-session([[:space:]].*)?[[:space:]]-a$word_end"
|
|
45
|
+
shell_c_re='^([^[:space:]]*/)?(ba|z|da)?sh([[:space:]]+-[a-zA-Z]+)*[[:space:]]+-[a-zA-Z]*c[[:space:]]+'
|
|
46
|
+
has_socket_flag() { [[ $1 =~ (^|[[:space:]])-[A-Za-z]*[SL]([[:space:]]|[^[:space:]]|$) ]]; }
|
|
47
|
+
|
|
48
|
+
# Another session's message socket: Claude Code's, the Tarfa inbox Pi reads, the Codex app server.
|
|
49
|
+
inbox_re='(cc-socks/|\.(tarfa|perch)/inbox/|app-server-control/)'
|
|
50
|
+
# tmux takes `set`, `setw` and any prefix of `set-option` or `set-window-option`; a window option
|
|
51
|
+
# answers `#{@tarfa_parent}` as well as a session option does.
|
|
52
|
+
reparent_re='[[:space:]](set|setw|set-o[a-z]*|set-w[a-z-]*)[[:space:]].*@(tarfa|perch)_(parent|codex_thread)([^[:alnum:]_]|$)'
|
|
53
|
+
|
|
54
|
+
# A pid list computed from a name is a kill by name.
|
|
55
|
+
pgrep_kill_re='(^|[^[:alnum:]_-])kill[[:space:]][^;&|]*(\$\(|`)[[:space:]]*(pgrep|pidof)([[:space:]]|$)'
|
|
56
|
+
[[ $cmd =~ $pgrep_kill_re ]] && deny 'kill $(pgrep …) kills by name.'
|
|
57
|
+
|
|
58
|
+
# One simple command per line: split on separators outside quotes, so a quoted `a|pkill|b`
|
|
59
|
+
# (a grep pattern, a commit message) stays one argument. `$(` and a backquote split inside
|
|
60
|
+
# double quotes too, since they run there. Newlines inside quotes become spaces.
|
|
61
|
+
segments() {
|
|
62
|
+
printf '%s' "$1" | LC_ALL=C awk -v sq="'" '
|
|
63
|
+
BEGIN { RS = "\001" }
|
|
64
|
+
{
|
|
65
|
+
n = length($0); q = ""; out = ""
|
|
66
|
+
for (i = 1; i <= n; i++) {
|
|
67
|
+
c = substr($0, i, 1); c2 = substr($0, i, 2)
|
|
68
|
+
if (q == sq) { if (c == sq) q = ""; out = out (c == "\n" ? " " : c); continue }
|
|
69
|
+
if (c == "\\") { out = out (c2 == "\\\n" ? " " : c2); i++; continue }
|
|
70
|
+
if (c2 == "$(") { print out; out = ""; i++; continue }
|
|
71
|
+
if (c == "`") { print out; out = ""; continue }
|
|
72
|
+
if (q == "\"") { if (c == "\"") q = ""; out = out (c == "\n" ? " " : c); continue }
|
|
73
|
+
if (c == sq || c == "\"") { q = c; out = out c; continue }
|
|
74
|
+
if (c2 == "&&" || c2 == "||") { print out; out = ""; i++; continue }
|
|
75
|
+
if (c == ";" || c == "|" || c == "&" || c == "\n") { print out; out = ""; continue }
|
|
76
|
+
out = out c
|
|
77
|
+
}
|
|
78
|
+
print out
|
|
79
|
+
}'
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
check() {
|
|
83
|
+
local seg s word payload args i
|
|
84
|
+
while IFS= read -r seg; do
|
|
85
|
+
s="$seg"
|
|
86
|
+
# Strip what can precede the command word: grouping, keywords, VAR=value, wrappers, and a
|
|
87
|
+
# `sh -c '` prefix (later commands inside the wrapper arrive as their own segments).
|
|
88
|
+
while :; do
|
|
89
|
+
s="${s#"${s%%[![:space:]]*}"}"
|
|
90
|
+
case "$s" in
|
|
91
|
+
'('* | '{'* | '!'* | \'* | '"'* | '`'*) s="${s:1}" ;;
|
|
92
|
+
then\ * | do\ * | else\ * | time\ * | exec\ * | sudo\ * | command\ * | nohup\ * | nice\ * | setsid\ *) s="${s#* }" ;;
|
|
93
|
+
timeout\ *)
|
|
94
|
+
s="${s#timeout }"
|
|
95
|
+
while [[ $s =~ ^[[:space:]]*(-[^[:space:]]+)[[:space:]]+ ]]; do s="${s:${#BASH_REMATCH[0]}}"; done
|
|
96
|
+
[[ $s =~ ^[[:space:]]*[0-9.]+[smhd]?[[:space:]]+ ]] && s="${s:${#BASH_REMATCH[0]}}"
|
|
97
|
+
;;
|
|
98
|
+
env\ *)
|
|
99
|
+
s="${s#env }"
|
|
100
|
+
while [[ $s =~ ^[[:space:]]*(-u[[:space:]]+[^[:space:]]+|-[^[:space:]]+)[[:space:]]+ ]]; do
|
|
101
|
+
s="${s:${#BASH_REMATCH[0]}}"
|
|
102
|
+
done
|
|
103
|
+
;;
|
|
104
|
+
*)
|
|
105
|
+
if [[ $s =~ $shell_c_re ]]; then
|
|
106
|
+
s="${s:${#BASH_REMATCH[0]}}"
|
|
107
|
+
# A quoted script is a command line of its own: check every command in it.
|
|
108
|
+
case "$s" in
|
|
109
|
+
\'*) payload="${s:1}"; check "${payload%%\'*}"; continue 2 ;;
|
|
110
|
+
'"'*) payload="${s:1}"; check "${payload%%\"*}"; continue 2 ;;
|
|
111
|
+
esac
|
|
112
|
+
elif [[ $s =~ ^[A-Za-z_][A-Za-z0-9_]*=(\"[^\"]*\"|\'[^\']*\'|[^[:space:]]*)[[:space:]]+ ]]; then
|
|
113
|
+
s="${s:${#BASH_REMATCH[0]}}"
|
|
114
|
+
else
|
|
115
|
+
break
|
|
116
|
+
fi
|
|
117
|
+
;;
|
|
118
|
+
esac
|
|
119
|
+
done
|
|
120
|
+
|
|
121
|
+
word=${s%%[[:space:]]*}
|
|
122
|
+
word=${word##*/}
|
|
123
|
+
word=${word%%[\"\']*}
|
|
124
|
+
case "$word" in
|
|
125
|
+
pkill) deny 'pkill kills by name.' ;;
|
|
126
|
+
killall) deny 'killall kills by name.' ;;
|
|
127
|
+
xargs) [[ $s =~ [[:space:]]kill([[:space:]]|$) ]] && deny 'xargs kill kills a computed pid list.' ;;
|
|
128
|
+
kill)
|
|
129
|
+
# `-1` as a target (not the first argument, where it names a signal) is every process.
|
|
130
|
+
read -ra args <<< "${s#kill}"
|
|
131
|
+
for ((i = 1; i < ${#args[@]}; i++)); do
|
|
132
|
+
[ "${args[i]}" = "-1" ] && deny 'kill -1 signals every process you own.'
|
|
133
|
+
done
|
|
134
|
+
;;
|
|
135
|
+
nc | nc.* | ncat | netcat | socat | curl | websocat)
|
|
136
|
+
[[ $s =~ $inbox_re ]] && refuse "$word would write to another session's message socket directly, past the header and sender check that tarfa send puts on every message."
|
|
137
|
+
;;
|
|
138
|
+
tmux)
|
|
139
|
+
has_socket_flag "$s" && continue
|
|
140
|
+
[[ $s =~ $reparent_re ]] && refuse 'Setting @tarfa_parent or @tarfa_codex_thread by hand changes which workspace the agent treats as an orchestrator or a Codex thread, so messages would carry authority nobody granted.'
|
|
141
|
+
[[ $s =~ $kill_server_re ]] && deny 'tmux kill-server without -S/-L ends the shared tmux server every workspace runs in.'
|
|
142
|
+
[[ $s =~ $kill_others_re ]] && deny 'tmux kill-session -a kills every other workspace; kill one by its exact name with -t '\''=<name>'\''.'
|
|
143
|
+
;;
|
|
144
|
+
esac
|
|
145
|
+
done < <(segments "$1")
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
check "$cmd"
|
|
149
|
+
exit 0
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Tarfa hook for Claude Code and Codex: notify the local agent of an attention event.
|
|
3
|
+
# Usage: tarfa-hook.sh <Event> <port> [agent] (the CLI's JSON payload arrives on stdin)
|
|
4
|
+
# `agent` names a CLI other than Claude Code (`codex`). It is spliced into the JSON body without
|
|
5
|
+
# escaping, so only a plain lowercase name is passed on.
|
|
6
|
+
event="$1"
|
|
7
|
+
port="${2:-8787}"
|
|
8
|
+
agent_field=""
|
|
9
|
+
[[ "${3:-}" =~ ^[a-z]+$ ]] && agent_field=",\"agent\":\"$3\""
|
|
10
|
+
# The workspace is the tmux session of the pane this hook runs in, and the hook names no session
|
|
11
|
+
# unless it can prove which one that is. TMUX and TMUX_PANE alone prove nothing: Codex runs its
|
|
12
|
+
# hooks in a shared background server that kept the environment of whichever pane started it,
|
|
13
|
+
# and an untargeted `tmux display-message` falls back to the most recently active session. Either
|
|
14
|
+
# would stamp the status onto an unrelated workspace, so the pane's own process must be one of
|
|
15
|
+
# this script's ancestors.
|
|
16
|
+
pane_session() {
|
|
17
|
+
[ -z "${TMUX:-}" ] || [ -z "${TMUX_PANE:-}" ] && return 1
|
|
18
|
+
local pane_pid session pid
|
|
19
|
+
read -r pane_pid session < <(tmux display-message -p -t "${TMUX_PANE}" '#{pane_pid} #S' 2>/dev/null)
|
|
20
|
+
[ -z "${session:-}" ] && return 1
|
|
21
|
+
pid=$$
|
|
22
|
+
for _ in $(seq 1 32); do
|
|
23
|
+
[ "${pid}" = "${pane_pid}" ] && break
|
|
24
|
+
pid="$(ps -o ppid= -p "${pid}" 2>/dev/null | tr -d ' ')"
|
|
25
|
+
[ -z "${pid}" ] || [ "${pid}" -le 1 ] && return 1
|
|
26
|
+
done
|
|
27
|
+
[ "${pid}" = "${pane_pid}" ] || return 1
|
|
28
|
+
printf '%s' "${session}"
|
|
29
|
+
}
|
|
30
|
+
# Claude's payload is forwarded verbatim as a nested object so the agent can read
|
|
31
|
+
# `background_tasks` without this script needing jq (not installed on every agent machine) or
|
|
32
|
+
# any bash JSON parsing. The tty check keeps a hand-run hook from blocking forever on stdin.
|
|
33
|
+
payload=null
|
|
34
|
+
[ -t 0 ] || payload="$(cat)"
|
|
35
|
+
[ -z "$payload" ] && payload=null
|
|
36
|
+
if session="$(pane_session)"; then
|
|
37
|
+
target="\"sessionName\":\"${session}\""
|
|
38
|
+
elif [ "${3:-}" = codex ] && [[ "${payload}" =~ \"session_id\"[[:space:]]*:[[:space:]]*\"([0-9A-Za-z-]+)\" ]]; then
|
|
39
|
+
# A Codex bound to the app-server daemon runs hooks in the daemon, under no pane; its
|
|
40
|
+
# session_id is the thread id the agent recorded for the workspace, and the agent maps it.
|
|
41
|
+
# Only id characters are taken, since the value is spliced into the body unescaped.
|
|
42
|
+
target="\"thread\":\"${BASH_REMATCH[1]}\""
|
|
43
|
+
else
|
|
44
|
+
exit 0
|
|
45
|
+
fi
|
|
46
|
+
token=""
|
|
47
|
+
[ -r "${HOME}/.tarfa/token" ] && token="$(cat "${HOME}/.tarfa/token")"
|
|
48
|
+
# Every local user can read a process's argv, so the secret goes in as a header file and the body
|
|
49
|
+
# on stdin: `printf` is a shell builtin and puts neither on any command line.
|
|
50
|
+
printf '{%s,"event":"%s"%s,"claude":%s}' "${target}" "${event}" "${agent_field}" "${payload}" |
|
|
51
|
+
curl -s -m 2 -X POST "http://127.0.0.1:${port}/hooks" \
|
|
52
|
+
-H 'content-type: application/json' \
|
|
53
|
+
-H @<(printf 'authorization: Bearer %s\n' "${token}") \
|
|
54
|
+
--data-binary @- >/dev/null 2>&1 || true
|
|
55
|
+
exit 0
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>Label</key><string>app.tarfa.agent</string>
|
|
6
|
+
<key>ProgramArguments</key>
|
|
7
|
+
<array>
|
|
8
|
+
AGENT_ARGUMENTS
|
|
9
|
+
</array>
|
|
10
|
+
<key>RunAtLoad</key><true/>
|
|
11
|
+
<key>KeepAlive</key><true/>
|
|
12
|
+
<key>StandardOutPath</key><string>LOG_DIR/agent.log</string>
|
|
13
|
+
<key>StandardErrorPath</key><string>LOG_DIR/agent.err.log</string>
|
|
14
|
+
<key>EnvironmentVariables</key>
|
|
15
|
+
<dict>
|
|
16
|
+
<key>PATH</key>
|
|
17
|
+
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
|
|
18
|
+
</dict>
|
|
19
|
+
</dict>
|
|
20
|
+
</plist>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=Tarfa agent
|
|
3
|
+
After=network-online.target
|
|
4
|
+
|
|
5
|
+
[Service]
|
|
6
|
+
ExecStart=AGENT_COMMAND
|
|
7
|
+
Restart=always
|
|
8
|
+
RestartSec=3
|
|
9
|
+
# The agent spawns the tmux server as a child, so it lives in this unit's
|
|
10
|
+
# cgroup. With the default KillMode=control-group a restart would SIGTERM the
|
|
11
|
+
# whole cgroup and kill every workspace session. KillMode=process stops only
|
|
12
|
+
# the agent, leaving the tmux server and its sessions running across restarts.
|
|
13
|
+
KillMode=process
|
|
14
|
+
|
|
15
|
+
[Install]
|
|
16
|
+
WantedBy=default.target
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# The agent service's stable entry point: the unit or plist names this file, never a versioned
|
|
3
|
+
# path, so upgrading the package changes what runs without touching the service definition.
|
|
4
|
+
set -eu
|
|
5
|
+
# Kept apart from $HOME: agent.env is sourced below, and a HOME= line there must not move the lookups.
|
|
6
|
+
home="$HOME"
|
|
7
|
+
state="$home/.tarfa"
|
|
8
|
+
if [ ! -f "$state/agent.env" ]; then
|
|
9
|
+
echo "tarfa-agent: $state/agent.env is missing: run tarfa setup" >&2
|
|
10
|
+
exit 1
|
|
11
|
+
fi
|
|
12
|
+
set -a
|
|
13
|
+
# shellcheck disable=SC1091
|
|
14
|
+
. "$state/agent.env"
|
|
15
|
+
set +a
|
|
16
|
+
TARFA_AGENT_ENV="$state/agent.env"
|
|
17
|
+
export TARFA_AGENT_ENV
|
|
18
|
+
|
|
19
|
+
# A checkout named by agent-source runs instead of the release (deploy/serve-agent.sh writes it).
|
|
20
|
+
# A path that is gone, say a removed worktree, must not crash-loop the service.
|
|
21
|
+
if [ -f "$state/agent-source" ]; then
|
|
22
|
+
checkout=""
|
|
23
|
+
IFS= read -r checkout < "$state/agent-source" || true
|
|
24
|
+
if [ -n "$checkout" ] && [ -f "$checkout/deploy/run-agent.sh" ]; then
|
|
25
|
+
exec bash "$checkout/deploy/run-agent.sh"
|
|
26
|
+
fi
|
|
27
|
+
echo "tarfa-agent: $state/agent-source names $checkout, which has no deploy/run-agent.sh; running the released agent" >&2
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
# Service managers do not inherit an interactive PATH, so find mise as deploy/run-agent.sh does.
|
|
31
|
+
# From $home, so no project's mise.toml picks the runtime.
|
|
32
|
+
cd "$home"
|
|
33
|
+
# `mise exec` would install every tool missing from the global config before starting the agent:
|
|
34
|
+
# slow, and a tool that cannot install would block it for good. So run the installed package with
|
|
35
|
+
# its node directly, as the tarfa wrapper does, and keep mise's shims on PATH for what the agent's
|
|
36
|
+
# panes and child tools run.
|
|
37
|
+
PATH="$home/.local/bin:${MISE_DATA_DIR:-$home/.local/share/mise}/shims:$PATH"
|
|
38
|
+
export PATH
|
|
39
|
+
for mise_bin in "$home/.local/bin/mise" /opt/homebrew/bin/mise /usr/local/bin/mise; do
|
|
40
|
+
if [ -x "$mise_bin" ]; then
|
|
41
|
+
install="$("$mise_bin" --cd "$home" where npm:@tarfa/cli 2>/dev/null || true)"
|
|
42
|
+
node_home="$("$mise_bin" --cd "$home" where node 2>/dev/null || true)"
|
|
43
|
+
if [ -n "$install" ] && [ -n "$node_home" ] && [ -x "$node_home/bin/node" ] && [ -f "$install/bin/tarfa" ]; then
|
|
44
|
+
exec "$node_home/bin/node" "$install/bin/tarfa" serve
|
|
45
|
+
fi
|
|
46
|
+
exec "$mise_bin" exec -- tarfa serve
|
|
47
|
+
fi
|
|
48
|
+
done
|
|
49
|
+
exec tarfa serve
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# tarfa-cli-wrapper (managed by tarfa): written by `tarfa setup` and `tarfa apply`, replaced when
|
|
3
|
+
# it differs. Runs the installed @tarfa/cli from the caller's own folder, so `tarfa spawn .` means
|
|
4
|
+
# the caller's project, whatever mise config that folder holds.
|
|
5
|
+
#
|
|
6
|
+
# The install and its node are looked up from $HOME: a mise shim would take the caller's folder's
|
|
7
|
+
# mise.toml, which dies with a trust error in an untrusted folder and runs Tarfa under that
|
|
8
|
+
# project's node in a trusted one.
|
|
9
|
+
set -u
|
|
10
|
+
|
|
11
|
+
mise_bin=""
|
|
12
|
+
for candidate in "$HOME/.local/bin/mise" /opt/homebrew/bin/mise /usr/local/bin/mise; do
|
|
13
|
+
if [ -x "$candidate" ]; then
|
|
14
|
+
mise_bin="$candidate"
|
|
15
|
+
break
|
|
16
|
+
fi
|
|
17
|
+
done
|
|
18
|
+
if [ -z "$mise_bin" ]; then
|
|
19
|
+
mise_bin="$(command -v mise 2>/dev/null || true)"
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
if [ -n "$mise_bin" ]; then
|
|
23
|
+
install="$("$mise_bin" --cd "$HOME" where npm:@tarfa/cli 2>/dev/null || true)"
|
|
24
|
+
node_home="$("$mise_bin" --cd "$HOME" where node 2>/dev/null || true)"
|
|
25
|
+
if [ -n "$install" ] && [ -n "$node_home" ] && [ -x "$node_home/bin/node" ] && [ -f "$install/bin/tarfa" ]; then
|
|
26
|
+
exec "$node_home/bin/node" "$install/bin/tarfa" "$@"
|
|
27
|
+
fi
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
# No mise, or it knows no install: the next tarfa on PATH that is not this file.
|
|
31
|
+
if [ -n "${TARFA_CLI_WRAPPER:-}" ]; then
|
|
32
|
+
echo "tarfa: no installed @tarfa/cli found (mise use -g npm:@tarfa/cli)" >&2
|
|
33
|
+
exit 1
|
|
34
|
+
fi
|
|
35
|
+
TARFA_CLI_WRAPPER=1
|
|
36
|
+
export TARFA_CLI_WRAPPER
|
|
37
|
+
old_ifs="$IFS"
|
|
38
|
+
IFS=:
|
|
39
|
+
for dir in $PATH; do
|
|
40
|
+
candidate="${dir:-.}/tarfa"
|
|
41
|
+
if [ -f "$candidate" ] && [ -x "$candidate" ] && ! [ "$candidate" -ef "$0" ]; then
|
|
42
|
+
IFS="$old_ifs"
|
|
43
|
+
exec "$candidate" "$@"
|
|
44
|
+
fi
|
|
45
|
+
done
|
|
46
|
+
IFS="$old_ifs"
|
|
47
|
+
echo "tarfa: no installed @tarfa/cli found (mise use -g npm:@tarfa/cli)" >&2
|
|
48
|
+
exit 1
|
package/README.md
DELETED