@imdeadpool/guardex 7.0.14 → 7.0.15

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imdeadpool/guardex",
3
- "version": "7.0.14",
3
+ "version": "7.0.15",
4
4
  "description": "GitGuardex: hardened multi-agent git guardrails for parallel agent work.",
5
5
  "license": "MIT",
6
6
  "preferGlobal": true,
@@ -60,7 +60,7 @@
60
60
  "bugs": {
61
61
  "url": "https://github.com/recodeee/gitguardex/issues"
62
62
  },
63
- "homepage": "https://guardextutorial.com",
63
+ "homepage": "https://github.com/recodeee/gitguardex-frontend",
64
64
  "funding": "https://github.com/sponsors/recodeecom",
65
65
  "publishConfig": {
66
66
  "access": "public"
@@ -144,24 +144,44 @@ else
144
144
  common_git_dir="$(cd "$repo_root/$common_git_dir_raw" && pwd -P)"
145
145
  fi
146
146
  repo_common_root="$(cd "$common_git_dir/.." && pwd -P)"
147
- agent_worktree_root="${repo_common_root}/.omx/agent-worktrees"
148
147
 
149
148
  if [[ -z "$SOURCE_BRANCH" ]]; then
150
149
  SOURCE_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
151
150
  fi
152
151
 
152
+ stored_worktree_root_rel="$(git -C "$repo_root" config --get "branch.${SOURCE_BRANCH}.guardexWorktreeRoot" || true)"
153
+ if [[ -z "$stored_worktree_root_rel" ]]; then
154
+ stored_worktree_root_rel=".omx/agent-worktrees"
155
+ fi
156
+ agent_worktree_root="${repo_common_root}/${stored_worktree_root_rel}"
157
+
153
158
  if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 && -z "$BASE_BRANCH" ]]; then
154
159
  echo "[agent-branch-finish] --base requires a non-empty branch name." >&2
155
160
  exit 1
156
161
  fi
157
162
 
158
163
  if [[ "$BASE_BRANCH_EXPLICIT" -eq 0 ]]; then
159
- configured_base="$(git -C "$repo_root" config --get multiagent.baseBranch || true)"
160
- if [[ -n "$configured_base" ]]; then
161
- BASE_BRANCH="$configured_base"
164
+ source_branch_base="$(git -C "$repo_root" config --get "branch.${SOURCE_BRANCH}.guardexBase" || true)"
165
+ if [[ -n "$source_branch_base" ]]; then
166
+ BASE_BRANCH="$source_branch_base"
167
+ else
168
+ configured_base="$(git -C "$repo_root" config --get multiagent.baseBranch || true)"
169
+ if [[ -n "$configured_base" ]]; then
170
+ BASE_BRANCH="$configured_base"
171
+ fi
162
172
  fi
163
173
  fi
164
174
 
175
+ if [[ -z "$BASE_BRANCH" ]]; then
176
+ for fallback_branch in dev main master; do
177
+ if git -C "$repo_root" show-ref --verify --quiet "refs/heads/${fallback_branch}" \
178
+ || git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/${fallback_branch}"; then
179
+ BASE_BRANCH="$fallback_branch"
180
+ break
181
+ fi
182
+ done
183
+ fi
184
+
165
185
  if [[ -z "$BASE_BRANCH" ]]; then
166
186
  BASE_BRANCH="dev"
167
187
  fi
@@ -268,8 +288,17 @@ if [[ "$should_require_sync" -eq 1 ]] && git -C "$repo_root" show-ref --verify -
268
288
  fi
269
289
  fi
270
290
 
