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.
Files changed (77) hide show
  1. package/AGENTS.md +92 -0
  2. package/LICENSE +21 -0
  3. package/README.md +221 -0
  4. package/README_zh.md +218 -0
  5. package/SKILL.md +722 -0
  6. package/agents/README.md +25 -0
  7. package/agents/openai.yaml +12 -0
  8. package/bin/direxio-deployer.mjs +375 -0
  9. package/package.json +28 -0
  10. package/references/agent-targets.md +128 -0
  11. package/references/architecture.md +44 -0
  12. package/references/bug-history.md +78 -0
  13. package/references/deployment-lessons.md +218 -0
  14. package/references/deployment-optimization-audit.md +317 -0
  15. package/references/deployment-workflow.md +341 -0
  16. package/references/iam-policy.json +52 -0
  17. package/references/runtime-wiring.md +209 -0
  18. package/references/state-machine.md +46 -0
  19. package/references/token-refresh.md +81 -0
  20. package/references/tooling.md +106 -0
  21. package/references/troubleshooting.md +26 -0
  22. package/references/user-journey.md +75 -0
  23. package/references/verification-recovery.md +84 -0
  24. package/references/voip-turn-runbook.md +154 -0
  25. package/references/windows-deployment-notes.md +119 -0
  26. package/scripts/aws-credentials.sh +195 -0
  27. package/scripts/cloud-init/Caddyfile +48 -0
  28. package/scripts/cloud-init/docker-compose.yml +125 -0
  29. package/scripts/cloud-init/init-tokens.sh +238 -0
  30. package/scripts/cloud-init/user-data.yaml +40 -0
  31. package/scripts/destroy.ps1 +77 -0
  32. package/scripts/destroy.sh +589 -0
  33. package/scripts/lib/aws.sh +73 -0
  34. package/scripts/lib/domain.sh +175 -0
  35. package/scripts/lib/operation_report.sh +240 -0
  36. package/scripts/lib/ops.sh +230 -0
  37. package/scripts/lib/paths.sh +35 -0
  38. package/scripts/lib/state.sh +137 -0
  39. package/scripts/mcp-tools-list.mjs +95 -0
  40. package/scripts/orchestrate.ps1 +112 -0
  41. package/scripts/orchestrate.sh +1126 -0
  42. package/scripts/phases/s0_prereq_aws.sh +39 -0
  43. package/scripts/phases/s1_preflight.sh +72 -0
  44. package/scripts/phases/s2_domain.sh +103 -0
  45. package/scripts/phases/s3_provision.sh +421 -0
  46. package/scripts/phases/s4_bootstrap_stack.sh +38 -0
  47. package/scripts/phases/s5_init_tokens.sh +118 -0
  48. package/scripts/phases/s6_wire_local.sh +1435 -0
  49. package/scripts/phases/s7_verify_e2e.sh +136 -0
  50. package/scripts/pricing-estimate.sh +256 -0
  51. package/scripts/render/render-userdata.sh +86 -0
  52. package/scripts/reset-app-data.sh +40 -0
  53. package/scripts/update.sh +30 -0
  54. package/tests/aws_credentials_test.sh +139 -0
  55. package/tests/connect_daemon_runtime_check_test.sh +120 -0
  56. package/tests/default_paths_test.sh +58 -0
  57. package/tests/destroy_local_bridge_test.sh +154 -0
  58. package/tests/destroy_root_identity_test.sh +91 -0
  59. package/tests/destroy_route53_zone_test.sh +80 -0
  60. package/tests/domain_authoritative_dns_test.sh +49 -0
  61. package/tests/mcp_doctor_runtime_check_test.sh +86 -0
  62. package/tests/mcp_smoke_runtime_check_test.sh +121 -0
  63. package/tests/mcp_tools_runtime_check_test.sh +123 -0
  64. package/tests/npm_skill_distribution_test.sh +95 -0
  65. package/tests/operation_report_test.sh +258 -0
  66. package/tests/orchestrate_status_recovery_test.sh +91 -0
  67. package/tests/phase_timeout_test.sh +88 -0
  68. package/tests/pricing_estimate_test.sh +159 -0
  69. package/tests/render_userdata_remote_nodes_test.sh +40 -0
  70. package/tests/root_volume_tracking_test.sh +41 -0
  71. package/tests/route53_overwrite_guard_test.sh +86 -0
  72. package/tests/route53_zone_auto_create_test.sh +66 -0
  73. package/tests/runtime_summary_check_test.sh +203 -0
  74. package/tests/s6_wire_local_test.sh +405 -0
  75. package/tests/skill_structure_test.sh +298 -0
  76. package/tests/update_reset_ops_test.sh +230 -0
  77. package/tests/user_confirmation_gates_test.sh +152 -0
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT=$(cd "$(dirname "$0")/.." && pwd)
5
+ tmp=$(mktemp -d)
6
+ trap 'rm -rf "$tmp"' EXIT
7
+
8
+ fakebin="$tmp/bin"
9
+ mkdir -p "$fakebin"
10
+ cat > "$fakebin/dig" <<'EOF'
11
+ #!/usr/bin/env bash
12
+ set -euo pipefail
13
+
14
+ if [ "${1:-}" = "+short" ] && [ "${2:-}" = "A" ] && [ "${3:-}" = "app.example.test" ]; then
15
+ printf '203.0.113.88\n'
16
+ exit 0
17
+ fi
18
+
19
+ if [ "${1:-}" = "+short" ] && [ "${2:-}" = "NS" ] && [ "${3:-}" = "app.example.test" ]; then
20
+ exit 0
21
+ fi
22
+
23
+ if [ "${1:-}" = "+short" ] && [ "${2:-}" = "NS" ] && [ "${3:-}" = "example.test" ]; then
24
+ printf 'ns1.example.test.\n'
25
+ exit 0
26
+ fi
27
+
28
+ if [ "${1:-}" = "+short" ] && [ "${2:-}" = "@ns1.example.test" ] && [ "${3:-}" = "A" ] && [ "${4:-}" = "app.example.test" ]; then
29
+ printf '%s\n' "${AUTHORITATIVE_A:-198.51.100.10}"
30
+ exit 0
31
+ fi
32
+
33
+ echo "unexpected dig call: $*" >&2
34
+ exit 1
35
+ EOF
36
+ chmod 700 "$fakebin/dig"
37
+
38
+ PATH="$fakebin:$PATH"
39
+ # shellcheck disable=SC1091
40
+ source "$ROOT/scripts/lib/domain.sh"
41
+
42
+ if AUTHORITATIVE_A=198.51.100.10 domain_resolves_to_ip app.example.test 203.0.113.88; then
43
+ echo "recursive DNS alone must not pass when authoritative DNS still points at a different IP" >&2
44
+ exit 1
45
+ fi
46
+
47
+ AUTHORITATIVE_A=203.0.113.88 domain_resolves_to_ip app.example.test 203.0.113.88
48
+
49
+ echo "domain authoritative dns ok"
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT=$(cd "$(dirname "$0")/.." && pwd)
5
+ tmp=$(mktemp -d)
6
+ trap 'rm -rf "$tmp"' EXIT
7
+
8
+ export HOME="$tmp/home"
9
+ mkdir -p "$HOME"
10
+
11
+ fakebin="$tmp/bin"
12
+ mkdir -p "$fakebin"
13
+ cat > "$fakebin/direxio-mcp" <<'EOF'
14
+ #!/usr/bin/env bash
15
+ set -euo pipefail
16
+
17
+ [ "${1:-}" = "doctor" ]
18
+ [ "${2:-}" = "--json" ]
19
+ [ "${DIREXIO_CREDENTIALS_FILE:-}" = "${EXPECTED_CREDENTIALS_FILE:-}" ]
20
+
21
+ cat <<JSON
22
+ {
23
+ "ok": true,
24
+ "domain": "mcp-check.example.test",
25
+ "agent_room_id": "!agent:mcp-check.example.test",
26
+ "token": "redacted"
27
+ }
28
+ JSON
29
+ EOF
30
+ chmod 700 "$fakebin/direxio-mcp"
31
+
32
+ service_dir="$HOME/.direxio/nodes/mcp-check.example.test"
33
+ mkdir -p "$service_dir"
34
+ credentials="$service_dir/credentials.json"
35
+ : > "$credentials"
36
+ expected_credentials="$credentials"
37
+ if command -v cygpath >/dev/null 2>&1; then
38
+ expected_credentials=$(cygpath -m "$expected_credentials")
39
+ fi
40
+ state="$service_dir/state.json"
41
+ jq -n \
42
+ --arg service_dir "$service_dir" \
43
+ --arg credentials "$credentials" \
44
+ '{
45
+ run_id: "mcp-doctor-test",
46
+ region: "ap-northeast-1",
47
+ domain_mode: "user",
48
+ domain: "mcp-check.example.test",
49
+ agent_service_id: "mcp-check.example.test",
50
+ agent_service_dir: $service_dir,
51
+ agent_credentials_file: $credentials,
52
+ mcp_credentials_file: $credentials,
53
+ mcp_command: "direxio-mcp",
54
+ phase: "S7_VERIFY_E2E",
55
+ phases: {
56
+ S0_PREREQ_AWS: {status: "done"},
57
+ S1_PREFLIGHT: {status: "done"},
58
+ S2_DOMAIN: {status: "done"},
59
+ S3_PROVISION: {status: "done"},
60
+ S4_BOOTSTRAP_STACK: {status: "done"},
61
+ S5_INIT_TOKENS: {status: "done"},
62
+ S6_WIRE_LOCAL: {status: "done"},
63
+ S7_VERIFY_E2E: {status: "done"}
64
+ },
65
+ resources: {}
66
+ }' > "$state"
67
+
68
+ verify_output=$(P2P_WORKDIR="$service_dir" PATH="$fakebin:$PATH" EXPECTED_CREDENTIALS_FILE="$expected_credentials" bash "$ROOT/scripts/orchestrate.sh" verify mcp_doctor)
69
+ printf '%s\n' "$verify_output" | grep -q 'verified runtime check: mcp_doctor'
70
+
71
+ jq -e '
72
+ .runtime_checks.mcp_doctor.status == "passed"
73
+ and .runtime_checks.mcp_doctor.domain == "mcp-check.example.test"
74
+ and .runtime_checks.mcp_doctor.agent_room_id == "!agent:mcp-check.example.test"
75
+ and .runtime_checks.mcp_doctor.token == "redacted"
76
+ and (.user_confirmations.agent_mcp_runtime | not)
77
+ ' "$state" >/dev/null
78
+
79
+ report_output=$(P2P_WORKDIR="$service_dir" bash "$ROOT/scripts/orchestrate.sh" report new_deploy)
80
+ report_path=$(printf '%s\n' "$report_output" | sed -nE 's/^operation report: //p' | tail -n 1)
81
+ jq -e '
82
+ .runtime_checks.mcp_doctor.status == "passed"
83
+ and .gates.user_confirmation.agent_mcp_runtime == "pending_runtime_confirmation"
84
+ ' "$report_path" >/dev/null
85
+
86
+ echo "mcp doctor runtime check ok"
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT=$(cd "$(dirname "$0")/.." && pwd)
5
+ tmp=$(mktemp -d)
6
+ trap 'rm -rf "$tmp"' EXIT
7
+
8
+ export HOME="$tmp/home"
9
+ mkdir -p "$HOME"
10
+
11
+ fakebin="$tmp/bin"
12
+ mkdir -p "$fakebin"
13
+ cat > "$fakebin/curl" <<'EOF'
14
+ #!/usr/bin/env bash
15
+ set -euo pipefail
16
+
17
+ printf '%s\n' "$*" >> "$CURL_CALLS"
18
+
19
+ want_url="https://mcp-smoke.example.test/_p2p/query"
20
+ case " $* " in
21
+ *" $want_url "*|*" $want_url")
22
+ ;;
23
+ *)
24
+ echo "unexpected curl URL: $*" >&2
25
+ exit 1
26
+ ;;
27
+ esac
28
+
29
+ case " $* " in
30
+ *"Authorization: Bearer AGENT_TOKEN_SMOKE"*) ;;
31
+ *)
32
+ echo "missing or wrong Authorization header: $*" >&2
33
+ exit 1
34
+ ;;
35
+ esac
36
+
37
+ case " $* " in
38
+ *'"action":"mcp.messages.list"'*'"room_id":"!agent:mcp-smoke.example.test"'*) ;;
39
+ *)
40
+ echo "wrong smoke request body: $*" >&2
41
+ exit 1
42
+ ;;
43
+ esac
44
+
45
+ body_path=""
46
+ write_code=0
47
+ while [ "$#" -gt 0 ]; do
48
+ case "$1" in
49
+ -o)
50
+ body_path=$2
51
+ shift 2
52
+ ;;
53
+ -w)
54
+ write_code=1
55
+ shift 2
56
+ ;;
57
+ *)
58
+ shift
59
+ ;;
60
+ esac
61
+ done
62
+
63
+ payload='{"room_id":"!agent:mcp-smoke.example.test","messages":[]}'
64
+ if [ -n "$body_path" ]; then
65
+ printf '%s\n' "$payload" > "$body_path"
66
+ else
67
+ printf '%s\n' "$payload"
68
+ fi
69
+ [ "$write_code" -eq 1 ] && printf '200'
70
+ EOF
71
+ chmod 700 "$fakebin/curl"
72
+
73
+ service_dir="$HOME/.direxio/nodes/mcp-smoke.example.test"
74
+ mkdir -p "$service_dir"
75
+ state="$service_dir/state.json"
76
+ jq -n \
77
+ --arg service_dir "$service_dir" \
78
+ '{
79
+ run_id: "mcp-smoke-test",
80
+ region: "ap-northeast-1",
81
+ domain_mode: "user",
82
+ domain: "mcp-smoke.example.test",
83
+ as_url: "https://mcp-smoke.example.test",
84
+ agent_service_id: "mcp-smoke.example.test",
85
+ agent_service_dir: $service_dir,
86
+ agent_token: "AGENT_TOKEN_SMOKE",
87
+ agent_room_id: "!agent:mcp-smoke.example.test",
88
+ phase: "S7_VERIFY_E2E",
89
+ phases: {
90
+ S0_PREREQ_AWS: {status: "done"},
91
+ S1_PREFLIGHT: {status: "done"},
92
+ S2_DOMAIN: {status: "done"},
93
+ S3_PROVISION: {status: "done"},
94
+ S4_BOOTSTRAP_STACK: {status: "done"},
95
+ S5_INIT_TOKENS: {status: "done"},
96
+ S6_WIRE_LOCAL: {status: "done"},
97
+ S7_VERIFY_E2E: {status: "done"}
98
+ },
99
+ resources: {}
100
+ }' > "$state"
101
+
102
+ calls="$tmp/curl.calls"
103
+ verify_output=$(P2P_WORKDIR="$service_dir" PATH="$fakebin:$PATH" CURL_CALLS="$calls" bash "$ROOT/scripts/orchestrate.sh" verify mcp_smoke)
104
+ printf '%s\n' "$verify_output" | grep -q 'verified runtime check: mcp_smoke'
105
+
106
+ jq -e '
107
+ .runtime_checks.mcp_smoke.status == "passed"
108
+ and .runtime_checks.mcp_smoke.action == "mcp.messages.list"
109
+ and .runtime_checks.mcp_smoke.room_id == "!agent:mcp-smoke.example.test"
110
+ and .runtime_checks.mcp_smoke.response_messages_type == "array"
111
+ and (.user_confirmations.agent_mcp_runtime | not)
112
+ ' "$state" >/dev/null
113
+
114
+ report_output=$(P2P_WORKDIR="$service_dir" bash "$ROOT/scripts/orchestrate.sh" report new_deploy)
115
+ report_path=$(printf '%s\n' "$report_output" | sed -nE 's/^operation report: //p' | tail -n 1)
116
+ jq -e '
117
+ .runtime_checks.mcp_smoke.status == "passed"
118
+ and .gates.user_confirmation.agent_mcp_runtime == "pending_runtime_confirmation"
119
+ ' "$report_path" >/dev/null
120
+
121
+ echo "mcp smoke runtime check ok"
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT=$(cd "$(dirname "$0")/.." && pwd)
5
+ tmp=$(mktemp -d "$ROOT/.tmp-mcp-tools.XXXXXX")
6
+ trap 'rm -rf "$tmp"' EXIT
7
+
8
+ export HOME="$tmp/home"
9
+ mkdir -p "$HOME"
10
+
11
+ fakebin="$tmp/bin"
12
+ mkdir -p "$fakebin"
13
+
14
+ windows_path() {
15
+ local path=$1 drive rest
16
+ case "$path" in
17
+ /mnt/[A-Za-z]/*)
18
+ drive=${path#/mnt/}
19
+ drive=${drive%%/*}
20
+ rest=${path#/mnt/$drive/}
21
+ printf '%s:\\%s\n' "$(printf '%s' "$drive" | tr '[:lower:]' '[:upper:]')" "$(printf '%s' "$rest" | sed 's#/#\\#g')"
22
+ ;;
23
+ /[A-Za-z]/*)
24
+ drive=${path#/}
25
+ drive=${drive%%/*}
26
+ rest=${path#/$drive/}
27
+ printf '%s:\\%s\n' "$(printf '%s' "$drive" | tr '[:lower:]' '[:upper:]')" "$(printf '%s' "$rest" | sed 's#/#\\#g')"
28
+ ;;
29
+ *) printf '%s\n' "$path" ;;
30
+ esac
31
+ }
32
+
33
+ cat > "$fakebin/direxio-mcp" <<'EOF'
34
+ #!/usr/bin/env bash
35
+ set -euo pipefail
36
+ if [ "${DIREXIO_CREDENTIALS_FILE:-}" != "${EXPECTED_CREDENTIALS_FILE:-}" ]; then
37
+ echo "wrong DIREXIO_CREDENTIALS_FILE" >&2
38
+ exit 1
39
+ fi
40
+
41
+ printf '%s\n' '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{"tools":{}},"serverInfo":{"name":"fake-direxio-mcp","version":"0.0.0"}}}'
42
+ printf '%s\n' '{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"search_rooms","description":"Search rooms"},{"name":"send_message","description":"Send message"},{"name":"list_messages","description":"List messages"}]}}'
43
+ EOF
44
+ chmod 700 "$fakebin/direxio-mcp"
45
+
46
+ cat > "$tmp/fake-mcp.ps1" <<'EOF'
47
+ if ($env:DIREXIO_CREDENTIALS_FILE -ne $env:EXPECTED_CREDENTIALS_FILE) {
48
+ [Console]::Error.WriteLine("wrong DIREXIO_CREDENTIALS_FILE")
49
+ exit 1
50
+ }
51
+
52
+ [Console]::Out.WriteLine('{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{"tools":{}},"serverInfo":{"name":"fake-direxio-mcp","version":"0.0.0"}}}')
53
+ [Console]::Out.WriteLine('{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"search_rooms","description":"Search rooms"},{"name":"send_message","description":"Send message"},{"name":"list_messages","description":"List messages"}]}}')
54
+ EOF
55
+
56
+ mcp_command=direxio-mcp
57
+ case "$(uname -s)" in
58
+ MINGW*|MSYS*|CYGWIN*) use_windows_mcp=1 ;;
59
+ *) use_windows_mcp=0 ;;
60
+ esac
61
+ if { [ "$use_windows_mcp" = "1" ] || ! command -v node >/dev/null 2>&1; } && command -v node.exe >/dev/null 2>&1; then
62
+ fake_mcp_ps1=$(windows_path "$tmp/fake-mcp.ps1")
63
+ mcp_command="powershell.exe -NoProfile -ExecutionPolicy Bypass -File \"$fake_mcp_ps1\""
64
+ fi
65
+
66
+ service_dir="$HOME/.direxio/nodes/mcp-tools.example.test"
67
+ mkdir -p "$service_dir"
68
+ credentials="$service_dir/credentials.json"
69
+ : > "$credentials"
70
+ expected_credentials="$credentials"
71
+ if command -v cygpath >/dev/null 2>&1; then
72
+ expected_credentials=$(cygpath -m "$expected_credentials")
73
+ fi
74
+ state="$service_dir/state.json"
75
+ jq -n \
76
+ --arg service_dir "$service_dir" \
77
+ --arg credentials "$credentials" \
78
+ --arg mcp_command "$mcp_command" \
79
+ '{
80
+ run_id: "mcp-tools-test",
81
+ region: "ap-northeast-1",
82
+ domain_mode: "user",
83
+ domain: "mcp-tools.example.test",
84
+ agent_service_id: "mcp-tools.example.test",
85
+ agent_service_dir: $service_dir,
86
+ agent_credentials_file: $credentials,
87
+ mcp_credentials_file: $credentials,
88
+ mcp_command: $mcp_command,
89
+ phase: "S7_VERIFY_E2E",
90
+ phases: {
91
+ S0_PREREQ_AWS: {status: "done"},
92
+ S1_PREFLIGHT: {status: "done"},
93
+ S2_DOMAIN: {status: "done"},
94
+ S3_PROVISION: {status: "done"},
95
+ S4_BOOTSTRAP_STACK: {status: "done"},
96
+ S5_INIT_TOKENS: {status: "done"},
97
+ S6_WIRE_LOCAL: {status: "done"},
98
+ S7_VERIFY_E2E: {status: "done"}
99
+ },
100
+ resources: {}
101
+ }' > "$state"
102
+
103
+ verify_output=$(P2P_WORKDIR="$service_dir" PATH="$fakebin:$PATH" EXPECTED_CREDENTIALS_FILE="$expected_credentials" bash "$ROOT/scripts/orchestrate.sh" verify mcp_tools)
104
+ printf '%s\n' "$verify_output" | grep -q 'verified runtime check: mcp_tools'
105
+
106
+ jq -e '
107
+ .runtime_checks.mcp_tools.status == "passed"
108
+ and .runtime_checks.mcp_tools.tool_count == 3
109
+ and (.runtime_checks.mcp_tools.tools | index("search_rooms") != null)
110
+ and (.runtime_checks.mcp_tools.tools | index("send_message") != null)
111
+ and (.runtime_checks.mcp_tools.tools | index("list_messages") != null)
112
+ and (.user_confirmations.agent_mcp_runtime | not)
113
+ ' "$state" >/dev/null
114
+
115
+ report_output=$(P2P_WORKDIR="$service_dir" bash "$ROOT/scripts/orchestrate.sh" report new_deploy)
116
+ report_path=$(printf '%s\n' "$report_output" | sed -nE 's/^operation report: //p' | tail -n 1)
117
+ jq -e '
118
+ .runtime_checks.mcp_tools.status == "passed"
119
+ and .runtime_checks.mcp_tools.tool_count == 3
120
+ and .gates.user_confirmation.agent_mcp_runtime == "pending_runtime_confirmation"
121
+ ' "$report_path" >/dev/null
122
+
123
+ echo "mcp tools runtime check ok"
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT=$(cd "$(dirname "$0")/.." && pwd)
5
+ cd "$ROOT"
6
+
7
+ assert_file_exists() {
8
+ [ -f "$1" ] || {
9
+ echo "missing expected file: $1" >&2
10
+ exit 1
11
+ }
12
+ }
13
+
14
+ assert_contains() {
15
+ local path=$1 pattern=$2
16
+ grep -q "$pattern" "$path" || {
17
+ echo "expected $path to contain: $pattern" >&2
18
+ exit 1
19
+ }
20
+ }
21
+
22
+ tmp=$(mktemp -d)
23
+ trap 'rm -rf "$tmp"' EXIT
24
+
25
+ NODE_BIN=${NODE:-node}
26
+
27
+ "$NODE_BIN" -e '
28
+ const pkg = require("./package.json");
29
+ if (pkg.name !== "direxio-deployer") throw new Error("unexpected package name");
30
+ if (!pkg.bin || pkg.bin["direxio-deployer"] !== "bin/direxio-deployer.mjs") {
31
+ throw new Error("missing direxio-deployer bin");
32
+ }
33
+ '
34
+
35
+ project="$tmp/project"
36
+ mkdir -p "$project"
37
+
38
+ "$NODE_BIN" bin/direxio-deployer.mjs skill install --agent codex --scope project --project "$project" > "$tmp/install.out"
39
+ target="$project/.codex/skills/direxio-deployer"
40
+ assert_file_exists "$target/SKILL.md"
41
+ assert_file_exists "$target/references/agent-targets.md"
42
+ assert_file_exists "$target/scripts/orchestrate.sh"
43
+ assert_file_exists "$target/.direxio-skill-install.json"
44
+ assert_contains "$target/.direxio-skill-install.json" '"agent": "codex"'
45
+ assert_contains "$target/.direxio-skill-install.json" '"scope": "project"'
46
+
47
+ printf 'stale\n' > "$target/STALE.txt"
48
+ "$NODE_BIN" bin/direxio-deployer.mjs skill update --agent codex --scope project --project "$project" > "$tmp/update.out"
49
+ if [ -f "$target/STALE.txt" ]; then
50
+ echo "managed update should replace stale target contents" >&2
51
+ exit 1
52
+ fi
53
+
54
+ unmanaged_project="$tmp/unmanaged"
55
+ mkdir -p "$unmanaged_project/.codex/skills/direxio-deployer"
56
+ printf 'manual\n' > "$unmanaged_project/.codex/skills/direxio-deployer/manual.txt"
57
+ if "$NODE_BIN" bin/direxio-deployer.mjs skill install --agent codex --scope project --project "$unmanaged_project" >"$tmp/unmanaged.out" 2>"$tmp/unmanaged.err"; then
58
+ echo "unmanaged install should require --force" >&2
59
+ exit 1
60
+ fi
61
+ assert_contains "$tmp/unmanaged.err" 'refusing to overwrite unmanaged target'
62
+
63
+ "$NODE_BIN" bin/direxio-deployer.mjs skill install --agent codex --scope project --project "$unmanaged_project" --force > "$tmp/force.out"
64
+ assert_file_exists "$unmanaged_project/.codex/skills/direxio-deployer/SKILL.md"
65
+ if [ -f "$unmanaged_project/.codex/skills/direxio-deployer/manual.txt" ]; then
66
+ echo "forced install should replace unmanaged contents" >&2
67
+ exit 1
68
+ fi
69
+
70
+ "$NODE_BIN" bin/direxio-deployer.mjs skill install --agent gemini --scope global --home "$tmp/home" --dry-run > "$tmp/dry-run.out"
71
+ assert_contains "$tmp/dry-run.out" '"dryRun": true'
72
+ assert_contains "$tmp/dry-run.out" '.gemini'
73
+ if [ -e "$tmp/home/.gemini" ]; then
74
+ echo "dry-run should not create global target directories" >&2
75
+ exit 1
76
+ fi
77
+
78
+ PI_CODING_AGENT_DIR="$tmp/pi-agent-root" "$NODE_BIN" bin/direxio-deployer.mjs skill install --agent pi --scope global --dry-run > "$tmp/pi-global.out"
79
+ assert_contains "$tmp/pi-global.out" 'pi-agent-root'
80
+ assert_contains "$tmp/pi-global.out" 'skills'
81
+ if grep -q 'pi-agent-root.*/agent/skills' "$tmp/pi-global.out"; then
82
+ echo "PI_CODING_AGENT_DIR already points at the agent root and must not append another agent segment" >&2
83
+ exit 1
84
+ fi
85
+
86
+ custom_target="$tmp/custom target/skill"
87
+ "$NODE_BIN" bin/direxio-deployer.mjs skill install --agent codex --target "$custom_target" > "$tmp/custom-target.out"
88
+ assert_file_exists "$custom_target/SKILL.md"
89
+ assert_file_exists "$custom_target/.direxio-skill-install.json"
90
+
91
+ "$NODE_BIN" bin/direxio-deployer.mjs skill refresh --agent codex --scope project --project "$project" --dry-run > "$tmp/refresh.out"
92
+ assert_contains "$tmp/refresh.out" '"command": "refresh"'
93
+ assert_contains "$tmp/refresh.out" '"target"'
94
+
95
+ echo "npm skill distribution ok"