direxio-deployer 0.1.0
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/AGENTS.md +92 -0
- package/LICENSE +21 -0
- package/README.md +221 -0
- package/README_zh.md +218 -0
- package/SKILL.md +722 -0
- package/agents/README.md +25 -0
- package/agents/openai.yaml +12 -0
- package/bin/direxio-deployer.mjs +375 -0
- package/package.json +28 -0
- package/references/agent-targets.md +128 -0
- package/references/architecture.md +44 -0
- package/references/bug-history.md +78 -0
- package/references/deployment-lessons.md +218 -0
- package/references/deployment-optimization-audit.md +317 -0
- package/references/deployment-workflow.md +341 -0
- package/references/iam-policy.json +52 -0
- package/references/runtime-wiring.md +209 -0
- package/references/state-machine.md +46 -0
- package/references/token-refresh.md +81 -0
- package/references/tooling.md +106 -0
- package/references/troubleshooting.md +26 -0
- package/references/user-journey.md +75 -0
- package/references/verification-recovery.md +84 -0
- package/references/voip-turn-runbook.md +154 -0
- package/references/windows-deployment-notes.md +119 -0
- package/scripts/aws-credentials.sh +195 -0
- package/scripts/cloud-init/Caddyfile +48 -0
- package/scripts/cloud-init/docker-compose.yml +125 -0
- package/scripts/cloud-init/init-tokens.sh +238 -0
- package/scripts/cloud-init/user-data.yaml +40 -0
- package/scripts/destroy.ps1 +77 -0
- package/scripts/destroy.sh +589 -0
- package/scripts/lib/aws.sh +73 -0
- package/scripts/lib/domain.sh +175 -0
- package/scripts/lib/operation_report.sh +240 -0
- package/scripts/lib/ops.sh +230 -0
- package/scripts/lib/paths.sh +35 -0
- package/scripts/lib/state.sh +137 -0
- package/scripts/mcp-tools-list.mjs +95 -0
- package/scripts/orchestrate.ps1 +112 -0
- package/scripts/orchestrate.sh +1126 -0
- package/scripts/phases/s0_prereq_aws.sh +39 -0
- package/scripts/phases/s1_preflight.sh +72 -0
- package/scripts/phases/s2_domain.sh +103 -0
- package/scripts/phases/s3_provision.sh +421 -0
- package/scripts/phases/s4_bootstrap_stack.sh +38 -0
- package/scripts/phases/s5_init_tokens.sh +118 -0
- package/scripts/phases/s6_wire_local.sh +1435 -0
- package/scripts/phases/s7_verify_e2e.sh +136 -0
- package/scripts/pricing-estimate.sh +256 -0
- package/scripts/render/render-userdata.sh +86 -0
- package/scripts/reset-app-data.sh +40 -0
- package/scripts/update.sh +30 -0
- package/tests/aws_credentials_test.sh +139 -0
- package/tests/connect_daemon_runtime_check_test.sh +120 -0
- package/tests/default_paths_test.sh +58 -0
- package/tests/destroy_local_bridge_test.sh +154 -0
- package/tests/destroy_root_identity_test.sh +91 -0
- package/tests/destroy_route53_zone_test.sh +80 -0
- package/tests/domain_authoritative_dns_test.sh +49 -0
- package/tests/mcp_doctor_runtime_check_test.sh +86 -0
- package/tests/mcp_smoke_runtime_check_test.sh +121 -0
- package/tests/mcp_tools_runtime_check_test.sh +123 -0
- package/tests/npm_skill_distribution_test.sh +95 -0
- package/tests/operation_report_test.sh +258 -0
- package/tests/orchestrate_status_recovery_test.sh +91 -0
- package/tests/phase_timeout_test.sh +88 -0
- package/tests/pricing_estimate_test.sh +159 -0
- package/tests/render_userdata_remote_nodes_test.sh +40 -0
- package/tests/root_volume_tracking_test.sh +41 -0
- package/tests/route53_overwrite_guard_test.sh +86 -0
- package/tests/route53_zone_auto_create_test.sh +66 -0
- package/tests/runtime_summary_check_test.sh +203 -0
- package/tests/s6_wire_local_test.sh +405 -0
- package/tests/skill_structure_test.sh +298 -0
- package/tests/update_reset_ops_test.sh +230 -0
- package/tests/user_confirmation_gates_test.sh +152 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# lib/ops.sh - existing-node update/reset helpers.
|
|
3
|
+
|
|
4
|
+
ops_state_path() {
|
|
5
|
+
local explicit=${1:-}
|
|
6
|
+
if [ -n "$explicit" ]; then
|
|
7
|
+
printf '%s\n' "$explicit"
|
|
8
|
+
return 0
|
|
9
|
+
fi
|
|
10
|
+
printf '%s/state.json\n' "$(direxio_default_workdir)"
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
ops_require_state() {
|
|
14
|
+
local state=$1
|
|
15
|
+
[ -f "$state" ] || {
|
|
16
|
+
echo "state.json not found: $state" >&2
|
|
17
|
+
return 1
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
ops_state_get() {
|
|
22
|
+
local state=$1 path=$2
|
|
23
|
+
jq -r "$path // empty" "$state"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
ops_sh_quote() {
|
|
27
|
+
printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
ops_path_dirname() {
|
|
31
|
+
local path=$1
|
|
32
|
+
path=${path%/}
|
|
33
|
+
case "$path" in
|
|
34
|
+
*/*) printf '%s\n' "${path%/*}" ;;
|
|
35
|
+
*) printf '.\n' ;;
|
|
36
|
+
esac
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
ops_normalize_path() {
|
|
40
|
+
local path=$1
|
|
41
|
+
path=$(printf '%s' "$path" | sed 's#\\#/#g')
|
|
42
|
+
if command -v cygpath >/dev/null 2>&1; then
|
|
43
|
+
cygpath -m "$path" 2>/dev/null && return 0
|
|
44
|
+
fi
|
|
45
|
+
while [ "${#path}" -gt 1 ] && [ "${path%/}" != "$path" ]; do
|
|
46
|
+
case "$path" in [A-Za-z]:/) break ;; esac
|
|
47
|
+
path=${path%/}
|
|
48
|
+
done
|
|
49
|
+
printf '%s\n' "$path"
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
ops_paths_match() {
|
|
53
|
+
local left right
|
|
54
|
+
left=$(ops_normalize_path "$1")
|
|
55
|
+
right=$(ops_normalize_path "$2")
|
|
56
|
+
case "$left:$right" in
|
|
57
|
+
[A-Za-z]:/*:[A-Za-z]:/*)
|
|
58
|
+
[ "$(printf '%s' "$left" | tr '[:upper:]' '[:lower:]')" = "$(printf '%s' "$right" | tr '[:upper:]' '[:lower:]')" ]
|
|
59
|
+
;;
|
|
60
|
+
*)
|
|
61
|
+
[ "$left" = "$right" ]
|
|
62
|
+
;;
|
|
63
|
+
esac
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
ops_remote_base() {
|
|
67
|
+
local state=$1 keyfile pubip
|
|
68
|
+
keyfile=$(ops_state_get "$state" '.resources.key_file')
|
|
69
|
+
pubip=$(ops_state_get "$state" '.resources.public_ip')
|
|
70
|
+
[ -n "$keyfile" ] && [ -n "$pubip" ] || {
|
|
71
|
+
echo "state is missing resources.key_file or resources.public_ip; cannot SSH to existing EC2" >&2
|
|
72
|
+
return 1
|
|
73
|
+
}
|
|
74
|
+
printf '%s\t%s\n' "$keyfile" "$pubip"
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
ops_ssh() {
|
|
78
|
+
local state=$1 command=$2 keyfile pubip
|
|
79
|
+
IFS=$'\t' read -r keyfile pubip < <(ops_remote_base "$state")
|
|
80
|
+
ssh -i "$keyfile" -o StrictHostKeyChecking=accept-new -o ConnectTimeout=10 ubuntu@"$pubip" "$command"
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
ops_cc_connect_service_name() {
|
|
84
|
+
local state=$1 service_name service_dir
|
|
85
|
+
service_name=$(ops_state_get "$state" '.agent_service_id')
|
|
86
|
+
[ -n "$service_name" ] || service_name=$(ops_state_get "$state" '.domain')
|
|
87
|
+
if [ -z "$service_name" ]; then
|
|
88
|
+
service_dir=$(ops_state_get "$state" '.agent_service_dir')
|
|
89
|
+
[ -n "$service_dir" ] && service_name=$(basename "$service_dir")
|
|
90
|
+
fi
|
|
91
|
+
printf '%s\n' "${service_name:-cc-connect}"
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
ops_cc_connect_target_work_dir() {
|
|
95
|
+
local state=$1 config runtime_dir service_dir
|
|
96
|
+
config=$(ops_state_get "$state" '.cc_connect_config')
|
|
97
|
+
runtime_dir=$(ops_state_get "$state" '.cc_connect_runtime_dir')
|
|
98
|
+
service_dir=$(ops_state_get "$state" '.agent_service_dir')
|
|
99
|
+
if [ -n "$config" ]; then
|
|
100
|
+
ops_path_dirname "$config"
|
|
101
|
+
elif [ -n "$runtime_dir" ]; then
|
|
102
|
+
printf '%s\n' "$runtime_dir"
|
|
103
|
+
elif [ -n "$service_dir" ]; then
|
|
104
|
+
printf '%s/cc-connect\n' "${service_dir%/}"
|
|
105
|
+
fi
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
ops_stop_scoped_daemon() {
|
|
109
|
+
local state=$1 binary service_name target_work_dir status_out daemon_status work_dir
|
|
110
|
+
binary=$(ops_state_get "$state" '.cc_connect_binary')
|
|
111
|
+
[ -n "$binary" ] || binary=direxio-connect
|
|
112
|
+
service_name=$(ops_cc_connect_service_name "$state")
|
|
113
|
+
target_work_dir=$(ops_cc_connect_target_work_dir "$state")
|
|
114
|
+
[ -n "$target_work_dir" ] || return 1
|
|
115
|
+
|
|
116
|
+
case "$binary" in
|
|
117
|
+
*/*|[A-Za-z]:/*|[A-Za-z]:\\*)
|
|
118
|
+
[ -x "$binary" ] || return 1
|
|
119
|
+
;;
|
|
120
|
+
*)
|
|
121
|
+
command -v "$binary" >/dev/null 2>&1 || return 1
|
|
122
|
+
;;
|
|
123
|
+
esac
|
|
124
|
+
|
|
125
|
+
status_out=$("$binary" daemon status --service-name "$service_name" 2>/dev/null) || return 1
|
|
126
|
+
daemon_status=$(printf '%s\n' "$status_out" | sed -nE 's/^[[:space:]]*Status:[[:space:]]*//p' | head -n 1)
|
|
127
|
+
work_dir=$(printf '%s\n' "$status_out" | sed -nE 's/^[[:space:]]*WorkDir:[[:space:]]*//p' | head -n 1)
|
|
128
|
+
[ "$daemon_status" = "Running" ] || return 1
|
|
129
|
+
[ -n "$work_dir" ] || return 1
|
|
130
|
+
ops_paths_match "$target_work_dir" "$work_dir" || return 1
|
|
131
|
+
|
|
132
|
+
"$binary" daemon stop --service-name "$service_name" >/dev/null 2>&1
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
ops_update_remote_command() {
|
|
136
|
+
local image=${1:-} image_q remote_script
|
|
137
|
+
image_q=$(ops_sh_quote "$image")
|
|
138
|
+
remote_script=$(cat <<'EOF'
|
|
139
|
+
set -eu
|
|
140
|
+
cd /opt/p2p
|
|
141
|
+
if [ -n "${MESSAGE_SERVER_IMAGE:-}" ]; then
|
|
142
|
+
IMAGE=$MESSAGE_SERVER_IMAGE
|
|
143
|
+
escaped_image=$(printf '%s\n' "$IMAGE" | sed 's/[\/&]/\\&/g')
|
|
144
|
+
if grep -q '^MESSAGE_SERVER_IMAGE=' .env; then
|
|
145
|
+
sed -i "s#^MESSAGE_SERVER_IMAGE=.*#MESSAGE_SERVER_IMAGE=$escaped_image#" .env
|
|
146
|
+
else
|
|
147
|
+
printf 'MESSAGE_SERVER_IMAGE=%s\n' "$IMAGE" | tee -a .env >/dev/null
|
|
148
|
+
fi
|
|
149
|
+
fi
|
|
150
|
+
docker compose --env-file .env pull
|
|
151
|
+
docker compose --env-file .env up -d
|
|
152
|
+
DOMAIN=$(grep '^DOMAIN=' .env | cut -d= -f2)
|
|
153
|
+
sync_container_bootstrap() {
|
|
154
|
+
tmp=$(mktemp)
|
|
155
|
+
if docker compose --env-file .env exec -T message-server sh -c 'test -s /var/direxio-message-server/p2p/bootstrap.json && cat /var/direxio-message-server/p2p/bootstrap.json' > "$tmp"; then
|
|
156
|
+
install -m 0600 "$tmp" /opt/p2p/bootstrap.json
|
|
157
|
+
rm -f "$tmp"
|
|
158
|
+
return 0
|
|
159
|
+
fi
|
|
160
|
+
rm -f "$tmp"
|
|
161
|
+
return 1
|
|
162
|
+
}
|
|
163
|
+
bootstrap_ready() {
|
|
164
|
+
test -s /opt/p2p/bootstrap.json \
|
|
165
|
+
&& grep -q '"password"[[:space:]]*:' /opt/p2p/bootstrap.json \
|
|
166
|
+
&& grep -q '"agent_token"[[:space:]]*:' /opt/p2p/bootstrap.json \
|
|
167
|
+
&& grep -q '"access_token"[[:space:]]*:' /opt/p2p/bootstrap.json \
|
|
168
|
+
&& grep -Eq '"agent_room_id"[[:space:]]*:[[:space:]]*"![^"]+"' /opt/p2p/bootstrap.json
|
|
169
|
+
}
|
|
170
|
+
if sync_container_bootstrap && bootstrap_ready; then
|
|
171
|
+
echo "[update] existing bootstrap credentials are present; skipping portal.bootstrap."
|
|
172
|
+
else
|
|
173
|
+
DOMAIN="$DOMAIN" bash /opt/p2p/init-tokens.sh
|
|
174
|
+
fi
|
|
175
|
+
EOF
|
|
176
|
+
)
|
|
177
|
+
printf 'sudo MESSAGE_SERVER_IMAGE=%s sh -lc %s\n' "$image_q" "$(ops_sh_quote "$remote_script")"
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
ops_reset_remote_command() {
|
|
181
|
+
cat <<'EOF'
|
|
182
|
+
set -eu
|
|
183
|
+
cd /opt/p2p
|
|
184
|
+
sudo docker compose --env-file .env down
|
|
185
|
+
project=$(basename "$PWD")
|
|
186
|
+
for volume in postgres-data message-config message-data; do
|
|
187
|
+
ids=$(sudo docker volume ls -q --filter "label=com.docker.compose.project=$project" --filter "label=com.docker.compose.volume=$volume" 2>/dev/null || true)
|
|
188
|
+
if [ -n "$ids" ]; then
|
|
189
|
+
sudo docker volume rm $ids >/dev/null 2>&1 || true
|
|
190
|
+
fi
|
|
191
|
+
sudo docker volume rm "${project}_${volume}" >/dev/null 2>&1 || true
|
|
192
|
+
done
|
|
193
|
+
sudo rm -f /opt/p2p/bootstrap.json /opt/p2p/wellknown/owner.json
|
|
194
|
+
new_code=$(od -An -N4 -tu4 /dev/urandom | awk '{printf "%08d", $1 % 100000000}')
|
|
195
|
+
sudo sed -i '/^P2P_PORTAL_PASSWORD=/d' .env
|
|
196
|
+
printf 'P2P_PORTAL_PASSWORD=%s\n' "$new_code" | sudo tee -a .env >/dev/null
|
|
197
|
+
sudo docker compose --env-file .env up -d
|
|
198
|
+
DOMAIN=$(grep '^DOMAIN=' .env | cut -d= -f2)
|
|
199
|
+
sudo DOMAIN="$DOMAIN" bash /opt/p2p/init-tokens.sh
|
|
200
|
+
EOF
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
ops_mark_refresh_pending() {
|
|
204
|
+
local state=$1 start_phase=${2:-S4_BOOTSTRAP_STACK} tmp
|
|
205
|
+
tmp="$state.tmp.$$"
|
|
206
|
+
jq --arg start "$start_phase" --arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" '
|
|
207
|
+
del(
|
|
208
|
+
.password,
|
|
209
|
+
.access_token,
|
|
210
|
+
.agent_token,
|
|
211
|
+
.agent_room_id,
|
|
212
|
+
.user_confirmations,
|
|
213
|
+
.runtime_checks
|
|
214
|
+
)
|
|
215
|
+
| .agent_install_status = "refresh_pending"
|
|
216
|
+
| .phase = $start
|
|
217
|
+
| (if ($start == "S4_BOOTSTRAP_STACK") then
|
|
218
|
+
.phases.S4_BOOTSTRAP_STACK = {status:"pending", ts:$ts, evidence:"existing node operation requires fresh health check"}
|
|
219
|
+
else . end)
|
|
220
|
+
| .phases.S5_INIT_TOKENS = {status:"pending", ts:$ts, evidence:"existing node operation requires fresh bootstrap credentials"}
|
|
221
|
+
| .phases.S6_WIRE_LOCAL = {status:"pending", ts:$ts, evidence:"existing node operation requires local credentials and MCP refresh"}
|
|
222
|
+
| .phases.S7_VERIFY_E2E = {status:"pending", ts:$ts, evidence:"existing node operation requires fresh verification"}
|
|
223
|
+
' "$state" > "$tmp" && mv "$tmp" "$state"
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
ops_write_report() {
|
|
227
|
+
local operation=$1 status=$2 state=$3 report
|
|
228
|
+
report=$(operation_report_write "$operation" "$status" "$state")
|
|
229
|
+
printf '%s\n' "$report"
|
|
230
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# lib/paths.sh - local Direxio service directory helpers.
|
|
3
|
+
|
|
4
|
+
direxio_home() {
|
|
5
|
+
printf '%s\n' "${DIREXIO_HOME:-$HOME/.direxio}"
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
direxio_service_id() {
|
|
9
|
+
local raw=${1:-} host
|
|
10
|
+
host=${raw#http://}
|
|
11
|
+
host=${host#https://}
|
|
12
|
+
host=${host%%/*}
|
|
13
|
+
case "$host" in
|
|
14
|
+
*:*) host="${host%%:*}-${host#*:}" ;;
|
|
15
|
+
esac
|
|
16
|
+
printf '%s\n' "$host" | tr '[:upper:]' '[:lower:]' | sed -E 's/:/-/g; s/[^a-z0-9._-]+/-/g; s/^-+//; s/-+$//; s/^$/direxio-service/'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
direxio_service_dir() {
|
|
20
|
+
local service_id
|
|
21
|
+
service_id=$(direxio_service_id "$1")
|
|
22
|
+
printf '%s/nodes/%s\n' "$(direxio_home)" "$service_id"
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
direxio_default_workdir() {
|
|
26
|
+
if [ -n "${P2P_WORKDIR:-}" ]; then
|
|
27
|
+
printf '%s\n' "$P2P_WORKDIR"
|
|
28
|
+
elif [ -n "${DIREXIO_WORKDIR:-}" ]; then
|
|
29
|
+
printf '%s\n' "$DIREXIO_WORKDIR"
|
|
30
|
+
elif [ -n "${DOMAIN:-}" ]; then
|
|
31
|
+
direxio_service_dir "$DOMAIN"
|
|
32
|
+
else
|
|
33
|
+
printf '%s/nodes\n' "$(direxio_home)"
|
|
34
|
+
fi
|
|
35
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# lib/state.sh - state.json helpers for the deployment state machine.
|
|
3
|
+
#
|
|
4
|
+
# Sourced by orchestrate.sh and phases/*.sh. All state.json reads/writes go
|
|
5
|
+
# through this file to keep structure and fields consistent. Requires jq.
|
|
6
|
+
#
|
|
7
|
+
# state.json path: $P2P_WORKDIR/state.json.
|
|
8
|
+
# By default, DOMAIN=__DOMAIN__ maps to ~/.direxio/nodes/<service_id>/state.json.
|
|
9
|
+
#
|
|
10
|
+
# PHASES order is the state-machine execution order.
|
|
11
|
+
|
|
12
|
+
STATE_LIB_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
13
|
+
# shellcheck disable=SC1090
|
|
14
|
+
source "$STATE_LIB_DIR/paths.sh"
|
|
15
|
+
|
|
16
|
+
# Phase list; order matters.
|
|
17
|
+
PHASES=(
|
|
18
|
+
S0_PREREQ_AWS
|
|
19
|
+
S1_PREFLIGHT
|
|
20
|
+
S2_DOMAIN
|
|
21
|
+
S3_PROVISION
|
|
22
|
+
S4_BOOTSTRAP_STACK
|
|
23
|
+
S5_INIT_TOKENS
|
|
24
|
+
S6_WIRE_LOCAL
|
|
25
|
+
S7_VERIFY_E2E
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Paths.
|
|
29
|
+
P2P_WORKDIR=$(direxio_default_workdir)
|
|
30
|
+
STATE_JSON="$P2P_WORKDIR/state.json"
|
|
31
|
+
|
|
32
|
+
# Timestamp helper.
|
|
33
|
+
_now() { date -u +%Y-%m-%dT%H:%M:%SZ; }
|
|
34
|
+
|
|
35
|
+
# Shared logging helpers.
|
|
36
|
+
log() { echo -e "\033[36m[p2p]\033[0m $*" >&2; }
|
|
37
|
+
ok() { echo -e "\033[32m[p2p]\033[0m $*" >&2; }
|
|
38
|
+
warn() { echo -e "\033[33m[p2p]\033[0m $*" >&2; }
|
|
39
|
+
fail() { echo -e "\033[31m[p2p][FATAL]\033[0m $*" >&2; exit 1; }
|
|
40
|
+
is_yes() {
|
|
41
|
+
case "$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')" in
|
|
42
|
+
y|yes|true|1) return 0 ;;
|
|
43
|
+
*) return 1 ;;
|
|
44
|
+
esac
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# Initialize state.json for a new deployment.
|
|
48
|
+
state_init() {
|
|
49
|
+
mkdir -p "$P2P_WORKDIR"
|
|
50
|
+
local run_id=${RUN_ID:-p2p-$(date -u +%Y%m%d-%H%M%S)}
|
|
51
|
+
local phases_json="{}"
|
|
52
|
+
local p
|
|
53
|
+
for p in "${PHASES[@]}"; do
|
|
54
|
+
phases_json=$(echo "$phases_json" | jq --arg k "$p" '. + {($k): {"status":"pending"}}')
|
|
55
|
+
done
|
|
56
|
+
jq -n \
|
|
57
|
+
--arg run_id "$run_id" \
|
|
58
|
+
--arg region "${AWS_DEFAULT_REGION:-${AWS_REGION:-}}" \
|
|
59
|
+
--argjson phases "$phases_json" \
|
|
60
|
+
--arg ts "$(_now)" \
|
|
61
|
+
'{
|
|
62
|
+
run_id: $run_id,
|
|
63
|
+
region: (if $region == "" then null else $region end),
|
|
64
|
+
domain_mode: null,
|
|
65
|
+
domain: null,
|
|
66
|
+
domain_confirmed_irreversible: false,
|
|
67
|
+
instance_type: null,
|
|
68
|
+
dns_ready: false,
|
|
69
|
+
existing_state_confirmed: false,
|
|
70
|
+
phase: "S0_PREREQ_AWS",
|
|
71
|
+
created_at: $ts,
|
|
72
|
+
phases: $phases,
|
|
73
|
+
resources: {}
|
|
74
|
+
}' > "$STATE_JSON"
|
|
75
|
+
log "Initialized state.json -> $STATE_JSON (run_id=$run_id)"
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# Ensure state.json exists.
|
|
79
|
+
state_ensure() {
|
|
80
|
+
[ -f "$STATE_JSON" ] || state_init
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# Atomic write using a jq filter.
|
|
84
|
+
_state_write() {
|
|
85
|
+
local filter=$1; shift
|
|
86
|
+
local tmp="$STATE_JSON.tmp.$$"
|
|
87
|
+
jq "$@" "$filter" "$STATE_JSON" > "$tmp" && mv "$tmp" "$STATE_JSON"
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# Top-level field accessors.
|
|
91
|
+
state_get() { jq -r --arg k "$1" '.[$k] // empty' "$STATE_JSON"; }
|
|
92
|
+
state_set() { _state_write '.[$k] = $v' --arg k "$1" --arg v "$2"; }
|
|
93
|
+
state_set_raw() { _state_write ".$1 = $2"; }
|
|
94
|
+
|
|
95
|
+
# Resource records used by destroy.sh.
|
|
96
|
+
res_set() { _state_write '.resources[$k] = $v' --arg k "$1" --arg v "$2"; }
|
|
97
|
+
res_get() { jq -r --arg k "$1" '.resources[$k] // empty' "$STATE_JSON"; }
|
|
98
|
+
|
|
99
|
+
# Phase status helpers.
|
|
100
|
+
# phase_status <PHASE>
|
|
101
|
+
phase_status() { jq -r --arg p "$1" '.phases[$p].status // "pending"' "$STATE_JSON"; }
|
|
102
|
+
|
|
103
|
+
# phase_set <PHASE> <status> [evidence]
|
|
104
|
+
phase_set() {
|
|
105
|
+
local p=$1 st=$2 ev=${3:-}
|
|
106
|
+
_state_write '
|
|
107
|
+
.phases[$p].status = $st
|
|
108
|
+
| .phases[$p].ts = $ts
|
|
109
|
+
| (if $ev != "" then .phases[$p].evidence = $ev else . end)
|
|
110
|
+
| .phase = $p
|
|
111
|
+
' --arg p "$p" --arg st "$st" --arg ev "$ev" --arg ts "$(_now)"
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# Find the first phase whose status is not done.
|
|
115
|
+
first_unfinished_phase() {
|
|
116
|
+
local p
|
|
117
|
+
for p in "${PHASES[@]}"; do
|
|
118
|
+
[ "$(phase_status "$p")" != "done" ] && { echo "$p"; return 0; }
|
|
119
|
+
done
|
|
120
|
+
echo "DONE"
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
# poll_until <description> <interval-seconds> <max-attempts> <check-command...>
|
|
124
|
+
# Return 0 when the check command succeeds. max=0 means poll forever.
|
|
125
|
+
poll_until() {
|
|
126
|
+
local desc=$1 interval=$2 maxn=$3; shift 3
|
|
127
|
+
local i=0
|
|
128
|
+
while true; do
|
|
129
|
+
if "$@"; then ok "$desc ✓"; return 0; fi
|
|
130
|
+
i=$((i+1))
|
|
131
|
+
if [ "$maxn" -gt 0 ] && [ "$i" -ge "$maxn" ]; then
|
|
132
|
+
warn "$desc timed out after $i unsuccessful attempts"; return 1
|
|
133
|
+
fi
|
|
134
|
+
log "$desc waiting (attempt $i, retry in ${interval}s)"
|
|
135
|
+
sleep "$interval"
|
|
136
|
+
done
|
|
137
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
|
|
4
|
+
const command = process.argv[2] || "direxio-mcp";
|
|
5
|
+
const timeoutMs = Number.parseInt(process.env.DIREXIO_MCP_TOOLS_TIMEOUT_MS || "8000", 10);
|
|
6
|
+
|
|
7
|
+
const child = spawn(command, {
|
|
8
|
+
env: process.env,
|
|
9
|
+
shell: true,
|
|
10
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
let stdout = Buffer.alloc(0);
|
|
14
|
+
let stderr = "";
|
|
15
|
+
let completed = false;
|
|
16
|
+
const responses = new Map();
|
|
17
|
+
|
|
18
|
+
const timer = setTimeout(() => {
|
|
19
|
+
finishWithError(`timed out waiting for MCP tools/list after ${timeoutMs}ms`);
|
|
20
|
+
}, timeoutMs);
|
|
21
|
+
|
|
22
|
+
child.stderr.on("data", (chunk) => {
|
|
23
|
+
stderr += chunk.toString("utf8");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
child.stdout.on("data", (chunk) => {
|
|
27
|
+
stdout = Buffer.concat([stdout, chunk]);
|
|
28
|
+
readFrames();
|
|
29
|
+
if (responses.has(2)) {
|
|
30
|
+
const response = responses.get(2);
|
|
31
|
+
const tools = Array.isArray(response?.result?.tools) ? response.result.tools : [];
|
|
32
|
+
const names = tools
|
|
33
|
+
.map((tool) => tool?.name)
|
|
34
|
+
.filter((name) => typeof name === "string" && name.length > 0);
|
|
35
|
+
finish({ tools: names, tool_count: names.length });
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
child.on("error", (error) => {
|
|
40
|
+
finishWithError(error.message);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
child.on("exit", (code) => {
|
|
44
|
+
if (!completed && code !== 0) {
|
|
45
|
+
finishWithError(`MCP server exited with code ${code}${stderr ? `: ${stderr.trim()}` : ""}`);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
send({
|
|
50
|
+
jsonrpc: "2.0",
|
|
51
|
+
id: 1,
|
|
52
|
+
method: "initialize",
|
|
53
|
+
params: {
|
|
54
|
+
protocolVersion: "2024-11-05",
|
|
55
|
+
capabilities: {},
|
|
56
|
+
clientInfo: { name: "direxio-deployer", version: "0.0.0" }
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
send({ jsonrpc: "2.0", method: "notifications/initialized", params: {} });
|
|
60
|
+
send({ jsonrpc: "2.0", id: 2, method: "tools/list", params: {} });
|
|
61
|
+
|
|
62
|
+
function send(message) {
|
|
63
|
+
child.stdin.write(`${JSON.stringify(message)}\n`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function readFrames() {
|
|
67
|
+
while (true) {
|
|
68
|
+
const lineEnd = stdout.indexOf("\n");
|
|
69
|
+
if (lineEnd < 0) return;
|
|
70
|
+
const line = stdout.subarray(0, lineEnd).toString("utf8").replace(/\r$/, "");
|
|
71
|
+
stdout = stdout.subarray(lineEnd + 1);
|
|
72
|
+
if (line.length === 0) continue;
|
|
73
|
+
const message = JSON.parse(line);
|
|
74
|
+
if (typeof message.id !== "undefined") {
|
|
75
|
+
responses.set(message.id, message);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function finish(value) {
|
|
81
|
+
if (completed) return;
|
|
82
|
+
completed = true;
|
|
83
|
+
clearTimeout(timer);
|
|
84
|
+
console.log(JSON.stringify(value));
|
|
85
|
+
child.kill();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function finishWithError(message) {
|
|
89
|
+
if (completed) return;
|
|
90
|
+
completed = true;
|
|
91
|
+
clearTimeout(timer);
|
|
92
|
+
console.error(message);
|
|
93
|
+
child.kill();
|
|
94
|
+
process.exitCode = 1;
|
|
95
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
param(
|
|
2
|
+
[Parameter(ValueFromRemainingArguments = $true)]
|
|
3
|
+
[string[]] $OrchestrateArgs
|
|
4
|
+
)
|
|
5
|
+
|
|
6
|
+
$ErrorActionPreference = 'Stop'
|
|
7
|
+
|
|
8
|
+
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
9
|
+
$RepoRoot = Split-Path -Parent $ScriptDir
|
|
10
|
+
|
|
11
|
+
function Find-GitBash {
|
|
12
|
+
$candidates = @(
|
|
13
|
+
"$env:ProgramFiles\Git\bin\bash.exe",
|
|
14
|
+
"$env:ProgramFiles\Git\usr\bin\bash.exe",
|
|
15
|
+
"$env:LOCALAPPDATA\Programs\Git\bin\bash.exe",
|
|
16
|
+
"$env:LOCALAPPDATA\Programs\Git\usr\bin\bash.exe"
|
|
17
|
+
)
|
|
18
|
+
foreach ($candidate in $candidates) {
|
|
19
|
+
if ($candidate -and (Test-Path $candidate)) {
|
|
20
|
+
return $candidate
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
throw "Git Bash was not found. Install Git for Windows or run scripts/orchestrate.sh from a POSIX shell."
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function ConvertTo-GitBashPath([string] $Path) {
|
|
27
|
+
$full = [System.IO.Path]::GetFullPath($Path)
|
|
28
|
+
$drive = $full.Substring(0, 1).ToLowerInvariant()
|
|
29
|
+
$rest = $full.Substring(2).Replace('\', '/')
|
|
30
|
+
return "/$drive$rest"
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function Quote-BashArg([string] $Value) {
|
|
34
|
+
return "'" + ($Value -replace "'", "'\''") + "'"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function Find-CodexBinary {
|
|
38
|
+
$root = Join-Path $env:LOCALAPPDATA 'OpenAI\Codex\bin'
|
|
39
|
+
if (-not (Test-Path $root)) {
|
|
40
|
+
return Find-CommandPath @('codex.exe', 'codex.cmd', 'codex')
|
|
41
|
+
}
|
|
42
|
+
$bundled = Get-ChildItem $root -Filter codex.exe -Recurse -ErrorAction SilentlyContinue |
|
|
43
|
+
Select-Object -First 1 -ExpandProperty FullName
|
|
44
|
+
if ($bundled) {
|
|
45
|
+
return $bundled
|
|
46
|
+
}
|
|
47
|
+
return Find-CommandPath @('codex.exe', 'codex.cmd', 'codex')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function Find-CommandPath([string[]] $Names) {
|
|
51
|
+
foreach ($name in $Names) {
|
|
52
|
+
$cmd = Get-Command $name -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
53
|
+
if ($cmd -and $cmd.Source) {
|
|
54
|
+
return $cmd.Source
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return $null
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function Set-AgentCommandIfMissing([string[]] $EnvNames, [string[]] $CommandNames) {
|
|
61
|
+
foreach ($envName in $EnvNames) {
|
|
62
|
+
if ([Environment]::GetEnvironmentVariable($envName, 'Process')) {
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
$path = Find-CommandPath $CommandNames
|
|
67
|
+
if (-not $path) {
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
[Environment]::SetEnvironmentVariable($EnvNames[0], $path, 'Process')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
$bash = Find-GitBash
|
|
74
|
+
|
|
75
|
+
$windowsDirexioHome = if ($env:DIREXIO_HOME) { $env:DIREXIO_HOME } else { Join-Path $env:USERPROFILE '.direxio' }
|
|
76
|
+
$env:DIREXIO_WINDOWS_HOME = $windowsDirexioHome
|
|
77
|
+
$env:DIREXIO_HOME = ConvertTo-GitBashPath $windowsDirexioHome
|
|
78
|
+
$env:DIREXIO_LOCAL_PATH_STYLE = 'windows'
|
|
79
|
+
|
|
80
|
+
if (-not $env:DIREXIO_AGENT_WORKSPACE) {
|
|
81
|
+
$env:DIREXIO_AGENT_WORKSPACE_WINDOWS = (Get-Location).ProviderPath
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if ($env:P2P_WORKDIR) {
|
|
85
|
+
$env:P2P_WORKDIR_WINDOWS = $env:P2P_WORKDIR
|
|
86
|
+
$env:P2P_WORKDIR = ConvertTo-GitBashPath $env:P2P_WORKDIR
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (-not $env:DIREXIO_CODEX_COMMAND) {
|
|
90
|
+
$codex = Find-CodexBinary
|
|
91
|
+
if ($codex) { $env:DIREXIO_CODEX_COMMAND = $codex }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
Set-AgentCommandIfMissing @('DIREXIO_CLAUDECODE_COMMAND', 'DIREXIO_CLAUDE_CODE_COMMAND', 'DIREXIO_CLAUDE_COMMAND') @('claude.exe', 'claude.cmd', 'claude', 'claude-code.exe', 'claude-code.cmd', 'claude-code')
|
|
95
|
+
Set-AgentCommandIfMissing @('DIREXIO_GEMINI_COMMAND') @('gemini.exe', 'gemini.cmd', 'gemini')
|
|
96
|
+
Set-AgentCommandIfMissing @('DIREXIO_COPILOT_COMMAND') @('copilot.exe', 'copilot.cmd', 'copilot')
|
|
97
|
+
Set-AgentCommandIfMissing @('DIREXIO_DEVIN_COMMAND') @('devin.exe', 'devin.cmd', 'devin')
|
|
98
|
+
Set-AgentCommandIfMissing @('DIREXIO_KIMI_COMMAND') @('kimi.exe', 'kimi.cmd', 'kimi')
|
|
99
|
+
Set-AgentCommandIfMissing @('DIREXIO_OPENCODE_COMMAND', 'DIREXIO_OPEN_CODE_COMMAND') @('opencode.exe', 'opencode.cmd', 'opencode')
|
|
100
|
+
Set-AgentCommandIfMissing @('DIREXIO_IFLOW_COMMAND') @('iflow.exe', 'iflow.cmd', 'iflow')
|
|
101
|
+
Set-AgentCommandIfMissing @('DIREXIO_QODER_COMMAND', 'DIREXIO_QODERCLI_COMMAND') @('qodercli.exe', 'qodercli.cmd', 'qodercli', 'qoder.exe', 'qoder.cmd', 'qoder')
|
|
102
|
+
Set-AgentCommandIfMissing @('DIREXIO_PI_COMMAND') @('pi.exe', 'pi.cmd', 'pi')
|
|
103
|
+
Set-AgentCommandIfMissing @('DIREXIO_ANTIGRAVITY_COMMAND', 'DIREXIO_AGY_COMMAND') @('agy.exe', 'agy.cmd', 'agy')
|
|
104
|
+
Set-AgentCommandIfMissing @('DIREXIO_OPENCLAW_COMMAND') @('openclaw.exe', 'openclaw.cmd', 'openclaw')
|
|
105
|
+
Set-AgentCommandIfMissing @('DIREXIO_HERMES_COMMAND') @('hermes.exe', 'hermes.cmd', 'hermes')
|
|
106
|
+
|
|
107
|
+
$repoRootForBash = ConvertTo-GitBashPath $RepoRoot
|
|
108
|
+
$quotedArgs = ($OrchestrateArgs | ForEach-Object { Quote-BashArg $_ }) -join ' '
|
|
109
|
+
$command = "cd $(Quote-BashArg $repoRootForBash) && ./scripts/orchestrate.sh $quotedArgs"
|
|
110
|
+
|
|
111
|
+
& $bash -lc $command
|
|
112
|
+
exit $LASTEXITCODE
|