271
- integration_worktree="${agent_worktree_root}/__integrate-${BASE_BRANCH//\//__}-$(date +%Y%m%d-%H%M%S)"
272
- integration_branch="__agent_integrate_${BASE_BRANCH//\//_}_$(date +%Y%m%d_%H%M%S)"
291
+ integration_stamp="$(date +%Y%m%d-%H%M%S)"
292
+ integration_worktree_base="${agent_worktree_root}/__integrate-${BASE_BRANCH//\//__}-${integration_stamp}"
293
+ integration_branch_base="__agent_integrate_${BASE_BRANCH//\//_}_$(date +%Y%m%d_%H%M%S)"
294
+ integration_worktree="$integration_worktree_base"
295
+ integration_branch="$integration_branch_base"
296
+ integration_suffix=1
297
+ while [[ -e "$integration_worktree" ]] || git -C "$repo_root" show-ref --verify --quiet "refs/heads/${integration_branch}"; do
298
+ integration_worktree="${integration_worktree_base}-${integration_suffix}"
299
+ integration_branch="${integration_branch_base}_${integration_suffix}"
300
+ integration_suffix=$((integration_suffix + 1))
301
+ done
273
302
  mkdir -p "$(dirname "$integration_worktree")"
274
303
 
275
304
  git -C "$repo_root" worktree add "$integration_worktree" "$start_ref" >/dev/null
@@ -5,7 +5,8 @@ TASK_NAME="task"
5
5
  AGENT_NAME="agent"
6
6
  BASE_BRANCH=""
7
7
  BASE_BRANCH_EXPLICIT=0
8
- WORKTREE_ROOT_REL=".omx/agent-worktrees"
8
+ WORKTREE_ROOT_REL=""
9
+ WORKTREE_ROOT_EXPLICIT=0
9
10
  OPENSPEC_AUTO_INIT_RAW="${GUARDEX_OPENSPEC_AUTO_INIT:-false}"
10
11
  OPENSPEC_PLAN_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_PLAN_SLUG:-}"
11
12
  OPENSPEC_CHANGE_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_CHANGE_SLUG:-}"
@@ -44,6 +45,7 @@ while [[ $# -gt 0 ]]; do
44
45
  ;;
45
46
  --worktree-root)
46
47
  WORKTREE_ROOT_REL="${2:-.omx/agent-worktrees}"
48
+ WORKTREE_ROOT_EXPLICIT=1
47
49
  shift 2
48
50
  ;;
49
51
  --)
@@ -123,12 +125,41 @@ shorten_slug() {
123
125
  printf '%s' "$shortened"
124
126
  }
125
127
 
