Rename project to Constant and prepare public repo
This commit is contained in:
Executable
+24
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
script_source="${BASH_SOURCE[0]:-$0}"
|
||||
while [[ -L "$script_source" ]]; do
|
||||
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
|
||||
script_source="$(readlink "$script_source")"
|
||||
[[ "$script_source" != /* ]] && script_source="$script_dir/$script_source"
|
||||
done
|
||||
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
|
||||
repo_dir="$(cd "$script_dir/.." && pwd -P)"
|
||||
default_python="$HOME/.local/share/constant/venv/bin/python3"
|
||||
|
||||
python_bin="${CONSTANT_PYTHON:-}"
|
||||
if [[ -z "$python_bin" ]]; then
|
||||
if [[ -x "$default_python" ]]; then
|
||||
python_bin="$default_python"
|
||||
else
|
||||
python_bin="python3"
|
||||
fi
|
||||
fi
|
||||
|
||||
export PYTHONPATH="$repo_dir${PYTHONPATH+:$PYTHONPATH}"
|
||||
exec "$python_bin" -m constant "$@"
|
||||
Executable
+373
@@ -0,0 +1,373 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
script_source="${BASH_SOURCE[0]:-$0}"
|
||||
while [[ -L "$script_source" ]]; do
|
||||
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
|
||||
script_source="$(readlink "$script_source")"
|
||||
[[ "$script_source" != /* ]] && script_source="$script_dir/$script_source"
|
||||
done
|
||||
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
|
||||
source "$script_dir/zellij-ai-common.sh"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") <command> [options]
|
||||
|
||||
Commands:
|
||||
send Relay one message to one machine
|
||||
broadcast Relay one message to all machines
|
||||
sync Refresh a local cache of remote inbox summaries
|
||||
tail Poll all machines and print new summaries
|
||||
|
||||
Defaults:
|
||||
repo-dir: $(zellij_ai_default_repo_dir)
|
||||
session: $(zellij_ai_default_session)
|
||||
EOF
|
||||
}
|
||||
|
||||
repo_dir="$(zellij_ai_default_repo_dir)"
|
||||
session="$(zellij_ai_default_session)"
|
||||
cache_dir="$(zellij_ai_bridge_cache_dir)"
|
||||
machines=()
|
||||
|
||||
default_machine_specs_array() {
|
||||
local line
|
||||
while IFS= read -r line; do
|
||||
[[ -z "$line" ]] && continue
|
||||
printf '%s\n' "$line"
|
||||
done < <(zellij_ai_default_machine_specs)
|
||||
}
|
||||
|
||||
load_machines() {
|
||||
if [[ ${#machines[@]} -eq 0 ]]; then
|
||||
mapfile -t machines < <(default_machine_specs_array)
|
||||
fi
|
||||
}
|
||||
|
||||
resolve_machine() {
|
||||
local needle="$1"
|
||||
local resolved
|
||||
|
||||
if ! resolved="$(zellij_ai_resolve_machine_spec "$needle")"; then
|
||||
echo "Unknown machine: $needle" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
printf '%s\n' "$resolved"
|
||||
}
|
||||
|
||||
run_ai_msg() {
|
||||
local label="$1"
|
||||
local target="$2"
|
||||
shift 2
|
||||
|
||||
if zellij_ai_is_local_target "$target"; then
|
||||
ZELLIJ_AI_MACHINE_NAME="$label" \
|
||||
ZELLIJ_AI_SESSION="$session" \
|
||||
"$(zellij_ai_expand_home_path "$repo_dir")/scripts/ai-msg.sh" "$@"
|
||||
return 0
|
||||
fi
|
||||
|
||||
ssh "$target" bash -s -- "$label" "$session" "$repo_dir" "$@" <<'EOF'
|
||||
set -euo pipefail
|
||||
|
||||
label="$1"
|
||||
shift
|
||||
session="$1"
|
||||
shift
|
||||
repo_dir_input="$1"
|
||||
shift
|
||||
|
||||
expand_home_path() {
|
||||
case "$1" in
|
||||
'$HOME'|'$HOME/'*)
|
||||
printf '%s\n' "${HOME}${1#\$HOME}"
|
||||
;;
|
||||
"~")
|
||||
printf '%s\n' "$HOME"
|
||||
;;
|
||||
"~/"*)
|
||||
printf '%s\n' "$HOME/${1#~/}"
|
||||
;;
|
||||
*)
|
||||
printf '%s\n' "$1"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
repo_dir="$(expand_home_path "$repo_dir_input")"
|
||||
|
||||
export ZELLIJ_AI_MACHINE_NAME="$label"
|
||||
export ZELLIJ_AI_SESSION="$session"
|
||||
|
||||
exec "$repo_dir/scripts/ai-msg.sh" "$@"
|
||||
EOF
|
||||
}
|
||||
|
||||
sync_once() {
|
||||
local machine_spec resolved label target cache_file
|
||||
mkdir -p "$cache_dir"
|
||||
load_machines
|
||||
|
||||
for machine_spec in "${machines[@]}"; do
|
||||
IFS=$'\t' read -r label target <<<"$(resolve_machine "$machine_spec")"
|
||||
cache_file="$cache_dir/${label}.tsv"
|
||||
run_ai_msg "$label" "$target" list --for all >"$cache_file"
|
||||
done
|
||||
}
|
||||
|
||||
command_send() {
|
||||
local from_machine="$(zellij_ai_local_machine_label)"
|
||||
local from_role="bridge"
|
||||
local to_machine=""
|
||||
local to_role=""
|
||||
local message_text=""
|
||||
local message_file=""
|
||||
local resolved label target
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--from-machine)
|
||||
from_machine="$2"
|
||||
shift 2
|
||||
;;
|
||||
--from)
|
||||
from_role="$2"
|
||||
shift 2
|
||||
;;
|
||||
--to-machine)
|
||||
to_machine="$2"
|
||||
shift 2
|
||||
;;
|
||||
--to)
|
||||
to_role="$2"
|
||||
shift 2
|
||||
;;
|
||||
--message)
|
||||
message_text="$2"
|
||||
shift 2
|
||||
;;
|
||||
--file)
|
||||
message_file="$2"
|
||||
shift 2
|
||||
;;
|
||||
--machine)
|
||||
machines+=("$2")
|
||||
shift 2
|
||||
;;
|
||||
--repo-dir)
|
||||
repo_dir="$2"
|
||||
shift 2
|
||||
;;
|
||||
--session)
|
||||
session="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option for send: $1" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$to_machine" || -z "$to_role" ]]; then
|
||||
echo "--to-machine and --to are required." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
IFS=$'\t' read -r label target <<<"$(resolve_machine "$to_machine")"
|
||||
|
||||
if [[ -n "$message_text" ]]; then
|
||||
run_ai_msg "$label" "$target" send --from "$from_role" --to "$to_role" --source-machine "$from_machine" --source-role "$from_role" --relay-via "$(zellij_ai_local_machine_label)" --message "$message_text"
|
||||
elif [[ -n "$message_file" ]]; then
|
||||
run_ai_msg "$label" "$target" send --from "$from_role" --to "$to_role" --source-machine "$from_machine" --source-role "$from_role" --relay-via "$(zellij_ai_local_machine_label)" --file "$message_file"
|
||||
elif [[ ! -t 0 ]]; then
|
||||
run_ai_msg "$label" "$target" send --from "$from_role" --to "$to_role" --source-machine "$from_machine" --source-role "$from_role" --relay-via "$(zellij_ai_local_machine_label)"
|
||||
else
|
||||
echo "Provide --message, --file, or stdin." >&2
|
||||
exit 2
|
||||
fi
|
||||
}
|
||||
|
||||
command_broadcast() {
|
||||
local from_machine="$(zellij_ai_local_machine_label)"
|
||||
local from_role="bridge"
|
||||
local message_text=""
|
||||
local message_file=""
|
||||
local machine_spec resolved label target
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--from-machine)
|
||||
from_machine="$2"
|
||||
shift 2
|
||||
;;
|
||||
--from)
|
||||
from_role="$2"
|
||||
shift 2
|
||||
;;
|
||||
--message)
|
||||
message_text="$2"
|
||||
shift 2
|
||||
;;
|
||||
--file)
|
||||
message_file="$2"
|
||||
shift 2
|
||||
;;
|
||||
--machine)
|
||||
machines+=("$2")
|
||||
shift 2
|
||||
;;
|
||||
--repo-dir)
|
||||
repo_dir="$2"
|
||||
shift 2
|
||||
;;
|
||||
--session)
|
||||
session="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option for broadcast: $1" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
load_machines
|
||||
|
||||
for machine_spec in "${machines[@]}"; do
|
||||
IFS=$'\t' read -r label target <<<"$(resolve_machine "$machine_spec")"
|
||||
if [[ -n "$message_text" ]]; then
|
||||
run_ai_msg "$label" "$target" send --from "$from_role" --to all --source-machine "$from_machine" --source-role "$from_role" --relay-via "$(zellij_ai_local_machine_label)" --message "$message_text"
|
||||
elif [[ -n "$message_file" ]]; then
|
||||
run_ai_msg "$label" "$target" send --from "$from_role" --to all --source-machine "$from_machine" --source-role "$from_role" --relay-via "$(zellij_ai_local_machine_label)" --file "$message_file"
|
||||
elif [[ ! -t 0 ]]; then
|
||||
run_ai_msg "$label" "$target" send --from "$from_role" --to all --source-machine "$from_machine" --source-role "$from_role" --relay-via "$(zellij_ai_local_machine_label)"
|
||||
else
|
||||
echo "Provide --message, --file, or stdin." >&2
|
||||
exit 2
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
command_sync() {
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--machine)
|
||||
machines+=("$2")
|
||||
shift 2
|
||||
;;
|
||||
--repo-dir)
|
||||
repo_dir="$2"
|
||||
shift 2
|
||||
;;
|
||||
--session)
|
||||
session="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option for sync: $1" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
sync_once
|
||||
|
||||
find "$cache_dir" -type f -name '*.tsv' | sort | while IFS= read -r file; do
|
||||
cat "$file"
|
||||
done
|
||||
}
|
||||
|
||||
command_tail() {
|
||||
local interval=3
|
||||
declare -A seen=()
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--machine)
|
||||
machines+=("$2")
|
||||
shift 2
|
||||
;;
|
||||
--repo-dir)
|
||||
repo_dir="$2"
|
||||
shift 2
|
||||
;;
|
||||
--session)
|
||||
session="$2"
|
||||
shift 2
|
||||
;;
|
||||
--interval)
|
||||
interval="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option for tail: $1" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
while true; do
|
||||
sync_once
|
||||
while IFS= read -r file; do
|
||||
while IFS= read -r line; do
|
||||
[[ -z "$line" ]] && continue
|
||||
if [[ -n "${seen[$line]:-}" ]]; then
|
||||
continue
|
||||
fi
|
||||
seen["$line"]=1
|
||||
printf '%s\n' "$line"
|
||||
done <"$file"
|
||||
done < <(find "$cache_dir" -type f -name '*.tsv' | sort)
|
||||
sleep "$interval"
|
||||
done
|
||||
}
|
||||
|
||||
if [[ $# -lt 1 ]]; then
|
||||
usage >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
command="$1"
|
||||
shift
|
||||
|
||||
case "$command" in
|
||||
send)
|
||||
command_send "$@"
|
||||
;;
|
||||
broadcast)
|
||||
command_broadcast "$@"
|
||||
;;
|
||||
sync)
|
||||
command_sync "$@"
|
||||
;;
|
||||
tail)
|
||||
command_tail "$@"
|
||||
;;
|
||||
-h|--help|help)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
echo "Unknown command: $command" >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
Executable
+65
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: ai-copy.sh [options]
|
||||
|
||||
Copy text into the macOS clipboard on the current machine.
|
||||
|
||||
Options:
|
||||
--text TEXT Copy this text directly
|
||||
--file FILE Copy the contents of FILE
|
||||
-h, --help Show this help
|
||||
|
||||
If neither --text nor --file is provided, stdin is copied.
|
||||
EOF
|
||||
}
|
||||
|
||||
text=""
|
||||
file=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--text)
|
||||
text="$2"
|
||||
shift 2
|
||||
;;
|
||||
--file)
|
||||
file="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1" >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -n "$text" && -n "$file" ]]; then
|
||||
echo "Use either --text or --file, not both." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if ! command -v pbcopy >/dev/null 2>&1; then
|
||||
echo "pbcopy is not available on this machine." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "$text" ]]; then
|
||||
printf '%s' "$text" | pbcopy
|
||||
elif [[ -n "$file" ]]; then
|
||||
cat "$file" | pbcopy
|
||||
elif [[ ! -t 0 ]]; then
|
||||
cat | pbcopy
|
||||
else
|
||||
echo "Nothing to copy. Provide --text, --file, or pipe stdin." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
printf 'Copied to the macOS clipboard.\n'
|
||||
Executable
+376
@@ -0,0 +1,376 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
script_source="${BASH_SOURCE[0]:-$0}"
|
||||
while [[ -L "$script_source" ]]; do
|
||||
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
|
||||
script_source="$(readlink "$script_source")"
|
||||
[[ "$script_source" != /* ]] && script_source="$script_dir/$script_source"
|
||||
done
|
||||
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
|
||||
source "$script_dir/zellij-ai-common.sh"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: ai-msg.sh <command> [options]
|
||||
|
||||
Commands:
|
||||
send Send a message to one role
|
||||
broadcast Send a message to all roles on the current machine
|
||||
inbox Show messages addressed to one role
|
||||
tail Poll and print new messages for one role
|
||||
list List matching messages in TSV form
|
||||
|
||||
Environment defaults:
|
||||
ZELLIJ_AI_BUS_DIR
|
||||
ZELLIJ_AI_MACHINE_NAME
|
||||
ZELLIJ_AI_ROLE
|
||||
ZELLIJ_AI_SESSION
|
||||
EOF
|
||||
}
|
||||
|
||||
machine_name="$(zellij_ai_current_machine_name)"
|
||||
session="${ZELLIJ_AI_SESSION:-$(zellij_ai_default_session)}"
|
||||
role_default="${ZELLIJ_AI_ROLE:-all}"
|
||||
bus_dir="${ZELLIJ_AI_BUS_DIR:-$(zellij_ai_session_state_dir "$session")/bus}"
|
||||
messages_dir="${bus_dir}/messages"
|
||||
|
||||
ensure_messages_dir() {
|
||||
mkdir -p "$messages_dir"
|
||||
}
|
||||
|
||||
random_suffix() {
|
||||
od -An -N4 -tx1 /dev/urandom 2>/dev/null | tr -d ' \n' || printf '%s' "$$"
|
||||
}
|
||||
|
||||
message_id() {
|
||||
printf '%s\n' "${machine_name}-${session}-$(date -u +%Y%m%dT%H%M%SZ)-$$-$(random_suffix)"
|
||||
}
|
||||
|
||||
timestamp_utc() {
|
||||
date -u +"%Y-%m-%dT%H:%M:%SZ"
|
||||
}
|
||||
|
||||
read_body_input() {
|
||||
local message_text="$1"
|
||||
local message_file="$2"
|
||||
|
||||
if [[ -n "$message_text" ]]; then
|
||||
printf '%s' "$message_text"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -n "$message_file" ]]; then
|
||||
cat "$message_file"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ ! -t 0 ]]; then
|
||||
cat
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "No message body provided. Use --message, --file, or stdin." >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
read_header() {
|
||||
local key="$1"
|
||||
local file="$2"
|
||||
sed -n "s/^${key}: //p" "$file" | head -n 1
|
||||
}
|
||||
|
||||
read_body() {
|
||||
awk 'seen_blank { print } /^$/ { seen_blank = 1 }' "$1"
|
||||
}
|
||||
|
||||
message_matches_role() {
|
||||
local file="$1"
|
||||
local role="$2"
|
||||
local target
|
||||
|
||||
target="$(read_header target "$file")"
|
||||
|
||||
if [[ "$role" == "all" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
[[ "$target" == "$role" || "$target" == "all" ]]
|
||||
}
|
||||
|
||||
list_message_files() {
|
||||
if [[ ! -d "$messages_dir" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
find "$messages_dir" -type f -name '*.msg' | sort
|
||||
}
|
||||
|
||||
print_message_tsv() {
|
||||
local file="$1"
|
||||
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
|
||||
"$(read_header id "$file")" \
|
||||
"$(read_header timestamp "$file")" \
|
||||
"$(read_header machine "$file")" \
|
||||
"$(read_header session "$file")" \
|
||||
"$(read_header sender "$file")" \
|
||||
"$(read_header target "$file")" \
|
||||
"$(read_header source_machine "$file")" \
|
||||
"$(read_header source_role "$file")" \
|
||||
"$file"
|
||||
}
|
||||
|
||||
print_message_pretty() {
|
||||
local file="$1"
|
||||
local id timestamp sender target stored_machine source_machine source_role relay_via
|
||||
|
||||
id="$(read_header id "$file")"
|
||||
timestamp="$(read_header timestamp "$file")"
|
||||
sender="$(read_header sender "$file")"
|
||||
target="$(read_header target "$file")"
|
||||
stored_machine="$(read_header machine "$file")"
|
||||
source_machine="$(read_header source_machine "$file")"
|
||||
source_role="$(read_header source_role "$file")"
|
||||
relay_via="$(read_header relay_via "$file")"
|
||||
|
||||
printf '[%s] %s -> %s on %s (%s)\n' "$timestamp" "$sender" "$target" "$stored_machine" "$id"
|
||||
if [[ -n "$source_machine" || -n "$source_role" ]]; then
|
||||
printf 'source: %s/%s\n' "${source_machine:-unknown}" "${source_role:-unknown}"
|
||||
fi
|
||||
if [[ -n "$relay_via" ]]; then
|
||||
printf 'relay: %s\n' "$relay_via"
|
||||
fi
|
||||
printf '%s\n' '---'
|
||||
read_body "$file"
|
||||
printf '\n'
|
||||
}
|
||||
|
||||
write_message_file() {
|
||||
local sender="$1"
|
||||
local target="$2"
|
||||
local source_machine="$3"
|
||||
local source_role="$4"
|
||||
local relay_via="$5"
|
||||
local body="$6"
|
||||
local id timestamp file tmp_file
|
||||
|
||||
ensure_messages_dir
|
||||
|
||||
id="$(message_id)"
|
||||
timestamp="$(timestamp_utc)"
|
||||
file="${messages_dir}/${id}.msg"
|
||||
tmp_file="$(mktemp "${messages_dir}/.tmp.XXXXXX")"
|
||||
|
||||
{
|
||||
printf 'id: %s\n' "$id"
|
||||
printf 'timestamp: %s\n' "$timestamp"
|
||||
printf 'machine: %s\n' "$machine_name"
|
||||
printf 'session: %s\n' "$session"
|
||||
printf 'sender: %s\n' "$sender"
|
||||
printf 'target: %s\n' "$target"
|
||||
printf 'source_machine: %s\n' "$source_machine"
|
||||
printf 'source_role: %s\n' "$source_role"
|
||||
printf 'relay_via: %s\n' "$relay_via"
|
||||
printf '\n'
|
||||
printf '%s\n' "$body"
|
||||
} >"$tmp_file"
|
||||
|
||||
mv "$tmp_file" "$file"
|
||||
printf '%s\n' "$file"
|
||||
}
|
||||
|
||||
command_send() {
|
||||
local sender="$role_default"
|
||||
local target=""
|
||||
local message_text=""
|
||||
local message_file=""
|
||||
local source_machine=""
|
||||
local source_role=""
|
||||
local relay_via=""
|
||||
local body file
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--from)
|
||||
sender="$2"
|
||||
shift 2
|
||||
;;
|
||||
--to)
|
||||
target="$2"
|
||||
shift 2
|
||||
;;
|
||||
--message)
|
||||
message_text="$2"
|
||||
shift 2
|
||||
;;
|
||||
--file)
|
||||
message_file="$2"
|
||||
shift 2
|
||||
;;
|
||||
--source-machine)
|
||||
source_machine="$2"
|
||||
shift 2
|
||||
;;
|
||||
--source-role)
|
||||
source_role="$2"
|
||||
shift 2
|
||||
;;
|
||||
--relay-via)
|
||||
relay_via="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option for send: $1" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$target" ]]; then
|
||||
echo "--to is required for send." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
body="$(read_body_input "$message_text" "$message_file")"
|
||||
file="$(write_message_file "$sender" "$target" "$source_machine" "$source_role" "$relay_via" "$body")"
|
||||
printf 'stored: %s\n' "$file"
|
||||
}
|
||||
|
||||
command_broadcast() {
|
||||
command_send --to all "$@"
|
||||
}
|
||||
|
||||
command_list() {
|
||||
local role="all"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--for)
|
||||
role="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option for list: $1" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
while IFS= read -r file; do
|
||||
message_matches_role "$file" "$role" || continue
|
||||
print_message_tsv "$file"
|
||||
done < <(list_message_files)
|
||||
}
|
||||
|
||||
command_inbox() {
|
||||
local role="$role_default"
|
||||
local found=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--for)
|
||||
role="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option for inbox: $1" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
while IFS= read -r file; do
|
||||
message_matches_role "$file" "$role" || continue
|
||||
print_message_pretty "$file"
|
||||
found=true
|
||||
done < <(list_message_files)
|
||||
|
||||
if ! $found; then
|
||||
printf 'No messages for %s in %s\n' "$role" "$messages_dir"
|
||||
fi
|
||||
}
|
||||
|
||||
command_tail() {
|
||||
local role="$role_default"
|
||||
local interval=2
|
||||
declare -A seen=()
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--for)
|
||||
role="$2"
|
||||
shift 2
|
||||
;;
|
||||
--interval)
|
||||
interval="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option for tail: $1" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
while true; do
|
||||
while IFS= read -r file; do
|
||||
message_matches_role "$file" "$role" || continue
|
||||
if [[ -n "${seen[$file]:-}" ]]; then
|
||||
continue
|
||||
fi
|
||||
seen["$file"]=1
|
||||
print_message_pretty "$file"
|
||||
done < <(list_message_files)
|
||||
sleep "$interval"
|
||||
done
|
||||
}
|
||||
|
||||
if [[ $# -lt 1 ]]; then
|
||||
usage >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
command="$1"
|
||||
shift
|
||||
|
||||
case "$command" in
|
||||
send)
|
||||
command_send "$@"
|
||||
;;
|
||||
broadcast)
|
||||
command_broadcast "$@"
|
||||
;;
|
||||
list)
|
||||
command_list "$@"
|
||||
;;
|
||||
inbox)
|
||||
command_inbox "$@"
|
||||
;;
|
||||
tail)
|
||||
command_tail "$@"
|
||||
;;
|
||||
-h|--help|help)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
echo "Unknown command: $command" >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
Executable
+13
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
script_source="${BASH_SOURCE[0]:-$0}"
|
||||
while [[ -L "$script_source" ]]; do
|
||||
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
|
||||
script_source="$(readlink "$script_source")"
|
||||
[[ "$script_source" != /* ]] && script_source="$script_dir/$script_source"
|
||||
done
|
||||
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
|
||||
|
||||
export CONSTANT_SCRIPT_NAME="$(basename "$0")"
|
||||
exec "$script_dir/zellij-ai-fleet-install.sh" "$@"
|
||||
Executable
+13
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
script_source="${BASH_SOURCE[0]:-$0}"
|
||||
while [[ -L "$script_source" ]]; do
|
||||
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
|
||||
script_source="$(readlink "$script_source")"
|
||||
[[ "$script_source" != /* ]] && script_source="$script_dir/$script_source"
|
||||
done
|
||||
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
|
||||
|
||||
export CONSTANT_SCRIPT_NAME="$(basename "$0")"
|
||||
exec "$script_dir/zellij-ai-fleet.sh" "$@"
|
||||
Executable
+13
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
script_source="${BASH_SOURCE[0]:-$0}"
|
||||
while [[ -L "$script_source" ]]; do
|
||||
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
|
||||
script_source="$(readlink "$script_source")"
|
||||
[[ "$script_source" != /* ]] && script_source="$script_dir/$script_source"
|
||||
done
|
||||
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
|
||||
|
||||
export CONSTANT_SCRIPT_NAME="$(basename "$0")"
|
||||
exec "$script_dir/zellij-ai-triple.sh" "$@"
|
||||
@@ -1,7 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
|
||||
script_source="${BASH_SOURCE[0]:-$0}"
|
||||
while [[ -L "$script_source" ]]; do
|
||||
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
|
||||
script_source="$(readlink "$script_source")"
|
||||
[[ "$script_source" != /* ]] && script_source="$script_dir/$script_source"
|
||||
done
|
||||
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
|
||||
bootstrap_file="${ZELLIJ_AI_BOOTSTRAP_FILE:-}"
|
||||
workspace="${ZELLIJ_AI_WORKSPACE:-$PWD}"
|
||||
|
||||
@@ -12,17 +18,23 @@ fi
|
||||
if [[ -n "$bootstrap_file" && ! -f "$bootstrap_file" ]]; then
|
||||
: >"$bootstrap_file"
|
||||
|
||||
printf -v codex1_cmd '%q %q' "$script_dir/zellij-ai-codex-pane.sh" "1"
|
||||
printf -v codex2_cmd '%q %q' "$script_dir/zellij-ai-codex-pane.sh" "2"
|
||||
printf -v codex_cmd '%q' "$script_dir/zellij-ai-codex-pane.sh"
|
||||
printf -v copilot_cmd '%q' "$script_dir/zellij-ai-copilot-pane.sh"
|
||||
printf -v vibe_cmd '%q' "$script_dir/zellij-ai-vibe-pane.sh"
|
||||
|
||||
zellij action new-pane -d right -n "${ZELLIJ_AI_CODEX1_LABEL:-codex-1}" --cwd "$workspace"
|
||||
zellij action new-pane -d right -n "${ZELLIJ_AI_CODEX_LABEL:-codex}" --cwd "$workspace"
|
||||
zellij action move-focus right
|
||||
zellij action write-chars "$codex1_cmd"
|
||||
zellij action write-chars "$codex_cmd"
|
||||
zellij action write 10
|
||||
|
||||
zellij action new-pane -d down -n "${ZELLIJ_AI_CODEX2_LABEL:-codex-2}" --cwd "$workspace"
|
||||
zellij action new-pane -d down -n "${ZELLIJ_AI_COPILOT_LABEL:-copilot}" --cwd "$workspace"
|
||||
zellij action move-focus down
|
||||
zellij action write-chars "$codex2_cmd"
|
||||
zellij action write-chars "$copilot_cmd"
|
||||
zellij action write 10
|
||||
|
||||
zellij action new-pane -d down -n "${ZELLIJ_AI_VIBE_LABEL:-vibe}" --cwd "$workspace"
|
||||
zellij action move-focus down
|
||||
zellij action write-chars "$vibe_cmd"
|
||||
zellij action write 10
|
||||
|
||||
zellij action move-focus left
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
script_source="${BASH_SOURCE[0]:-$0}"
|
||||
while [[ -L "$script_source" ]]; do
|
||||
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
|
||||
script_source="$(readlink "$script_source")"
|
||||
[[ "$script_source" != /* ]] && script_source="$script_dir/$script_source"
|
||||
done
|
||||
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
|
||||
source "$script_dir/zellij-ai-common.sh"
|
||||
|
||||
workspace="${ZELLIJ_AI_WORKSPACE:-$PWD}"
|
||||
machine_name="${ZELLIJ_AI_MACHINE_NAME:-unknown}"
|
||||
session_name="${ZELLIJ_AI_SESSION:-$(zellij_ai_default_session)}"
|
||||
repo_dir="${ZELLIJ_AI_REPO_DIR:-}"
|
||||
bus_dir="${ZELLIJ_AI_BUS_DIR:-$(zellij_ai_session_state_dir "$session_name")/bus}"
|
||||
|
||||
if [[ ! -d "$workspace" ]]; then
|
||||
echo "Workspace not found: $workspace" >&2
|
||||
@@ -12,10 +25,26 @@ if [[ -n "${ZELLIJ_AI_CLAUDE_CONFIG_DIR:-}" ]]; then
|
||||
export CLAUDE_CONFIG_DIR="$ZELLIJ_AI_CLAUDE_CONFIG_DIR"
|
||||
fi
|
||||
|
||||
export PATH="$(zellij_ai_agent_path)"
|
||||
|
||||
if [[ -n "$repo_dir" && -d "$repo_dir/scripts" ]]; then
|
||||
export PATH="$repo_dir/scripts:$PATH"
|
||||
fi
|
||||
|
||||
export ZELLIJ_AI_ROLE="claude"
|
||||
export ZELLIJ_AI_MACHINE_NAME="$machine_name"
|
||||
export ZELLIJ_AI_SESSION="$session_name"
|
||||
export ZELLIJ_AI_BUS_DIR="$bus_dir"
|
||||
|
||||
cd "$workspace"
|
||||
|
||||
echo "Claude pane"
|
||||
echo "machine: $machine_name"
|
||||
echo "workspace: $workspace"
|
||||
echo "bus: $bus_dir"
|
||||
if [[ -n "$repo_dir" ]]; then
|
||||
echo "helper: $repo_dir/scripts/ai-msg.sh"
|
||||
fi
|
||||
if [[ -n "${CLAUDE_CONFIG_DIR:-}" ]]; then
|
||||
echo "config: $CLAUDE_CONFIG_DIR"
|
||||
fi
|
||||
|
||||
+72
-121
@@ -1,160 +1,111 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
|
||||
script_source="${BASH_SOURCE[0]:-$0}"
|
||||
while [[ -L "$script_source" ]]; do
|
||||
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
|
||||
script_source="$(readlink "$script_source")"
|
||||
[[ "$script_source" != /* ]] && script_source="$script_dir/$script_source"
|
||||
done
|
||||
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
|
||||
source "$script_dir/zellij-ai-common.sh"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: zellij-ai-codex-pane.sh <slot>
|
||||
Usage: zellij-ai-codex-pane.sh [slot]
|
||||
|
||||
slot:
|
||||
1 Launch the first Codex container/profile
|
||||
2 Launch the second Codex container/profile
|
||||
1 Deprecated alias for the single host-local Codex pane
|
||||
2 Deprecated alias for the single host-local Codex pane
|
||||
EOF
|
||||
}
|
||||
|
||||
find_codex_binary() {
|
||||
if [[ -n "${ZELLIJ_AI_CODEX_BINARY:-}" && -x "${ZELLIJ_AI_CODEX_BINARY}" ]]; then
|
||||
printf '%s\n' "${ZELLIJ_AI_CODEX_BINARY}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -x "$HOME/.npm-global/lib/node_modules/@openai/codex/node_modules/@openai/codex-linux-x64/vendor/x86_64-unknown-linux-musl/codex/codex" ]]; then
|
||||
printf '%s\n' "$HOME/.npm-global/lib/node_modules/@openai/codex/node_modules/@openai/codex-linux-x64/vendor/x86_64-unknown-linux-musl/codex/codex"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -n "$(command -v codex || true)" ]]; then
|
||||
local wrapper_dir package_dir candidate
|
||||
wrapper_dir="$(cd "$(dirname "$(command -v codex)")" && pwd -P)"
|
||||
package_dir="$(cd "$wrapper_dir/../lib/node_modules/@openai/codex/node_modules/@openai" 2>/dev/null && pwd -P || true)"
|
||||
if [[ -n "$package_dir" ]]; then
|
||||
candidate="$(find "$package_dir" -path '*/vendor/*/codex/codex' -type f -print -quit 2>/dev/null || true)"
|
||||
if [[ -n "$candidate" && -x "$candidate" ]]; then
|
||||
printf '%s\n' "$candidate"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
return 1
|
||||
warn_deprecated() {
|
||||
printf 'Warning: %s\n' "$*" >&2
|
||||
}
|
||||
|
||||
if [[ $# -ne 1 ]]; then
|
||||
if [[ $# -gt 1 ]]; then
|
||||
usage >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
slot="$1"
|
||||
|
||||
case "$slot" in
|
||||
1)
|
||||
label="${ZELLIJ_AI_CODEX1_LABEL:-codex-1}"
|
||||
profile_dir="${ZELLIJ_AI_CODEX1_HOME:-$HOME/.codex-profiles/codex-1}"
|
||||
;;
|
||||
2)
|
||||
label="${ZELLIJ_AI_CODEX2_LABEL:-codex-2}"
|
||||
profile_dir="${ZELLIJ_AI_CODEX2_HOME:-$HOME/.codex-profiles/codex-2}"
|
||||
;;
|
||||
*)
|
||||
usage >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
if [[ $# -eq 1 ]]; then
|
||||
case "$1" in
|
||||
1)
|
||||
warn_deprecated "slot 1 is deprecated; Codex now runs as a single host-local pane."
|
||||
;;
|
||||
2)
|
||||
warn_deprecated "slot 2 is deprecated; launching the single host-local Codex pane."
|
||||
;;
|
||||
-h|--help|help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
usage >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
workspace="${ZELLIJ_AI_WORKSPACE:-$PWD}"
|
||||
image="${ZELLIJ_AI_CODEX_IMAGE:-codercom/code-server:latest}"
|
||||
profile_dir="${ZELLIJ_AI_CODEX_HOME:-$(zellij_ai_default_codex_home)}"
|
||||
label="${ZELLIJ_AI_CODEX_LABEL:-codex}"
|
||||
main_codex_config="${ZELLIJ_AI_MAIN_CODEX_CONFIG:-$HOME/.codex/config.toml}"
|
||||
codex_binary="$(find_codex_binary || true)"
|
||||
term_value="${ZELLIJ_AI_CODEX_TERM:-${TERM:-xterm-256color}}"
|
||||
color_term_value="${COLORTERM:-truecolor}"
|
||||
|
||||
if [[ "$term_value" == "dumb" ]]; then
|
||||
term_value="xterm-256color"
|
||||
fi
|
||||
repo_dir="${ZELLIJ_AI_REPO_DIR:-$(cd "$script_dir/.." && pwd -P)}"
|
||||
session_name="${ZELLIJ_AI_SESSION:-$(zellij_ai_default_session)}"
|
||||
bus_dir="${ZELLIJ_AI_BUS_DIR:-$(zellij_ai_session_state_dir "$session_name")/bus}"
|
||||
machine_name="$(zellij_ai_current_machine_name)"
|
||||
agent_path="$(zellij_ai_agent_path)"
|
||||
|
||||
if [[ ! -d "$workspace" ]]; then
|
||||
echo "Workspace not found: $workspace" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "$codex_binary" || ! -x "$codex_binary" ]]; then
|
||||
echo "Unable to locate the native Codex binary on the host." >&2
|
||||
echo "Set ZELLIJ_AI_CODEX_BINARY if Codex is installed elsewhere." >&2
|
||||
export PATH="$agent_path"
|
||||
|
||||
if ! command -v codex >/dev/null 2>&1; then
|
||||
echo "Codex CLI not found on the host." >&2
|
||||
echo "Install it with: npm install -g @openai/codex" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
platform_dir="$(dirname "$(dirname "$codex_binary")")"
|
||||
rg_binary="$platform_dir/path/rg"
|
||||
|
||||
if [[ ! -x "$rg_binary" ]]; then
|
||||
echo "Bundled rg binary not found next to Codex: $rg_binary" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! docker image inspect "$image" >/dev/null 2>&1; then
|
||||
echo "Docker image not found locally: $image" >&2
|
||||
echo "Pull or build the image first, or relaunch with --codex-image." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$profile_dir"
|
||||
profile_dir="$(mkdir -p "$(zellij_ai_expand_home_path "$profile_dir")" && cd "$(zellij_ai_expand_home_path "$profile_dir")" && pwd -P)"
|
||||
mkdir -p "$bus_dir/messages"
|
||||
|
||||
if [[ ! -f "$profile_dir/config.toml" && -f "$main_codex_config" ]]; then
|
||||
cp "$main_codex_config" "$profile_dir/config.toml"
|
||||
fi
|
||||
|
||||
container_name="zellij-$(printf '%s' "$label" | tr '[:upper:]' '[:lower:]' | tr -cs '[:alnum:]._-' '-')"
|
||||
uid="$(id -u)"
|
||||
gid="$(id -g)"
|
||||
if [[ -d "$repo_dir/scripts" ]]; then
|
||||
export PATH="$repo_dir/scripts:$PATH"
|
||||
fi
|
||||
|
||||
read -r -d '' inner_script <<EOF || true
|
||||
set -euo pipefail
|
||||
cd /workspace
|
||||
export HOME=/codex-home
|
||||
export CODEX_HOME=/codex-home
|
||||
echo "Codex pane: $label"
|
||||
echo "workspace: /workspace"
|
||||
echo "profile: /codex-home"
|
||||
echo "image: $image"
|
||||
export CODEX_HOME="$profile_dir"
|
||||
export ZELLIJ_AI_ROLE="$label"
|
||||
export ZELLIJ_AI_MACHINE_NAME="$machine_name"
|
||||
export ZELLIJ_AI_SESSION="$session_name"
|
||||
export ZELLIJ_AI_BUS_DIR="$bus_dir"
|
||||
|
||||
cd "$workspace"
|
||||
|
||||
echo "Codex pane"
|
||||
echo "machine: $machine_name"
|
||||
echo "workspace: $workspace"
|
||||
echo "profile: $profile_dir"
|
||||
echo "bus: $bus_dir"
|
||||
if [[ -n "$repo_dir" ]]; then
|
||||
echo "helper: $repo_dir/scripts/ai-msg.sh"
|
||||
fi
|
||||
echo
|
||||
if [ ! -f /codex-home/auth.json ]; then
|
||||
echo "No auth.json found for $label."
|
||||
echo "Starting: codex login --device-auth"
|
||||
echo "Use a different ChatGPT/OpenAI account here than in the other pane."
|
||||
echo
|
||||
codex login --device-auth
|
||||
echo
|
||||
fi
|
||||
exec codex --no-alt-screen -C /workspace
|
||||
EOF
|
||||
|
||||
docker_args=(
|
||||
run
|
||||
--rm
|
||||
--init
|
||||
-it
|
||||
--name "$container_name"
|
||||
--hostname "$container_name"
|
||||
--user "${uid}:${gid}"
|
||||
--workdir /workspace
|
||||
--entrypoint bash
|
||||
-e HOME=/codex-home
|
||||
-e CODEX_HOME=/codex-home
|
||||
-e USER="${USER:-$(id -un)}"
|
||||
-e TERM="$term_value"
|
||||
-e COLORTERM="$color_term_value"
|
||||
-e LANG="${LANG:-C.UTF-8}"
|
||||
-v "${workspace}:/workspace"
|
||||
-v "${profile_dir}:/codex-home"
|
||||
-v "${codex_binary}:/usr/local/bin/codex:ro"
|
||||
-v "${rg_binary}:/usr/local/bin/rg:ro"
|
||||
)
|
||||
|
||||
if [[ -f "$HOME/.gitconfig" ]]; then
|
||||
docker_args+=(-v "$HOME/.gitconfig:/codex-home/.gitconfig:ro")
|
||||
if [[ ! -f "$CODEX_HOME/auth.json" ]]; then
|
||||
echo "No auth.json found for $label."
|
||||
echo "Starting: codex login --device-auth"
|
||||
echo
|
||||
codex login --device-auth
|
||||
echo
|
||||
fi
|
||||
|
||||
docker_args+=("$image" -lc "$inner_script")
|
||||
|
||||
exec docker "${docker_args[@]}"
|
||||
exec codex --no-alt-screen -C "$workspace"
|
||||
|
||||
Executable
+185
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
zellij_ai_default_session() {
|
||||
printf '%s\n' "constant"
|
||||
}
|
||||
|
||||
zellij_ai_default_local_session() {
|
||||
printf '%s\n' "constant-fleet"
|
||||
}
|
||||
|
||||
zellij_ai_default_repo_dir() {
|
||||
printf '%s\n' '$HOME/constant'
|
||||
}
|
||||
|
||||
zellij_ai_default_codex_home() {
|
||||
printf '%s\n' '$HOME/.codex-profiles/codex'
|
||||
}
|
||||
|
||||
zellij_ai_default_codex_image() {
|
||||
printf '%s\n' "codercom/code-server:latest"
|
||||
}
|
||||
|
||||
zellij_ai_runtime_root() {
|
||||
printf '%s\n' "${ZELLIJ_AI_RUNTIME_ROOT:-$HOME/.cache/constant/zellij}"
|
||||
}
|
||||
|
||||
zellij_ai_session_state_dir() {
|
||||
printf '%s\n' "$(zellij_ai_runtime_root)/$1"
|
||||
}
|
||||
|
||||
zellij_ai_fleet_state_dir() {
|
||||
printf '%s\n' "$(zellij_ai_runtime_root)/$1-fleet"
|
||||
}
|
||||
|
||||
zellij_ai_bridge_cache_dir() {
|
||||
printf '%s\n' "$(zellij_ai_runtime_root)/bridge-cache"
|
||||
}
|
||||
|
||||
zellij_ai_default_machine_specs() {
|
||||
cat <<'EOF'
|
||||
command-center=local
|
||||
builder-a=dev@builder-a
|
||||
builder-b=dev@builder-b
|
||||
edge-a=dev@edge-a
|
||||
lab-a=dev@lab-a
|
||||
EOF
|
||||
}
|
||||
|
||||
zellij_ai_strip_ansi() {
|
||||
sed -E 's/\x1b\[[0-9;]*m//g'
|
||||
}
|
||||
|
||||
zellij_ai_session_exists() {
|
||||
zellij list-sessions 2>/dev/null | zellij_ai_strip_ansi | awk '{print $1}' | grep -Fxq "$1"
|
||||
}
|
||||
|
||||
zellij_ai_kdl_escape() {
|
||||
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
|
||||
}
|
||||
|
||||
zellij_ai_expand_home_path() {
|
||||
case "$1" in
|
||||
'$HOME'|'$HOME/'*)
|
||||
printf '%s\n' "${HOME}${1#\$HOME}"
|
||||
;;
|
||||
"~")
|
||||
printf '%s\n' "$HOME"
|
||||
;;
|
||||
"~/"*)
|
||||
printf '%s\n' "$HOME/${1#~/}"
|
||||
;;
|
||||
*)
|
||||
printf '%s\n' "$1"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
zellij_ai_expand_home_path_for_home() {
|
||||
local value="$1"
|
||||
local target_home="$2"
|
||||
|
||||
case "$value" in
|
||||
'$HOME'|'$HOME/'*)
|
||||
printf '%s\n' "${target_home}${value#\$HOME}"
|
||||
;;
|
||||
"~")
|
||||
printf '%s\n' "$target_home"
|
||||
;;
|
||||
"~/"*)
|
||||
printf '%s\n' "$target_home/${value#~/}"
|
||||
;;
|
||||
*)
|
||||
printf '%s\n' "$value"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
zellij_ai_is_local_target() {
|
||||
local value="$1"
|
||||
local current_full current_short
|
||||
|
||||
current_full="$(hostname || true)"
|
||||
current_short="$(hostname -s 2>/dev/null || printf '%s' "$current_full")"
|
||||
|
||||
case "$value" in
|
||||
local|localhost|127.0.0.1|::1|"${current_full}"|"${current_short}")
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
zellij_ai_parse_machine_spec() {
|
||||
local spec="$1"
|
||||
local label target
|
||||
|
||||
if [[ "$spec" == *"="* ]]; then
|
||||
label="${spec%%=*}"
|
||||
target="${spec#*=}"
|
||||
else
|
||||
label="$spec"
|
||||
target="$spec"
|
||||
fi
|
||||
|
||||
if [[ -z "$label" || -z "$target" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf '%s\t%s\n' "$label" "$target"
|
||||
}
|
||||
|
||||
zellij_ai_resolve_machine_spec() {
|
||||
local needle="$1"
|
||||
local line label target
|
||||
|
||||
while IFS= read -r line; do
|
||||
[[ -z "$line" ]] && continue
|
||||
IFS=$'\t' read -r label target <<<"$(zellij_ai_parse_machine_spec "$line")" || continue
|
||||
if [[ "$needle" == "$label" || "$needle" == "$target" || "$needle" == "$line" ]]; then
|
||||
printf '%s\t%s\n' "$label" "$target"
|
||||
return 0
|
||||
fi
|
||||
done < <(zellij_ai_default_machine_specs)
|
||||
|
||||
if IFS=$'\t' read -r label target <<<"$(zellij_ai_parse_machine_spec "$needle" 2>/dev/null)"; then
|
||||
printf '%s\t%s\n' "$label" "$target"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
zellij_ai_require_command() {
|
||||
if ! command -v "$1" >/dev/null 2>&1; then
|
||||
echo "Required command not found: $1" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
zellij_ai_current_machine_name() {
|
||||
if [[ -n "${ZELLIJ_AI_MACHINE_NAME:-}" ]]; then
|
||||
printf '%s\n' "${ZELLIJ_AI_MACHINE_NAME}"
|
||||
else
|
||||
hostname -s 2>/dev/null || hostname || printf 'local'
|
||||
fi
|
||||
}
|
||||
|
||||
zellij_ai_local_machine_label() {
|
||||
printf '%s\n' "command-center"
|
||||
}
|
||||
|
||||
zellij_ai_agent_path() {
|
||||
local path_value="$PATH"
|
||||
|
||||
if [[ -d "$HOME/.local/bin" ]]; then
|
||||
path_value="$HOME/.local/bin:$path_value"
|
||||
fi
|
||||
if [[ -d "$HOME/.npm-global/bin" ]]; then
|
||||
path_value="$HOME/.npm-global/bin:$path_value"
|
||||
fi
|
||||
|
||||
printf '%s\n' "$path_value"
|
||||
}
|
||||
Executable
+65
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
script_source="${BASH_SOURCE[0]:-$0}"
|
||||
while [[ -L "$script_source" ]]; do
|
||||
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
|
||||
script_source="$(readlink "$script_source")"
|
||||
[[ "$script_source" != /* ]] && script_source="$script_dir/$script_source"
|
||||
done
|
||||
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
|
||||
source "$script_dir/zellij-ai-common.sh"
|
||||
|
||||
workspace="${ZELLIJ_AI_WORKSPACE:-$PWD}"
|
||||
machine_name="${ZELLIJ_AI_MACHINE_NAME:-unknown}"
|
||||
session_name="${ZELLIJ_AI_SESSION:-$(zellij_ai_default_session)}"
|
||||
repo_dir="${ZELLIJ_AI_REPO_DIR:-$(cd "$script_dir/.." && pwd -P)}"
|
||||
bus_dir="${ZELLIJ_AI_BUS_DIR:-$(zellij_ai_session_state_dir "$session_name")/bus}"
|
||||
label="${ZELLIJ_AI_COPILOT_LABEL:-copilot}"
|
||||
|
||||
if [[ ! -d "$workspace" ]]; then
|
||||
echo "Workspace not found: $workspace" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export PATH="$(zellij_ai_agent_path)"
|
||||
|
||||
if [[ -n "$repo_dir" && -d "$repo_dir/scripts" ]]; then
|
||||
export PATH="$repo_dir/scripts:$PATH"
|
||||
fi
|
||||
|
||||
if ! command -v copilot >/dev/null 2>&1; then
|
||||
echo "GitHub Copilot CLI not found on the host." >&2
|
||||
echo "Install it with: npm install -g @github/copilot" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$bus_dir/messages"
|
||||
|
||||
export ZELLIJ_AI_ROLE="$label"
|
||||
export ZELLIJ_AI_MACHINE_NAME="$machine_name"
|
||||
export ZELLIJ_AI_SESSION="$session_name"
|
||||
export ZELLIJ_AI_BUS_DIR="$bus_dir"
|
||||
|
||||
cd "$workspace"
|
||||
|
||||
echo "Copilot pane"
|
||||
echo "machine: $machine_name"
|
||||
echo "workspace: $workspace"
|
||||
echo "bus: $bus_dir"
|
||||
if [[ -n "$repo_dir" ]]; then
|
||||
echo "helper: $repo_dir/scripts/ai-msg.sh"
|
||||
fi
|
||||
echo "login: start copilot and use /login if needed"
|
||||
echo
|
||||
|
||||
copilot
|
||||
status=$?
|
||||
|
||||
if [[ $status -ne 0 ]]; then
|
||||
echo
|
||||
echo "Copilot exited with status $status."
|
||||
echo "If macOS reports 'SecItemCopyMatching failed -50', open copilot manually once and re-run /login."
|
||||
fi
|
||||
|
||||
exit "$status"
|
||||
Executable
+42
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
tabs_file="${ZELLIJ_AI_FLEET_TABS_FILE:-}"
|
||||
tabs_created_file="${ZELLIJ_AI_FLEET_CREATED_FILE:-}"
|
||||
current_label="${ZELLIJ_AI_FLEET_CURRENT_LABEL:-}"
|
||||
current_wrapper="${ZELLIJ_AI_FLEET_CURRENT_WRAPPER:-}"
|
||||
|
||||
if [[ -z "$current_wrapper" || ! -x "$current_wrapper" ]]; then
|
||||
echo "Fleet bootstrap wrapper not found: $current_wrapper" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "$current_label" ]]; then
|
||||
zellij action rename-tab "$current_label"
|
||||
fi
|
||||
|
||||
if [[ -n "$tabs_created_file" ]]; then
|
||||
mkdir -p "$(dirname "$tabs_created_file")"
|
||||
fi
|
||||
|
||||
if [[ -n "$tabs_file" && -f "$tabs_file" && -n "$tabs_created_file" && ! -f "$tabs_created_file" ]]; then
|
||||
: >"$tabs_created_file"
|
||||
|
||||
while IFS=$'\t' read -r tab_label layout_file tab_cwd; do
|
||||
if [[ -z "$tab_label" || -z "$layout_file" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ -n "$tab_cwd" ]]; then
|
||||
zellij action new-tab -n "$tab_label" -l "$layout_file" --cwd "$tab_cwd"
|
||||
else
|
||||
zellij action new-tab -n "$tab_label" -l "$layout_file"
|
||||
fi
|
||||
done <"$tabs_file"
|
||||
|
||||
if [[ -n "$current_label" ]]; then
|
||||
zellij action go-to-tab-name "$current_label"
|
||||
fi
|
||||
fi
|
||||
|
||||
exec "$current_wrapper"
|
||||
Executable
+196
@@ -0,0 +1,196 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
script_source="${BASH_SOURCE[0]:-$0}"
|
||||
while [[ -L "$script_source" ]]; do
|
||||
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
|
||||
script_source="$(readlink "$script_source")"
|
||||
[[ "$script_source" != /* ]] && script_source="$script_dir/$script_source"
|
||||
done
|
||||
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
|
||||
repo_source="$(cd "$script_dir/.." && pwd -P)"
|
||||
source "$script_dir/zellij-ai-common.sh"
|
||||
|
||||
usage() {
|
||||
local script_name="${CONSTANT_SCRIPT_NAME:-$(basename "$0")}"
|
||||
cat <<EOF
|
||||
Usage: ${script_name} <check|install> [options]
|
||||
|
||||
Check or install the Constant fleet runtime on one or more machines.
|
||||
|
||||
Options:
|
||||
--machine SPEC Machine definition or label to target
|
||||
--repo-dir DIR Repository path on each machine
|
||||
default: $(zellij_ai_default_repo_dir)
|
||||
--codex-image IMAGE Deprecated, ignored
|
||||
--yes Required for non-interactive install mode
|
||||
-h, --help Show this help
|
||||
EOF
|
||||
}
|
||||
|
||||
warn_deprecated() {
|
||||
printf '[fleet-install] %s\n' "$*" >&2
|
||||
}
|
||||
|
||||
require_local_tools() {
|
||||
local tool
|
||||
for tool in bash ssh tar rsync; do
|
||||
zellij_ai_require_command "$tool"
|
||||
done
|
||||
}
|
||||
|
||||
remote_home() {
|
||||
ssh "$1" 'printf "%s\n" "$HOME"'
|
||||
}
|
||||
|
||||
remote_repo_dir() {
|
||||
local target="$1"
|
||||
local repo_dir_input="$2"
|
||||
local target_home
|
||||
|
||||
if zellij_ai_is_local_target "$target"; then
|
||||
zellij_ai_expand_home_path "$repo_dir_input"
|
||||
return 0
|
||||
fi
|
||||
|
||||
target_home="$(remote_home "$target")"
|
||||
zellij_ai_expand_home_path_for_home "$repo_dir_input" "$target_home"
|
||||
}
|
||||
|
||||
sync_repo_to_remote() {
|
||||
local target="$1"
|
||||
local repo_dir_input="$2"
|
||||
local target_repo_dir local_real target_real
|
||||
|
||||
target_repo_dir="$(remote_repo_dir "$target" "$repo_dir_input")"
|
||||
|
||||
if zellij_ai_is_local_target "$target"; then
|
||||
mkdir -p "$target_repo_dir"
|
||||
local_real="$repo_source"
|
||||
target_real="$(cd "$target_repo_dir" && pwd -P)"
|
||||
if [[ "$local_real" == "$target_real" ]]; then
|
||||
return 0
|
||||
fi
|
||||
rsync -a --delete --exclude '.git/' "$repo_source/" "$target_repo_dir/"
|
||||
return 0
|
||||
fi
|
||||
|
||||
ssh "$target" "mkdir -p $(printf '%q' "$target_repo_dir")"
|
||||
|
||||
if ssh "$target" 'command -v rsync >/dev/null 2>&1'; then
|
||||
rsync -a --delete --exclude '.git/' "$repo_source/" "${target}:${target_repo_dir}/"
|
||||
else
|
||||
(
|
||||
cd "$repo_source"
|
||||
tar --exclude '.git' -cf - .
|
||||
) | ssh "$target" "tar -xf - -C $(printf '%q' "$target_repo_dir")"
|
||||
fi
|
||||
}
|
||||
|
||||
mode="${1:-}"
|
||||
if [[ -z "$mode" ]]; then
|
||||
usage >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
case "$mode" in
|
||||
-h|--help|help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
shift || true
|
||||
|
||||
machines=()
|
||||
repo_dir="$(zellij_ai_default_repo_dir)"
|
||||
yes=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--machine)
|
||||
machines+=("$2")
|
||||
shift 2
|
||||
;;
|
||||
--repo-dir)
|
||||
repo_dir="$2"
|
||||
shift 2
|
||||
;;
|
||||
--codex-image)
|
||||
warn_deprecated "--codex-image is deprecated and ignored."
|
||||
shift 2
|
||||
;;
|
||||
--yes)
|
||||
yes=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1" >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
case "$mode" in
|
||||
check|install)
|
||||
;;
|
||||
*)
|
||||
echo "Unknown mode: $mode" >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
require_local_tools
|
||||
|
||||
if [[ ${#machines[@]} -eq 0 ]]; then
|
||||
mapfile -t machines < <(zellij_ai_default_machine_specs)
|
||||
fi
|
||||
|
||||
overall_status=0
|
||||
|
||||
for machine_spec in "${machines[@]}"; do
|
||||
if ! resolved="$(zellij_ai_resolve_machine_spec "$machine_spec")"; then
|
||||
echo "Unknown machine: $machine_spec" >&2
|
||||
overall_status=1
|
||||
continue
|
||||
fi
|
||||
|
||||
IFS=$'\t' read -r label target <<<"$resolved"
|
||||
printf '=== %s (%s) ===\n' "$label" "$target"
|
||||
|
||||
target_repo_dir="$(remote_repo_dir "$target" "$repo_dir")"
|
||||
|
||||
if [[ "$mode" == "install" ]]; then
|
||||
sync_repo_to_remote "$target" "$repo_dir" || overall_status=1
|
||||
fi
|
||||
|
||||
install_args=("$mode" --repo-dir "$repo_dir")
|
||||
if $yes; then
|
||||
install_args+=(--yes)
|
||||
fi
|
||||
|
||||
if zellij_ai_is_local_target "$target"; then
|
||||
ZELLIJ_AI_MACHINE_NAME="$label" "$script_dir/zellij-ai-machine-install.sh" "${install_args[@]}" || overall_status=1
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ "$mode" == "install" ]]; then
|
||||
remote_script="${target_repo_dir}/scripts/zellij-ai-machine-install.sh"
|
||||
ssh -tt "$target" env ZELLIJ_AI_MACHINE_NAME="$label" "$remote_script" "${install_args[@]}" || overall_status=1
|
||||
continue
|
||||
fi
|
||||
|
||||
if $yes; then
|
||||
ssh "$target" env ZELLIJ_AI_MACHINE_NAME="$label" bash -s -- "${install_args[@]}" <"$script_dir/zellij-ai-machine-install.sh" || overall_status=1
|
||||
else
|
||||
ssh "$target" env ZELLIJ_AI_MACHINE_NAME="$label" bash -s -- "${install_args[@]}" <"$script_dir/zellij-ai-machine-install.sh" || overall_status=1
|
||||
fi
|
||||
done
|
||||
|
||||
exit "$overall_status"
|
||||
Executable
+250
@@ -0,0 +1,250 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
script_source="${BASH_SOURCE[0]:-$0}"
|
||||
while [[ -L "$script_source" ]]; do
|
||||
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
|
||||
script_source="$(readlink "$script_source")"
|
||||
[[ "$script_source" != /* ]] && script_source="$script_dir/$script_source"
|
||||
done
|
||||
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
|
||||
repo_dir="$(cd "$script_dir/.." && pwd -P)"
|
||||
source "$script_dir/zellij-ai-common.sh"
|
||||
|
||||
warn_deprecated() {
|
||||
printf 'Warning: %s\n' "$*" >&2
|
||||
}
|
||||
|
||||
usage() {
|
||||
local script_name="${CONSTANT_SCRIPT_NAME:-$(basename "$0")}"
|
||||
cat <<EOF
|
||||
Usage: ${script_name} [options]
|
||||
|
||||
Open a local Zellij cockpit with one tab per machine.
|
||||
Each tab attaches to the machine's own local Constant machine session.
|
||||
|
||||
Options:
|
||||
--machine SPEC Machine definition. Repeat once per machine.
|
||||
format: [TAB_NAME=]TARGET
|
||||
default fleet: command-center, builder-a, builder-b, edge-a, lab-a
|
||||
--session NAME Remote/local machine session name
|
||||
default: $(zellij_ai_default_session)
|
||||
--local-session NAME Cockpit session name on this machine
|
||||
default: $(zellij_ai_default_local_session)
|
||||
--repo-dir DIR Repository path on each machine
|
||||
default: $(zellij_ai_default_repo_dir)
|
||||
--workspace DIR Workspace path passed to each machine session
|
||||
required for the fixed 5-machine fleet
|
||||
--codex-image IMAGE Deprecated, ignored
|
||||
--claude-config DIR Optional Claude config override passed to each machine
|
||||
--remote-recreate Recreate each machine session before attaching
|
||||
--recreate Recreate the local cockpit session before launching
|
||||
-h, --help Show this help
|
||||
EOF
|
||||
}
|
||||
|
||||
machines=()
|
||||
remote_session="$(zellij_ai_default_session)"
|
||||
local_session="$(zellij_ai_default_local_session)"
|
||||
remote_repo_dir="$(zellij_ai_default_repo_dir)"
|
||||
workspace=""
|
||||
codex_image=""
|
||||
claude_config_dir=""
|
||||
recreate=false
|
||||
remote_recreate=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--machine)
|
||||
machines+=("$2")
|
||||
shift 2
|
||||
;;
|
||||
--session)
|
||||
remote_session="$2"
|
||||
shift 2
|
||||
;;
|
||||
--local-session)
|
||||
local_session="$2"
|
||||
shift 2
|
||||
;;
|
||||
--repo-dir)
|
||||
remote_repo_dir="$2"
|
||||
shift 2
|
||||
;;
|
||||
--workspace)
|
||||
workspace="$2"
|
||||
shift 2
|
||||
;;
|
||||
--codex-image)
|
||||
warn_deprecated "--codex-image is deprecated and ignored; Codex now runs on the host."
|
||||
codex_image="$2"
|
||||
shift 2
|
||||
;;
|
||||
--claude-config)
|
||||
claude_config_dir="$2"
|
||||
shift 2
|
||||
;;
|
||||
--remote-recreate)
|
||||
remote_recreate=true
|
||||
shift
|
||||
;;
|
||||
--recreate)
|
||||
recreate=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1" >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ ${#machines[@]} -eq 0 ]]; then
|
||||
mapfile -t machines < <(zellij_ai_default_machine_specs)
|
||||
fi
|
||||
|
||||
if [[ -z "$workspace" ]]; then
|
||||
echo "--workspace is required for fleet mode." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
workspace="$(cd "$workspace" && pwd -P)"
|
||||
|
||||
for required in \
|
||||
zellij \
|
||||
ssh \
|
||||
"$script_dir/zellij-ai-common.sh" \
|
||||
"$script_dir/zellij-ai-fleet-bootstrap.sh" \
|
||||
"$script_dir/zellij-ai-remote-tab.sh"
|
||||
do
|
||||
if [[ "$required" == */* ]]; then
|
||||
if [[ ! -x "$required" ]]; then
|
||||
echo "Helper script not executable: $required" >&2
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
zellij_ai_require_command "$required"
|
||||
fi
|
||||
done
|
||||
|
||||
state_dir="$(zellij_ai_fleet_state_dir "$local_session")"
|
||||
tabs_file="$state_dir/tabs.tsv"
|
||||
tabs_created_file="$state_dir/tabs-created"
|
||||
first_layout_file="$state_dir/first-tab.kdl"
|
||||
mkdir -p "$state_dir"
|
||||
: >"$tabs_file"
|
||||
rm -f "$tabs_created_file"
|
||||
|
||||
bootstrap_path="$(zellij_ai_kdl_escape "$script_dir/zellij-ai-fleet-bootstrap.sh")"
|
||||
|
||||
first_label=""
|
||||
first_wrapper=""
|
||||
seen_labels='|'
|
||||
machine_index=0
|
||||
|
||||
for machine_spec in "${machines[@]}"; do
|
||||
if ! parsed_spec="$(zellij_ai_parse_machine_spec "$machine_spec")"; then
|
||||
echo "Invalid machine spec: $machine_spec" >&2
|
||||
exit 2
|
||||
fi
|
||||
IFS=$'\t' read -r machine_label machine_target <<<"$parsed_spec"
|
||||
|
||||
if [[ "$seen_labels" == *"|${machine_label}|"* ]]; then
|
||||
echo "Duplicate machine label: $machine_label" >&2
|
||||
exit 2
|
||||
fi
|
||||
seen_labels+="${machine_label}|"
|
||||
|
||||
wrapper_path="$state_dir/machine-${machine_index}.sh"
|
||||
layout_path="$state_dir/machine-${machine_index}.kdl"
|
||||
|
||||
wrapper_cmd=(
|
||||
"$script_dir/zellij-ai-remote-tab.sh"
|
||||
--label "$machine_label"
|
||||
--target "$machine_target"
|
||||
--session "$remote_session"
|
||||
--repo-dir "$remote_repo_dir"
|
||||
)
|
||||
|
||||
if [[ -n "$workspace" ]]; then
|
||||
wrapper_cmd+=(--workspace "$workspace")
|
||||
fi
|
||||
if [[ -n "$codex_image" ]]; then
|
||||
wrapper_cmd+=(--codex-image "$codex_image")
|
||||
fi
|
||||
if [[ -n "$claude_config_dir" ]]; then
|
||||
wrapper_cmd+=(--claude-config "$claude_config_dir")
|
||||
fi
|
||||
if $remote_recreate; then
|
||||
wrapper_cmd+=(--remote-recreate)
|
||||
fi
|
||||
|
||||
printf -v wrapper_exec '%q ' "${wrapper_cmd[@]}"
|
||||
wrapper_exec="${wrapper_exec% }"
|
||||
|
||||
cat >"$wrapper_path" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
exec $wrapper_exec
|
||||
EOF
|
||||
chmod +x "$wrapper_path"
|
||||
|
||||
wrapper_path_escaped="$(zellij_ai_kdl_escape "$wrapper_path")"
|
||||
cat >"$layout_path" <<EOF
|
||||
layout {
|
||||
pane size=1 borderless=true {
|
||||
plugin location="tab-bar"
|
||||
}
|
||||
pane command="$wrapper_path_escaped"
|
||||
pane size=2 borderless=true {
|
||||
plugin location="status-bar"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
if [[ $machine_index -eq 0 ]]; then
|
||||
first_label="$machine_label"
|
||||
first_wrapper="$wrapper_path"
|
||||
else
|
||||
printf '%s\t%s\t%s\n' "$machine_label" "$layout_path" "$repo_dir" >>"$tabs_file"
|
||||
fi
|
||||
|
||||
machine_index=$((machine_index + 1))
|
||||
done
|
||||
|
||||
if [[ -z "$first_label" || -z "$first_wrapper" ]]; then
|
||||
echo "No machine tabs were generated" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cat >"$first_layout_file" <<EOF
|
||||
layout {
|
||||
pane size=1 borderless=true {
|
||||
plugin location="tab-bar"
|
||||
}
|
||||
pane command="$bootstrap_path"
|
||||
pane size=2 borderless=true {
|
||||
plugin location="status-bar"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
export ZELLIJ_AI_FLEET_TABS_FILE="$tabs_file"
|
||||
export ZELLIJ_AI_FLEET_CREATED_FILE="$tabs_created_file"
|
||||
export ZELLIJ_AI_FLEET_CURRENT_LABEL="$first_label"
|
||||
export ZELLIJ_AI_FLEET_CURRENT_WRAPPER="$first_wrapper"
|
||||
|
||||
if $recreate && zellij_ai_session_exists "$local_session"; then
|
||||
zellij kill-session "$local_session" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
if zellij_ai_session_exists "$local_session"; then
|
||||
exec zellij attach "$local_session"
|
||||
fi
|
||||
|
||||
exec zellij --new-session-with-layout "$first_layout_file" --session "$local_session"
|
||||
Executable
+411
@@ -0,0 +1,411 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
script_source="${BASH_SOURCE[0]:-$0}"
|
||||
while [[ -L "$script_source" ]]; do
|
||||
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
|
||||
script_source="$(readlink "$script_source")"
|
||||
[[ "$script_source" != /* ]] && script_source="$script_dir/$script_source"
|
||||
done
|
||||
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
|
||||
if [[ -f "$script_dir/zellij-ai-common.sh" ]]; then
|
||||
source "$script_dir/zellij-ai-common.sh"
|
||||
else
|
||||
zellij_ai_default_repo_dir() {
|
||||
printf '%s\n' '$HOME/constant'
|
||||
}
|
||||
|
||||
zellij_ai_default_codex_home() {
|
||||
printf '%s\n' '$HOME/.codex-profiles/codex'
|
||||
}
|
||||
|
||||
zellij_ai_expand_home_path() {
|
||||
case "$1" in
|
||||
'$HOME'|'$HOME/'*)
|
||||
printf '%s\n' "${HOME}${1#\$HOME}"
|
||||
;;
|
||||
"~")
|
||||
printf '%s\n' "$HOME"
|
||||
;;
|
||||
"~/"*)
|
||||
printf '%s\n' "$HOME/${1#~/}"
|
||||
;;
|
||||
*)
|
||||
printf '%s\n' "$1"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
zellij_ai_current_machine_name() {
|
||||
if [[ -n "${ZELLIJ_AI_MACHINE_NAME:-}" ]]; then
|
||||
printf '%s\n' "${ZELLIJ_AI_MACHINE_NAME}"
|
||||
else
|
||||
hostname -s 2>/dev/null || hostname || printf 'local'
|
||||
fi
|
||||
}
|
||||
|
||||
zellij_ai_agent_path() {
|
||||
local path_value="$PATH"
|
||||
|
||||
if [[ -d "$HOME/.local/bin" ]]; then
|
||||
path_value="$HOME/.local/bin:$path_value"
|
||||
fi
|
||||
if [[ -d "$HOME/.npm-global/bin" ]]; then
|
||||
path_value="$HOME/.npm-global/bin:$path_value"
|
||||
fi
|
||||
|
||||
printf '%s\n' "$path_value"
|
||||
}
|
||||
fi
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") <check|install> [options]
|
||||
|
||||
Options:
|
||||
--repo-dir DIR Repository path on this machine
|
||||
default: $(zellij_ai_default_repo_dir)
|
||||
--codex-image IMAGE Deprecated, ignored
|
||||
--yes Required for non-interactive install mode
|
||||
-h, --help Show this help
|
||||
EOF
|
||||
}
|
||||
|
||||
log() {
|
||||
printf '[install] %s\n' "$*"
|
||||
}
|
||||
|
||||
fail() {
|
||||
printf '[install] %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
confirm_install() {
|
||||
if $yes; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ ! -t 0 ]]; then
|
||||
fail "Install mode requires --yes when stdin is not interactive."
|
||||
fi
|
||||
|
||||
printf 'Proceed with installation changes on this machine? [y/N] '
|
||||
read -r answer
|
||||
[[ "$answer" == "y" || "$answer" == "Y" ]]
|
||||
}
|
||||
|
||||
need_sudo() {
|
||||
[[ "${EUID:-$(id -u)}" -ne 0 ]]
|
||||
}
|
||||
|
||||
run_root() {
|
||||
if ! need_sudo; then
|
||||
"$@"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if command -v sudo >/dev/null 2>&1; then
|
||||
sudo "$@"
|
||||
return 0
|
||||
fi
|
||||
|
||||
fail "sudo is required to run: $*"
|
||||
}
|
||||
|
||||
ensure_command() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
warn_deprecated() {
|
||||
printf '[install] %s\n' "$*" >&2
|
||||
}
|
||||
|
||||
refresh_path() {
|
||||
export PATH="$(zellij_ai_agent_path)"
|
||||
|
||||
if [[ -x /opt/homebrew/bin/brew ]]; then
|
||||
eval "$(/opt/homebrew/bin/brew shellenv)"
|
||||
elif [[ -x /usr/local/bin/brew ]]; then
|
||||
eval "$(/usr/local/bin/brew shellenv)"
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_homebrew() {
|
||||
if command -v brew >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
ensure_command curl || fail "curl is required to install Homebrew."
|
||||
NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||
refresh_path
|
||||
ensure_command brew || fail "Homebrew installation did not make brew available."
|
||||
}
|
||||
|
||||
ensure_uv() {
|
||||
if command -v uv >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
case "$os_name" in
|
||||
Darwin)
|
||||
ensure_homebrew
|
||||
brew install uv
|
||||
;;
|
||||
Linux)
|
||||
ensure_command curl || fail "curl is required to install uv."
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
;;
|
||||
*)
|
||||
fail "Unsupported OS for uv installation: $os_name"
|
||||
;;
|
||||
esac
|
||||
|
||||
refresh_path
|
||||
ensure_command uv || fail "uv installation did not make uv available."
|
||||
}
|
||||
|
||||
ensure_npm_global() {
|
||||
local package_name="$1"
|
||||
local binary_name="$2"
|
||||
|
||||
if command -v "$binary_name" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
mkdir -p "$HOME/.npm-global"
|
||||
export PATH="$(zellij_ai_agent_path)"
|
||||
|
||||
if npm install -g --prefix "$HOME/.npm-global" "$package_name"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
fail "Failed to install ${package_name} with npm."
|
||||
}
|
||||
|
||||
ensure_uv_tool() {
|
||||
local package_name="$1"
|
||||
local binary_name="$2"
|
||||
|
||||
if command -v "$binary_name" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
ensure_uv
|
||||
uv tool install "$package_name"
|
||||
refresh_path
|
||||
ensure_command "$binary_name" || fail "Failed to install ${package_name} with uv."
|
||||
}
|
||||
|
||||
os_name="$(uname -s)"
|
||||
linux_id=""
|
||||
repo_dir="$(zellij_ai_expand_home_path "$(zellij_ai_default_repo_dir)")"
|
||||
codex_home="$(zellij_ai_expand_home_path "$(zellij_ai_default_codex_home)")"
|
||||
yes=false
|
||||
|
||||
refresh_path
|
||||
|
||||
if [[ -f /etc/os-release ]]; then
|
||||
# shellcheck disable=SC1091
|
||||
source /etc/os-release
|
||||
linux_id="${ID:-}"
|
||||
fi
|
||||
|
||||
if [[ $# -lt 1 ]]; then
|
||||
usage >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
mode="$1"
|
||||
|
||||
case "$mode" in
|
||||
-h|--help|help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
shift
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--repo-dir)
|
||||
repo_dir="$(zellij_ai_expand_home_path "$2")"
|
||||
shift 2
|
||||
;;
|
||||
--codex-image)
|
||||
warn_deprecated "--codex-image is deprecated and ignored."
|
||||
shift 2
|
||||
;;
|
||||
--yes)
|
||||
yes=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1" >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
print_status() {
|
||||
local key="$1"
|
||||
local value="$2"
|
||||
printf '%s=%s\n' "$key" "$value"
|
||||
}
|
||||
|
||||
run_check() {
|
||||
local ok=true
|
||||
local repo_state="missing"
|
||||
local codex_home_state="missing"
|
||||
local triple_help_state="missing"
|
||||
local binary
|
||||
|
||||
refresh_path
|
||||
|
||||
print_status machine "$(zellij_ai_current_machine_name)"
|
||||
print_status os "$os_name"
|
||||
[[ -n "$linux_id" ]] && print_status linux_id "$linux_id"
|
||||
print_status repo_dir "$repo_dir"
|
||||
print_status codex_home "$codex_home"
|
||||
|
||||
for binary in bash git zellij node npm uv claude codex copilot vibe; do
|
||||
if command -v "$binary" >/dev/null 2>&1; then
|
||||
print_status "$binary" "$(command -v "$binary")"
|
||||
else
|
||||
print_status "$binary" "missing"
|
||||
ok=false
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -x "$repo_dir/scripts/constant-machine.sh" || -x "$repo_dir/scripts/zellij-ai-triple.sh" ]]; then
|
||||
repo_state="present"
|
||||
else
|
||||
ok=false
|
||||
fi
|
||||
print_status repo "$repo_state"
|
||||
|
||||
if [[ -d "$codex_home" ]]; then
|
||||
codex_home_state="present"
|
||||
else
|
||||
ok=false
|
||||
fi
|
||||
print_status codex_profile "$codex_home_state"
|
||||
|
||||
if [[ -x "$repo_dir/scripts/constant-machine.sh" ]] && "$repo_dir/scripts/constant-machine.sh" --help >/dev/null 2>&1; then
|
||||
triple_help_state="ok"
|
||||
elif [[ -x "$repo_dir/scripts/zellij-ai-triple.sh" ]] && "$repo_dir/scripts/zellij-ai-triple.sh" --help >/dev/null 2>&1; then
|
||||
triple_help_state="ok"
|
||||
else
|
||||
ok=false
|
||||
fi
|
||||
print_status triple_help "$triple_help_state"
|
||||
|
||||
$ok
|
||||
}
|
||||
|
||||
install_darwin() {
|
||||
ensure_homebrew
|
||||
brew install git zellij node uv
|
||||
}
|
||||
|
||||
install_linux_ubuntu() {
|
||||
run_root apt-get update
|
||||
run_root apt-get install -y git curl
|
||||
|
||||
if ! command -v node >/dev/null 2>&1; then
|
||||
run_root apt-get install -y nodejs
|
||||
fi
|
||||
|
||||
if ! command -v npm >/dev/null 2>&1; then
|
||||
run_root apt-get install -y npm
|
||||
fi
|
||||
|
||||
if ! command -v zellij >/dev/null 2>&1; then
|
||||
if command -v snap >/dev/null 2>&1; then
|
||||
run_root snap install zellij --classic
|
||||
else
|
||||
run_root apt-get install -y zellij
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
install_linux_photon() {
|
||||
local packages=()
|
||||
|
||||
if ! command -v git >/dev/null 2>&1; then
|
||||
packages+=(git)
|
||||
fi
|
||||
if ! command -v curl >/dev/null 2>&1; then
|
||||
packages+=(curl)
|
||||
fi
|
||||
if ! command -v node >/dev/null 2>&1; then
|
||||
packages+=(nodejs)
|
||||
fi
|
||||
if ! command -v npm >/dev/null 2>&1; then
|
||||
packages+=(npm)
|
||||
fi
|
||||
|
||||
if [[ ${#packages[@]} -gt 0 ]]; then
|
||||
run_root tdnf install -y "${packages[@]}"
|
||||
fi
|
||||
|
||||
if ! command -v zellij >/dev/null 2>&1; then
|
||||
fail "zellij is missing on Photon OS and no native install path is scripted here."
|
||||
fi
|
||||
}
|
||||
|
||||
run_install() {
|
||||
confirm_install || fail "Installation cancelled."
|
||||
|
||||
mkdir -p "$repo_dir"
|
||||
|
||||
case "$os_name" in
|
||||
Darwin)
|
||||
install_darwin
|
||||
;;
|
||||
Linux)
|
||||
case "$linux_id" in
|
||||
ubuntu|debian)
|
||||
install_linux_ubuntu
|
||||
;;
|
||||
photon)
|
||||
install_linux_photon
|
||||
;;
|
||||
*)
|
||||
fail "Unsupported Linux distribution: ${linux_id:-unknown}"
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
*)
|
||||
fail "Unsupported OS: $os_name"
|
||||
;;
|
||||
esac
|
||||
|
||||
refresh_path
|
||||
mkdir -p "$codex_home"
|
||||
ensure_npm_global "@anthropic-ai/claude-code" claude
|
||||
ensure_npm_global "@openai/codex" codex
|
||||
ensure_npm_global "@github/copilot" copilot
|
||||
ensure_uv_tool "mistral-vibe" vibe
|
||||
|
||||
run_check
|
||||
}
|
||||
|
||||
case "$mode" in
|
||||
check)
|
||||
run_check
|
||||
;;
|
||||
install)
|
||||
run_install
|
||||
;;
|
||||
*)
|
||||
echo "Unknown mode: $mode" >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
Executable
+206
@@ -0,0 +1,206 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
script_source="${BASH_SOURCE[0]:-$0}"
|
||||
while [[ -L "$script_source" ]]; do
|
||||
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
|
||||
script_source="$(readlink "$script_source")"
|
||||
[[ "$script_source" != /* ]] && script_source="$script_dir/$script_source"
|
||||
done
|
||||
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
|
||||
source "$script_dir/zellij-ai-common.sh"
|
||||
|
||||
warn_deprecated() {
|
||||
printf 'Warning: %s\n' "$*" >&2
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: zellij-ai-remote-tab.sh [options]
|
||||
|
||||
Open or attach the local AI Zellij session for one machine.
|
||||
|
||||
Options:
|
||||
--label NAME Display label for logs and tab naming
|
||||
--target TARGET SSH target, or "local" for the current machine
|
||||
--session NAME Remote/local Zellij session name
|
||||
default: constant
|
||||
--repo-dir DIR Repository path on the target machine
|
||||
default: $HOME/constant
|
||||
--workspace DIR Workspace path passed to constant-machine.sh
|
||||
--codex-image IMAGE Deprecated, ignored
|
||||
--claude-config DIR Optional Claude config override
|
||||
--remote-recreate Recreate the target machine session before attaching
|
||||
-h, --help Show this help
|
||||
EOF
|
||||
}
|
||||
|
||||
label=""
|
||||
target=""
|
||||
session="$(zellij_ai_default_session)"
|
||||
repo_dir="$(zellij_ai_default_repo_dir)"
|
||||
workspace=""
|
||||
codex_image=""
|
||||
claude_config_dir=""
|
||||
remote_recreate=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--label)
|
||||
label="$2"
|
||||
shift 2
|
||||
;;
|
||||
--target)
|
||||
target="$2"
|
||||
shift 2
|
||||
;;
|
||||
--session)
|
||||
session="$2"
|
||||
shift 2
|
||||
;;
|
||||
--repo-dir)
|
||||
repo_dir="$2"
|
||||
shift 2
|
||||
;;
|
||||
--workspace)
|
||||
workspace="$2"
|
||||
shift 2
|
||||
;;
|
||||
--codex-image)
|
||||
warn_deprecated "--codex-image is deprecated and ignored; Codex now runs on the host."
|
||||
codex_image="$2"
|
||||
shift 2
|
||||
;;
|
||||
--claude-config)
|
||||
claude_config_dir="$2"
|
||||
shift 2
|
||||
;;
|
||||
--remote-recreate)
|
||||
remote_recreate=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1" >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$target" ]]; then
|
||||
echo "--target is required" >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [[ -z "$label" ]]; then
|
||||
label="$target"
|
||||
fi
|
||||
|
||||
if zellij_ai_is_local_target "$target"; then
|
||||
local_repo_dir="$(zellij_ai_expand_home_path "$repo_dir")"
|
||||
local_workspace="$(zellij_ai_expand_home_path "$workspace")"
|
||||
local_claude_config_dir="$(zellij_ai_expand_home_path "$claude_config_dir")"
|
||||
local_launcher="$local_repo_dir/scripts/constant-machine.sh"
|
||||
if [[ ! -x "$local_launcher" ]]; then
|
||||
local_launcher="$local_repo_dir/scripts/zellij-ai-triple.sh"
|
||||
fi
|
||||
|
||||
if [[ ! -x "$local_launcher" ]]; then
|
||||
echo "Local launcher not found for $label: $local_launcher" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cmd=("$local_launcher" --session "$session")
|
||||
|
||||
if [[ -n "$workspace" ]]; then
|
||||
cmd+=(--workspace "$local_workspace")
|
||||
fi
|
||||
if [[ -n "$codex_image" ]]; then
|
||||
cmd+=(--codex-image "$codex_image")
|
||||
fi
|
||||
if [[ -n "$claude_config_dir" ]]; then
|
||||
cmd+=(--claude-config "$local_claude_config_dir")
|
||||
fi
|
||||
if $remote_recreate; then
|
||||
cmd+=(--recreate)
|
||||
fi
|
||||
|
||||
exec env ZELLIJ_AI_MACHINE_NAME="$label" "${cmd[@]}"
|
||||
fi
|
||||
|
||||
printf -v session_q '%q' "$session"
|
||||
printf -v repo_dir_q '%q' "$repo_dir"
|
||||
printf -v workspace_q '%q' "$workspace"
|
||||
printf -v codex_image_q '%q' "$codex_image"
|
||||
printf -v claude_config_dir_q '%q' "$claude_config_dir"
|
||||
printf -v label_q '%q' "$label"
|
||||
remote_recreate_literal=false
|
||||
if $remote_recreate; then
|
||||
remote_recreate_literal=true
|
||||
fi
|
||||
|
||||
read -r -d '' remote_shell <<EOF || true
|
||||
set -euo pipefail
|
||||
|
||||
expand_home_path() {
|
||||
case "\$1" in
|
||||
'\$HOME'|'\$HOME/'*)
|
||||
printf '%s\n' "\${HOME}\${1#\\\$HOME}"
|
||||
;;
|
||||
"~")
|
||||
printf '%s\n' "\$HOME"
|
||||
;;
|
||||
"~/"*)
|
||||
printf '%s\n' "\$HOME/\${1#~/}"
|
||||
;;
|
||||
*)
|
||||
printf '%s\n' "\$1"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
session=$session_q
|
||||
repo_dir_input=$repo_dir_q
|
||||
workspace_input=$workspace_q
|
||||
codex_image_input=$codex_image_q
|
||||
claude_config_dir_input=$claude_config_dir_q
|
||||
remote_recreate=$remote_recreate_literal
|
||||
|
||||
repo_dir="\$(expand_home_path "\$repo_dir_input")"
|
||||
workspace="\$(expand_home_path "\$workspace_input")"
|
||||
claude_config_dir="\$(expand_home_path "\$claude_config_dir_input")"
|
||||
launcher="\$repo_dir/scripts/constant-machine.sh"
|
||||
if [[ ! -x "\$launcher" ]]; then
|
||||
launcher="\$repo_dir/scripts/zellij-ai-triple.sh"
|
||||
fi
|
||||
if [[ ! -x "\$launcher" ]]; then
|
||||
echo "Remote launcher not found: \$launcher" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cmd=("\$launcher" --session "\$session")
|
||||
|
||||
if [[ -n "\$workspace_input" ]]; then
|
||||
cmd+=(--workspace "\$workspace")
|
||||
fi
|
||||
if [[ -n "\$codex_image_input" ]]; then
|
||||
cmd+=(--codex-image "\$codex_image_input")
|
||||
fi
|
||||
if [[ -n "\$claude_config_dir_input" ]]; then
|
||||
cmd+=(--claude-config "\$claude_config_dir")
|
||||
fi
|
||||
if \$remote_recreate; then
|
||||
cmd+=(--recreate)
|
||||
fi
|
||||
|
||||
export ZELLIJ_AI_MACHINE_NAME=$label_q
|
||||
export ZELLIJ_AI_FORCE_OSC52=true
|
||||
exec "\${cmd[@]}"
|
||||
EOF
|
||||
|
||||
exec ssh -tt "$target" "bash -lc $(printf '%q' "$remote_shell")"
|
||||
+110
-60
@@ -1,61 +1,64 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
|
||||
script_source="${BASH_SOURCE[0]:-$0}"
|
||||
while [[ -L "$script_source" ]]; do
|
||||
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
|
||||
script_source="$(readlink "$script_source")"
|
||||
[[ "$script_source" != /* ]] && script_source="$script_dir/$script_source"
|
||||
done
|
||||
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
|
||||
repo_dir="$(cd "$script_dir/.." && pwd -P)"
|
||||
source "$script_dir/zellij-ai-common.sh"
|
||||
|
||||
usage() {
|
||||
local script_name="${CONSTANT_SCRIPT_NAME:-$(basename "$0")}"
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [options]
|
||||
Usage: ${script_name} [options]
|
||||
|
||||
Start a 3-pane Zellij session:
|
||||
Start a 4-pane Constant machine session:
|
||||
- left pane: Claude on the host
|
||||
- top-right pane: Codex in Docker with profile 1
|
||||
- bottom-right pane: Codex in Docker with profile 2
|
||||
- top-right pane: Codex on the host
|
||||
- middle-right pane: Copilot CLI on the host
|
||||
- bottom-right pane: Mistral Vibe on the host
|
||||
|
||||
Options:
|
||||
--workspace DIR Workspace mounted in both Codex containers and used by Claude
|
||||
--workspace DIR Workspace used by all panes
|
||||
default: current directory
|
||||
--session NAME Zellij session name
|
||||
default: ai-triple
|
||||
--codex-image IMAGE Docker image used for Codex panes
|
||||
default: codercom/code-server:latest
|
||||
--codex1-home DIR Persistent CODEX_HOME for the first Codex pane
|
||||
default: \$HOME/.codex-profiles/codex-1
|
||||
--codex2-home DIR Persistent CODEX_HOME for the second Codex pane
|
||||
default: \$HOME/.codex-profiles/codex-2
|
||||
--codex1-label LABEL Display label for the first Codex pane
|
||||
default: codex-1
|
||||
--codex2-label LABEL Display label for the second Codex pane
|
||||
default: codex-2
|
||||
--session NAME Session name
|
||||
default: $(zellij_ai_default_session)
|
||||
--codex-home DIR Persistent CODEX_HOME for the Codex pane
|
||||
default: $(zellij_ai_default_codex_home)
|
||||
--codex-label LABEL Display label for the Codex pane
|
||||
default: codex
|
||||
--copilot-label LABEL Display label for the Copilot pane
|
||||
default: copilot
|
||||
--vibe-label LABEL Display label for the Vibe pane
|
||||
default: vibe
|
||||
--claude-config DIR Optional CLAUDE_CONFIG_DIR override for the Claude pane
|
||||
--zellij-config-dir DIR
|
||||
Optional isolated Zellij config dir to use for session creation
|
||||
Optional isolated Zellij config dir for session creation
|
||||
default: a fresh temporary config dir per new session
|
||||
--recreate Kill the existing session before recreating it
|
||||
--codex-image IMAGE Deprecated, ignored
|
||||
--codex1-home DIR Deprecated alias for --codex-home
|
||||
--codex2-home DIR Deprecated, ignored
|
||||
--codex1-label LABEL Deprecated alias for --codex-label
|
||||
--codex2-label LABEL Deprecated, ignored
|
||||
--recreate Recreate the session and purge stale Zellij state first
|
||||
-h, --help Show this help
|
||||
EOF
|
||||
}
|
||||
|
||||
strip_ansi() {
|
||||
sed -E 's/\x1b\[[0-9;]*m//g'
|
||||
}
|
||||
|
||||
session_exists() {
|
||||
zellij list-sessions 2>/dev/null | strip_ansi | awk '{print $1}' | grep -Fxq "$1"
|
||||
}
|
||||
|
||||
kdl_escape() {
|
||||
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
|
||||
warn_deprecated() {
|
||||
printf 'Warning: %s\n' "$*" >&2
|
||||
}
|
||||
|
||||
workspace="$PWD"
|
||||
session="ai-triple"
|
||||
codex_image="codercom/code-server:latest"
|
||||
codex1_home="$HOME/.codex-profiles/codex-1"
|
||||
codex2_home="$HOME/.codex-profiles/codex-2"
|
||||
codex1_label="codex-1"
|
||||
codex2_label="codex-2"
|
||||
session="$(zellij_ai_default_session)"
|
||||
codex_home="$(zellij_ai_default_codex_home)"
|
||||
codex_label="codex"
|
||||
copilot_label="copilot"
|
||||
vibe_label="vibe"
|
||||
claude_config_dir=""
|
||||
zellij_config_dir=""
|
||||
recreate=false
|
||||
@@ -70,24 +73,42 @@ while [[ $# -gt 0 ]]; do
|
||||
session="$2"
|
||||
shift 2
|
||||
;;
|
||||
--codex-home)
|
||||
codex_home="$2"
|
||||
shift 2
|
||||
;;
|
||||
--codex-label)
|
||||
codex_label="$2"
|
||||
shift 2
|
||||
;;
|
||||
--copilot-label)
|
||||
copilot_label="$2"
|
||||
shift 2
|
||||
;;
|
||||
--vibe-label)
|
||||
vibe_label="$2"
|
||||
shift 2
|
||||
;;
|
||||
--codex-image)
|
||||
codex_image="$2"
|
||||
warn_deprecated "--codex-image is deprecated and ignored; Codex now runs on the host."
|
||||
shift 2
|
||||
;;
|
||||
--codex1-home)
|
||||
codex1_home="$2"
|
||||
warn_deprecated "--codex1-home is deprecated; use --codex-home."
|
||||
codex_home="$2"
|
||||
shift 2
|
||||
;;
|
||||
--codex2-home)
|
||||
codex2_home="$2"
|
||||
warn_deprecated "--codex2-home is deprecated and ignored."
|
||||
shift 2
|
||||
;;
|
||||
--codex1-label)
|
||||
codex1_label="$2"
|
||||
warn_deprecated "--codex1-label is deprecated; use --codex-label."
|
||||
codex_label="$2"
|
||||
shift 2
|
||||
;;
|
||||
--codex2-label)
|
||||
codex2_label="$2"
|
||||
warn_deprecated "--codex2-label is deprecated and ignored."
|
||||
shift 2
|
||||
;;
|
||||
--claude-config)
|
||||
@@ -115,11 +136,14 @@ while [[ $# -gt 0 ]]; do
|
||||
done
|
||||
|
||||
workspace="$(cd "$workspace" && pwd -P)"
|
||||
codex1_home="$(mkdir -p "$codex1_home" && cd "$codex1_home" && pwd -P)"
|
||||
codex2_home="$(mkdir -p "$codex2_home" && cd "$codex2_home" && pwd -P)"
|
||||
codex_home="$(mkdir -p "$(zellij_ai_expand_home_path "$codex_home")" && cd "$(zellij_ai_expand_home_path "$codex_home")" && pwd -P)"
|
||||
|
||||
bootstrap_file="$HOME/.cache/zellij-ai/${session}.bootstrapped"
|
||||
layout_file="/tmp/zellij-ai-layout-${session}.kdl"
|
||||
machine_name="$(zellij_ai_current_machine_name)"
|
||||
state_dir="$(zellij_ai_session_state_dir "$session")"
|
||||
bus_dir="$state_dir/bus"
|
||||
bootstrap_file="$state_dir/bootstrapped"
|
||||
layout_file="$state_dir/layout.kdl"
|
||||
clipboard_config_file="$state_dir/zellij.config.kdl"
|
||||
|
||||
if [[ ! -d "$workspace" ]]; then
|
||||
echo "Workspace not found: $workspace" >&2
|
||||
@@ -129,7 +153,10 @@ fi
|
||||
for required in \
|
||||
"$script_dir/zellij-ai-claude-pane.sh" \
|
||||
"$script_dir/zellij-ai-codex-pane.sh" \
|
||||
"$script_dir/zellij-ai-bootstrap.sh"
|
||||
"$script_dir/zellij-ai-copilot-pane.sh" \
|
||||
"$script_dir/zellij-ai-vibe-pane.sh" \
|
||||
"$script_dir/zellij-ai-bootstrap.sh" \
|
||||
"$script_dir/zellij-ai-common.sh"
|
||||
do
|
||||
if [[ ! -x "$required" ]]; then
|
||||
echo "Helper script not executable: $required" >&2
|
||||
@@ -137,36 +164,58 @@ do
|
||||
fi
|
||||
done
|
||||
|
||||
mkdir -p "$bus_dir/messages"
|
||||
|
||||
if [[ "${ZELLIJ_AI_FORCE_OSC52:-false}" == "true" || "${ZELLIJ_AI_FORCE_OSC52:-0}" == "1" ]]; then
|
||||
cat >"$clipboard_config_file" <<'EOF'
|
||||
copy_clipboard "system"
|
||||
EOF
|
||||
elif [[ "$(uname -s)" == "Darwin" && -x /usr/bin/pbcopy ]]; then
|
||||
cat >"$clipboard_config_file" <<'EOF'
|
||||
copy_command "pbcopy"
|
||||
copy_clipboard "system"
|
||||
EOF
|
||||
else
|
||||
cat >"$clipboard_config_file" <<'EOF'
|
||||
copy_clipboard "system"
|
||||
EOF
|
||||
fi
|
||||
|
||||
export ZELLIJ_AI_WORKSPACE="$workspace"
|
||||
export ZELLIJ_AI_CODEX_IMAGE="$codex_image"
|
||||
export ZELLIJ_AI_CODEX1_HOME="$codex1_home"
|
||||
export ZELLIJ_AI_CODEX2_HOME="$codex2_home"
|
||||
export ZELLIJ_AI_CODEX1_LABEL="$codex1_label"
|
||||
export ZELLIJ_AI_CODEX2_LABEL="$codex2_label"
|
||||
export ZELLIJ_AI_CODEX_HOME="$codex_home"
|
||||
export ZELLIJ_AI_CODEX_LABEL="$codex_label"
|
||||
export ZELLIJ_AI_COPILOT_LABEL="$copilot_label"
|
||||
export ZELLIJ_AI_VIBE_LABEL="$vibe_label"
|
||||
export ZELLIJ_AI_MAIN_CODEX_CONFIG="$HOME/.codex/config.toml"
|
||||
export ZELLIJ_AI_BOOTSTRAP_FILE="$bootstrap_file"
|
||||
export ZELLIJ_AI_REPO_DIR="$repo_dir"
|
||||
export ZELLIJ_AI_SESSION="$session"
|
||||
export ZELLIJ_AI_STATE_DIR="$state_dir"
|
||||
export ZELLIJ_AI_BUS_DIR="$bus_dir"
|
||||
export ZELLIJ_AI_MACHINE_NAME="$machine_name"
|
||||
|
||||
if [[ -n "$claude_config_dir" ]]; then
|
||||
export ZELLIJ_AI_CLAUDE_CONFIG_DIR="$claude_config_dir"
|
||||
fi
|
||||
|
||||
if $recreate && session_exists "$session"; then
|
||||
zellij_attach_cmd=(zellij --config "$clipboard_config_file")
|
||||
|
||||
if $recreate && zellij_ai_session_exists "$session"; then
|
||||
zellij delete-session -f "$session" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
if session_exists "$session"; then
|
||||
exec zellij attach "$session"
|
||||
if zellij_ai_session_exists "$session"; then
|
||||
exec "${zellij_attach_cmd[@]}" attach "$session"
|
||||
fi
|
||||
|
||||
zellij delete-session "$session" >/dev/null 2>&1 || true
|
||||
|
||||
mkdir -p "$(dirname "$bootstrap_file")"
|
||||
mkdir -p "$state_dir"
|
||||
rm -f "$bootstrap_file"
|
||||
|
||||
bootstrap_path="$(kdl_escape "$script_dir/zellij-ai-bootstrap.sh")"
|
||||
bootstrap_path="$(zellij_ai_kdl_escape "$script_dir/zellij-ai-bootstrap.sh")"
|
||||
cat >"$layout_file" <<EOF
|
||||
layout {
|
||||
tab name="AI Triple" {
|
||||
tab name="Constant" {
|
||||
pane size=1 borderless=true {
|
||||
plugin location="tab-bar"
|
||||
}
|
||||
@@ -179,9 +228,10 @@ layout {
|
||||
EOF
|
||||
|
||||
if [[ -z "$zellij_config_dir" ]]; then
|
||||
zellij_config_dir="$(mktemp -d "${TMPDIR:-/tmp}/zellij-ai-triple-${session}.XXXXXX")"
|
||||
zellij_config_dir="$(mktemp -d "${TMPDIR:-/tmp}/constant-zellij-${session}.XXXXXX")"
|
||||
else
|
||||
zellij_config_dir="$(zellij_ai_expand_home_path "$zellij_config_dir")"
|
||||
mkdir -p "$zellij_config_dir"
|
||||
fi
|
||||
|
||||
exec zellij --config-dir "$zellij_config_dir" --new-session-with-layout "$layout_file" --session "$session"
|
||||
exec zellij --config "$clipboard_config_file" --config-dir "$zellij_config_dir" --new-session-with-layout "$layout_file" --session "$session"
|
||||
|
||||
Executable
+55
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
script_source="${BASH_SOURCE[0]:-$0}"
|
||||
while [[ -L "$script_source" ]]; do
|
||||
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
|
||||
script_source="$(readlink "$script_source")"
|
||||
[[ "$script_source" != /* ]] && script_source="$script_dir/$script_source"
|
||||
done
|
||||
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
|
||||
source "$script_dir/zellij-ai-common.sh"
|
||||
|
||||
workspace="${ZELLIJ_AI_WORKSPACE:-$PWD}"
|
||||
machine_name="${ZELLIJ_AI_MACHINE_NAME:-unknown}"
|
||||
session_name="${ZELLIJ_AI_SESSION:-$(zellij_ai_default_session)}"
|
||||
repo_dir="${ZELLIJ_AI_REPO_DIR:-$(cd "$script_dir/.." && pwd -P)}"
|
||||
bus_dir="${ZELLIJ_AI_BUS_DIR:-$(zellij_ai_session_state_dir "$session_name")/bus}"
|
||||
label="${ZELLIJ_AI_VIBE_LABEL:-vibe}"
|
||||
|
||||
if [[ ! -d "$workspace" ]]; then
|
||||
echo "Workspace not found: $workspace" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export PATH="$(zellij_ai_agent_path)"
|
||||
|
||||
if [[ -n "$repo_dir" && -d "$repo_dir/scripts" ]]; then
|
||||
export PATH="$repo_dir/scripts:$PATH"
|
||||
fi
|
||||
|
||||
if ! command -v vibe >/dev/null 2>&1; then
|
||||
echo "Mistral Vibe CLI not found on the host." >&2
|
||||
echo "Install it with: uv tool install mistral-vibe" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$bus_dir/messages"
|
||||
|
||||
export ZELLIJ_AI_ROLE="$label"
|
||||
export ZELLIJ_AI_MACHINE_NAME="$machine_name"
|
||||
export ZELLIJ_AI_SESSION="$session_name"
|
||||
export ZELLIJ_AI_BUS_DIR="$bus_dir"
|
||||
|
||||
cd "$workspace"
|
||||
|
||||
echo "Vibe pane"
|
||||
echo "machine: $machine_name"
|
||||
echo "workspace: $workspace"
|
||||
echo "bus: $bus_dir"
|
||||
if [[ -n "$repo_dir" ]]; then
|
||||
echo "helper: $repo_dir/scripts/ai-msg.sh"
|
||||
fi
|
||||
echo
|
||||
|
||||
exec vibe
|
||||
Reference in New Issue
Block a user