126
- # Collapse arbitrary agent identifiers to a clean role token: claude | codex |
127
- # <other-kebab>. Priority: GUARDEX_AGENT_TYPE env override, then the raw
128
- # AGENT_NAME (if it contains 'claude' or 'codex'), then CLAUDECODE=1 sentinel
129
- # (set by Claude Code CLI), else fall back to 'codex'. Any other role name
130
- # (integrator, executor, rust-port, etc.) is preserved as-is after slug
131
- # sanitization.
128
+ env_flag_truthy() {
129
+ local raw="${1:-}"
130
+ local lowered
131
+ lowered="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]')"
132
+ case "$lowered" in
133
+ 1|true|yes|on) return 0 ;;
134
+ *) return 1 ;;
135
+ esac
136
+ }
137
+
138
+ default_worktree_root_rel() {
139
+ local raw_agent="$1"
140
+ local override="${GUARDEX_AGENT_TYPE:-}"
141
+ local lowered_agent lowered_override
142
+ lowered_agent="$(printf '%s' "$raw_agent" | tr '[:upper:]' '[:lower:]')"
143
+ lowered_override="$(printf '%s' "$override" | tr '[:upper:]' '[:lower:]')"
144
+
145
+ if [[ -n "${CLAUDE_CODE_SESSION_ID:-}" ]] || env_flag_truthy "${CLAUDECODE:-}"; then
146
+ printf '.omc/agent-worktrees'
147
+ return 0
148
+ fi
149
+
150
+ if [[ "$lowered_agent" == *claude* ]] || [[ "$lowered_override" == *claude* ]]; then
151
+ printf '.omc/agent-worktrees'
152
+ return 0
153
+ fi
154
+
155
+ printf '.omx/agent-worktrees'
156
+ }
157
+
158
+ # Collapse arbitrary agent identifiers to a clean role token. Priority:
159
+ # GUARDEX_AGENT_TYPE env override, then recognizable claude/codex aliases, then
160
+ # a small legacy compatibility set, then the literal requested role after slug
161
+ # sanitization. This preserves explicit roles such as planner/executor while
162
+ # keeping the older bot -> codex fallback stable for existing callers.
132
163
  normalize_role() {
133
164
  local raw_agent="$1"
134
165
  local override="${GUARDEX_AGENT_TYPE:-}"
@@ -150,10 +181,13 @@ normalize_role() {
150
181
  printf 'claude'
151
182
  return 0
152
183
  fi
153
- # Unrecognized raw name (rust-port-lead, some-worker, empty, ...): default to
154
- # codex. To get a different role (integrator, executor, ...) pass the role
155
- # explicitly via GUARDEX_AGENT_TYPE, handled above.
156
- printf 'codex'
184
+ local sanitized
185
+ sanitized="$(sanitize_slug "$raw_agent" "codex")"
186
+ if [[ "$sanitized" == "bot" ]]; then
187
+ printf 'codex'
188
+ return 0
189
+ fi
190
+ printf '%s' "$sanitized"
157
191
  }
158
192
 
159
193
  # Timestamp the branch/worktree/openspec slug so parallel agents never collide
@@ -413,6 +447,9 @@ fi
413
447
 
414
448
  task_slug="$(sanitize_slug "$TASK_NAME" "task")"
415
449
  agent_slug="$(normalize_role "$AGENT_NAME")"
450
+ if [[ "$WORKTREE_ROOT_EXPLICIT" -eq 0 ]]; then
451
+ WORKTREE_ROOT_REL="$(default_worktree_root_rel "$AGENT_NAME")"
452
+ fi
416
453
  branch_timestamp="$(compose_branch_timestamp)"
417
454
  branch_descriptor="$(compose_branch_descriptor "$task_slug" "$branch_timestamp")"
418
455
  branch_name_base="agent/${agent_slug}/${branch_descriptor}"
@@ -497,6 +534,7 @@ if ! worktree_add_output="$(git -C "$repo_root" worktree add -b "$branch_name" "
497
534
  exit 1
498
535
  fi
499
536
  git -C "$repo_root" config "branch.${branch_name}.guardexBase" "$BASE_BRANCH" >/dev/null 2>&1 || true
537
+ git -C "$repo_root" config "branch.${branch_name}.guardexWorktreeRoot" "$WORKTREE_ROOT_REL" >/dev/null 2>&1 || true
500
538
  # Fresh agent branches should start unpublished; clear any inherited base-branch tracking.
501
539
  git -C "$worktree_path" branch --unset-upstream "$branch_name" >/dev/null 2>&1 || true
502
540
 
@@ -533,4 +571,4 @@ echo "[agent-branch-start] Next steps:"
533
571
  echo " cd \"${worktree_path}\""
534
572
  echo " python3 scripts/agent-file-locks.py claim --branch \"${branch_name}\" <file...>"
535
573
  echo " # implement + commit"
536
- echo " bash scripts/agent-branch-finish.sh --branch \"${branch_name}\" --base dev --via-pr --wait-for-merge"
574
+ echo " bash scripts/agent-branch-finish.sh --branch \"${branch_name}\" --base ${BASE_BRANCH} --via-pr --wait-for-merge"
@@ -18,6 +18,10 @@ GH_BIN="${GUARDEX_GH_BIN:-gh}"
18
18
  PR_MERGED_LOOKUP_DISABLED=0
19
19
  PR_MERGED_LOOKUP_LOADED=0
20
20
  declare -A MERGED_PR_BRANCHES=()
21
+ WORKTREE_ROOT_RELS=(
22
+ ".omx/agent-worktrees"
23
+ ".omc/agent-worktrees"
24
+ )
21
25
 
22
26
  if [[ -n "$BASE_BRANCH" ]]; then
23
27
  BASE_BRANCH_EXPLICIT=1
@@ -77,13 +81,36 @@ fi
77
81
 
78
82
  repo_root="$(git rev-parse --show-toplevel)"
79
83
  current_pwd="$(pwd -P)"
80
- worktree_root="${repo_root}/.omx/agent-worktrees"
81
84
  repo_common_dir="$(
82
85
  git -C "$repo_root" rev-parse --git-common-dir \
83
86
  | awk -v root="$repo_root" '{ if ($0 ~ /^\//) { print $0 } else { print root "/" $0 } }'
84
87
  )"
85
88
  repo_common_dir="$(cd "$repo_common_dir" && pwd -P)"
86
89
 
90
+ resolve_worktree_root_rel_for_entry() {
91
+ local entry="$1"
92
+ case "$entry" in
93
+ */.omc/agent-worktrees/*)
94
+ printf '%s' '.omc/agent-worktrees'
95
+ ;;
96
+ *)
97
+ printf '%s' '.omx/agent-worktrees'
98
+ ;;
99
+ esac
100
+ }
101
+
102
+ is_managed_worktree_path() {
103
+ local entry="$1"
104
+ local rel root
105
+ for rel in "${WORKTREE_ROOT_RELS[@]}"; do
106
+ root="${repo_root}/${rel}"
107
+ if [[ "$entry" == "${root}"/* ]]; then
108
+ return 0
109
+ fi
110
+ done
111
+ return 1
112
+ }
113
+
87
114
  resolve_base_branch() {
88
115
  local configured=""
89
116
  local current=""
@@ -308,54 +335,59 @@ relocated_foreign=0
308
335
  skipped_foreign=0
309
336
 
310
337
  relocate_foreign_worktree_entries() {
311
- [[ -d "$worktree_root" ]] || return 0
312
-
313
- local entry=""
314
- for entry in "${worktree_root}"/*; do
315
- [[ -d "$entry" ]] || continue
316
- if ! git -C "$entry" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
317
- continue
318
- fi
319
-
320
- local entry_common_dir=""
321
- entry_common_dir="$(resolve_worktree_common_dir "$entry" || true)"
322
- [[ -n "$entry_common_dir" ]] || continue
338
+ local rel="" worktree_root="" entry=""
339
+ for rel in "${WORKTREE_ROOT_RELS[@]}"; do
340
+ worktree_root="${repo_root}/${rel}"
341
+ [[ -d "$worktree_root" ]] || continue
342
+
343
+ for entry in "${worktree_root}"/*; do
344
+ [[ -d "$entry" ]] || continue
345
+ if ! git -C "$entry" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
346
+ continue
347
+ fi
323
348
 
324
- if [[ "$entry_common_dir" == "$repo_common_dir" ]]; then
325
- continue
326
- fi
349
+ local entry_common_dir=""
350
+ entry_common_dir="$(resolve_worktree_common_dir "$entry" || true)"
351
+ [[ -n "$entry_common_dir" ]] || continue
327
352
 
328
- if [[ "$(basename "$entry_common_dir")" != ".git" ]]; then
329
- skipped_foreign=$((skipped_foreign + 1))
330
- echo "[agent-worktree-prune] Skipping foreign worktree with unsupported git common dir: ${entry}"
331
- continue
332
- fi
353
+ if [[ "$entry_common_dir" == "$repo_common_dir" ]]; then
354
+ continue
355
+ fi
333
356
 
334
- local owner_repo_root
335
- owner_repo_root="$(dirname "$entry_common_dir")"
336
- local owner_worktree_root="${owner_repo_root}/.omx/agent-worktrees"
337
- local target_path
338
- target_path="$(select_unique_worktree_path "$owner_worktree_root" "$(basename "$entry")")"
357
+ if [[ "$(basename "$entry_common_dir")" != ".git" ]]; then
358
+ skipped_foreign=$((skipped_foreign + 1))
359
+ echo "[agent-worktree-prune] Skipping foreign worktree with unsupported git common dir: ${entry}"
360
+ continue
361
+ fi
339
362
 
340
- if [[ "$entry" == "$current_pwd" || "$current_pwd" == "${entry}"/* ]]; then
341
- skipped_foreign=$((skipped_foreign + 1))
342
- echo "[agent-worktree-prune] Skipping active foreign worktree: ${entry}"
343
- continue
344
- fi
363
+ local owner_repo_root
364
+ owner_repo_root="$(dirname "$entry_common_dir")"
365
+ local owner_worktree_root_rel owner_worktree_root
366
+ owner_worktree_root_rel="$(resolve_worktree_root_rel_for_entry "$entry")"
367
+ owner_worktree_root="${owner_repo_root}/${owner_worktree_root_rel}"
368
+ local target_path
369
+ target_path="$(select_unique_worktree_path "$owner_worktree_root" "$(basename "$entry")")"
370
+
371
+ if [[ "$entry" == "$current_pwd" || "$current_pwd" == "${entry}"/* ]]; then
372
+ skipped_foreign=$((skipped_foreign + 1))
373
+ echo "[agent-worktree-prune] Skipping active foreign worktree: ${entry}"
374
+ continue
375
+ fi
345
376
 
346
- echo "[agent-worktree-prune] Relocating foreign worktree to owning repo: ${entry} -> ${target_path}"
347
- if [[ "$DRY_RUN" -eq 1 ]]; then
348
- relocated_foreign=$((relocated_foreign + 1))
349
- continue
350
- fi
377
+ echo "[agent-worktree-prune] Relocating foreign worktree to owning repo: ${entry} -> ${target_path}"
378
+ if [[ "$DRY_RUN" -eq 1 ]]; then
379
+ relocated_foreign=$((relocated_foreign + 1))
380
+ continue
381
+ fi
351
382
 
352
- mkdir -p "$owner_worktree_root"
353
- if git -C "$owner_repo_root" worktree move "$entry" "$target_path" >/dev/null 2>&1; then
354
- relocated_foreign=$((relocated_foreign + 1))
355
- else
356
- skipped_foreign=$((skipped_foreign + 1))
357
- echo "[agent-worktree-prune] Failed to relocate foreign worktree: ${entry}" >&2
358
- fi
383
+ mkdir -p "$owner_worktree_root"
384
+ if git -C "$owner_repo_root" worktree move "$entry" "$target_path" >/dev/null 2>&1; then
385
+ relocated_foreign=$((relocated_foreign + 1))
386
+ else
387
+ skipped_foreign=$((skipped_foreign + 1))
388
+ echo "[agent-worktree-prune] Failed to relocate foreign worktree: ${entry}" >&2
389
+ fi
390
+ done
359
391
  done
360
392
  }
361
393
 
@@ -371,7 +403,9 @@ process_entry() {
371
403
  local branch_ref="$2"
372
404
 
373
405
  [[ -z "$wt" ]] && return
374
- [[ "$wt" != "${worktree_root}"/* ]] && return
406
+ if ! is_managed_worktree_path "$wt"; then
407
+ return
408
+ fi
375
409
 
376
410
  local branch=""
377
411
  if [[ -n "$branch_ref" ]]; then
@@ -249,6 +249,35 @@ resolve_start_ref() {
249
249
  return 1
250
250
  }
251
251
 
252
+ origin_remote_looks_like_github() {
253
+ local wt="$1"
254
+ local origin_url=""
255
+ origin_url="$(git -C "$wt" remote get-url origin 2>/dev/null || true)"
256
+ [[ -n "$origin_url" && "$origin_url" =~ github\.com[:/] ]]
257
+ }
258
+
259
+ auto_finish_context_is_ready() {
260
+ local wt="$1"
261
+ local gh_bin="${GUARDEX_GH_BIN:-gh}"
262
+
263
+ if ! git -C "$wt" remote get-url origin >/dev/null 2>&1; then
264
+ return 1
265
+ fi
266
+ if ! command -v "$gh_bin" >/dev/null 2>&1; then
267
+ return 1
268
+ fi
269
+
270
+ if [[ -n "${GUARDEX_GH_BIN:-}" ]]; then
271
+ return 0
272
+ fi
273
+
274
+ if ! origin_remote_looks_like_github "$wt"; then
275
+ return 1
276
+ fi
277
+
278
+ "$gh_bin" auth status >/dev/null 2>&1
279
+ }
280
+
252
281
  restore_repo_branch_if_changed() {
253
282
  local expected_branch="$1"
254
283
  if [[ -z "$expected_branch" || "$expected_branch" == "HEAD" ]]; then
@@ -372,6 +401,17 @@ has_origin_remote() {
372
401
  git -C "$repo_root" remote get-url origin >/dev/null 2>&1
373
402
  }
374
403
 
404
+ origin_remote_supports_pr_finish() {
405
+ local origin_url
406
+ origin_url="$(git -C "$repo_root" remote get-url origin 2>/dev/null || true)"
407
+ case "$origin_url" in
408
+ ''|/*|./*|../*|file://*)
409
+ return 1
410
+ ;;
411
+ esac
412
+ return 0
413
+ }
414
+
375
415
  resolve_worktree_base_branch() {
376
416
  local _wt="$1"
377
417
  if [[ "$BASE_BRANCH_EXPLICIT" -eq 1 && -n "$BASE_BRANCH" ]]; then
@@ -685,7 +725,12 @@ run_finish_flow() {
685
725
  echo "[codex-agent] Auto-finish requires GitHub CLI for PR flow; command not found: ${GUARDEX_GH_BIN:-gh}" >&2
686
726
  return 2
687
727
  fi
688
- finish_args+=(--via-pr)
728
+ if origin_remote_supports_pr_finish; then
729
+ finish_args+=(--via-pr)
730
+ else
731
+ echo "[codex-agent] Origin remote does not provide a mergeable PR surface; skipping auto-finish merge/PR pipeline." >&2
732
+ return 2
733
+ fi
689
734
  else
690
735
  echo "[codex-agent] No origin remote detected; skipping auto-finish merge/PR pipeline." >&2
691
736
  return 2
@@ -764,7 +809,9 @@ if [[ "$AUTO_FINISH" -eq 1 && -n "$worktree_branch" && "$worktree_branch" != "HE
764
809
  else
765
810
  echo "[codex-agent] Auto-finish enabled: commit -> push/PR -> merge (keep branch/worktree)."
766
811
  fi
767
- if auto_commit_worktree_changes "$worktree_path" "$worktree_branch"; then
812
+ if ! auto_finish_context_is_ready "$worktree_path"; then
813
+ echo "[codex-agent] Auto-finish skipped for '${worktree_branch}' (no mergeable remote context)." >&2
814
+ elif auto_commit_worktree_changes "$worktree_path" "$worktree_branch"; then
768
815
  if run_finish_flow "$worktree_path" "$worktree_branch"; then
769
816
  auto_finish_completed=1
770
817
  echo "[codex-agent] Auto-finish completed for '${worktree_branch}'."
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ compose_file="${GUARDEX_DOCKER_COMPOSE_FILE:-}"
6
+ service="${GUARDEX_DOCKER_SERVICE:-}"
7
+ mode="${GUARDEX_DOCKER_MODE:-auto}"
8
+ workdir_override="${GUARDEX_DOCKER_WORKDIR:-}"
9
+
10
+ usage() {
11
+ cat >&2 <<'EOF'
12
+ Usage: bash scripts/guardex-docker-loader.sh [--] <command...>
13
+
14
+ Environment:
15
+ GUARDEX_DOCKER_SERVICE=<compose-service> required unless compose defines exactly one service
16
+ GUARDEX_DOCKER_COMPOSE_FILE=<path> optional docker compose file override
17
+ GUARDEX_DOCKER_MODE=auto|exec|run default: auto
18
+ GUARDEX_DOCKER_WORKDIR=<path> optional working directory override inside the container
19
+ EOF
20
+ }
21
+
22
+ choose_compose_cmd() {
23
+ if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then
24
+ printf 'docker compose'
25
+ return 0
26
+ fi
27
+ if command -v docker-compose >/dev/null 2>&1; then
28
+ printf 'docker-compose'
29
+ return 0
30
+ fi
31
+ return 1
32
+ }
33
+
34
+ mapfile_from_lines() {
35
+ local raw="$1"
36
+ local -n out_ref="$2"
37
+ out_ref=()
38
+ while IFS= read -r line; do
39
+ [[ -n "$line" ]] || continue
40
+ out_ref+=("$line")
41
+ done <<<"$raw"
42
+ }
43
+
44
+ if [[ "${1:-}" == "--" ]]; then
45
+ shift
46
+ fi
47
+
48
+ if [[ $# -eq 0 ]]; then
49
+ usage
50
+ exit 1
51
+ fi
52
+
53
+ if [[ "$mode" != "auto" && "$mode" != "exec" && "$mode" != "run" ]]; then
54
+ echo "[guardex-docker-loader] Invalid GUARDEX_DOCKER_MODE: $mode" >&2
55
+ usage
56
+ exit 1
57
+ fi
58
+
59
+ compose_cmd_raw="$(choose_compose_cmd)" || {
60
+ echo "[guardex-docker-loader] Docker Compose is not available. Install docker compose or docker-compose first." >&2
61
+ exit 1
62
+ }
63
+ IFS=' ' read -r -a compose_cmd <<<"$compose_cmd_raw"
64
+ compose_args=()
65
+ if [[ -n "$compose_file" ]]; then
66
+ compose_args=(-f "$compose_file")
67
+ fi
68
+
69
+ cd "$repo_root"
70
+
71
+ services_raw="$("${compose_cmd[@]}" "${compose_args[@]}" config --services 2>/dev/null || true)"
72
+ declare -a services
73
+ mapfile_from_lines "$services_raw" services
74
+ if [[ ${#services[@]} -eq 0 ]]; then
75
+ echo "[guardex-docker-loader] No Docker Compose services found. Add a compose file or set GUARDEX_DOCKER_COMPOSE_FILE." >&2
76
+ exit 1
77
+ fi
78
+
79
+ if [[ -z "$service" ]]; then
80
+ if [[ ${#services[@]} -eq 1 ]]; then
81
+ service="${services[0]}"
82
+ else
83
+ echo "[guardex-docker-loader] Multiple services found (${services[*]}). Set GUARDEX_DOCKER_SERVICE." >&2
84
+ exit 1
85
+ fi
86
+ fi
87
+
88
+ service_known=0
89
+ for candidate in "${services[@]}"; do
90
+ if [[ "$candidate" == "$service" ]]; then
91
+ service_known=1
92
+ break
93
+ fi
94
+ done
95
+ if [[ $service_known -ne 1 ]]; then
96
+ echo "[guardex-docker-loader] Compose service not found: $service" >&2
97
+ exit 1
98
+ fi
99
+
100
+ run_mode="$mode"
101
+ if [[ "$run_mode" == "auto" ]]; then
102
+ run_mode="run"
103
+ running_raw="$("${compose_cmd[@]}" "${compose_args[@]}" ps --status running --services 2>/dev/null || true)"
104
+ declare -a running_services
105
+ mapfile_from_lines "$running_raw" running_services
106
+ for candidate in "${running_services[@]}"; do
107
+ if [[ "$candidate" == "$service" ]]; then
108
+ run_mode="exec"
109
+ break
110
+ fi
111
+ done
112
+ fi
113
+
114
+ workdir_args=()
115
+ if [[ -n "$workdir_override" ]]; then
116
+ workdir_args=(-w "$workdir_override")
117
+ fi
118
+
119
+ if [[ "$run_mode" == "exec" ]]; then
120
+ exec "${compose_cmd[@]}" "${compose_args[@]}" exec -T "${workdir_args[@]}" "$service" "$@"
121
+ fi
122
+
123
+ exec "${compose_cmd[@]}" "${compose_args[@]}" run --rm -T "${workdir_args[@]}" "$service" "$@"