@osfactory/har 0.62.1 → 0.63.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.
@@ -14,7 +14,21 @@ If missing, install it and re-check:
14
14
  npm install -g @osfactory/har
15
15
  ```
16
16
 
17
- ## 2. Pick a harness profile
17
+ ## 2. Check Docker
18
+
19
+ Docker is required: Mission Control runs as a container and harness infrastructure
20
+ (databases, queues, browsers) starts through Docker Compose.
21
+
22
+ ```bash
23
+ docker info
24
+ ```
25
+
26
+ If the command fails, tell the user Docker must be installed and running
27
+ (https://docs.docker.com/get-started/get-docker/). You can still scaffold the
28
+ harness, but say clearly that Mission Control and containerized infra stay
29
+ unavailable until Docker works.
30
+
31
+ ## 3. Pick a harness profile
18
32
 
19
33
  Inspect the repository and choose the profile that matches how agents will run it:
20
34
 
@@ -26,7 +40,7 @@ Inspect the repository and choose the profile that matches how agents will run i
26
40
 
27
41
  Tell the user which profile you picked and why before continuing.
28
42
 
29
- ## 3. Initialize the harness
43
+ ## 4. Initialize the harness
30
44
 
31
45
  ```bash
32
46
  har env init --profile <profile> # omit --profile for default
@@ -34,11 +48,11 @@ har env init --profile <profile> # omit --profile for default
34
48
 
35
49
  If `.har/` already exists, stop and suggest `/har-maintain` instead.
36
50
 
37
- ## 4. Perform the adaptation yourself
51
+ ## 5. Perform the adaptation yourself
38
52
 
39
53
  `har env init` prints an adaptation prompt and writes it to `.har/ADAPT-PROMPT.md`. Read that file and **execute its instructions yourself, now, in this session** — tailor `.har/` scripts (`launch.sh`, `verify.sh`, `setup-infra.sh`, `harness.env`, `stages.json`) and `AGENTS.md` to this repository's real stack, ports, and commands.
40
54
 
41
- ## 5. Register the project's checks as stages
55
+ ## 6. Register the project's checks as stages
42
56
 
43
57
  Convert the repository's real check commands (test, lint, typecheck, whatever CI runs) into registered stages so they run in `verify --full` and are visible to every agent. Read `.har/STAGES.md` for the contract, then:
44
58
 
@@ -49,7 +63,7 @@ har env add-stage unit-tests --custom --kind test --command "npm test" --verific
49
63
  - Use `--command` for one-liner checks; use `--script` when a check needs the slot's env, ports, or artifacts (then implement the scaffolded `.har/stages/<id>.sh`).
50
64
  - Rich integrations ship as **plugins**: `har env add-plugin --list`, then e.g. `har env add-plugin playwright` (web) or `har env add-plugin rocketsim` (iOS). Plugins install stages; agents only talk to the stage registry.
51
65
 
52
- ## 6. Prove the harness works
66
+ ## 7. Prove the harness works
53
67
 
54
68
  ```bash
55
69
  har env launch 1
@@ -58,7 +72,7 @@ har env verify 1
58
72
 
59
73
  Fix the harness scripts until both pass. Then tear down or keep the slot as the user prefers (`har env teardown 1` keeps the branch).
60
74
 
61
- ## 7. Commit
75
+ ## 8. Commit
62
76
 
63
77
  After the user confirms, commit the harness:
64
78
 
@@ -10,7 +10,7 @@
10
10
  "name": "setup-har",
11
11
  "description": "Install the har CLI and scaffold the .har/ agent harness in this repository (init, adapt, launch, verify, commit). User-invoked onboarding.",
12
12
  "disable-model-invocation": true,
13
- "allowed-tools": "Bash(har *) Bash(npm *) Bash(git *) Read Grep Glob Edit Write"
13
+ "allowed-tools": "Bash(har *) Bash(npm *) Bash(git *) Bash(docker *) Read Grep Glob Edit Write"
14
14
  }
15
15
  },
16
16
  "cursor": { "command": true },
@@ -586,9 +586,129 @@ provision_ruby() {
586
586
  append_path_prefix "$dir/vendor/bundle/bin"
587
587
  }
588
588
 
589
+ # ── iOS Xcode project helpers ─────────────────────────────────────────────────
590
+ # Tuist, XcodeGen and CocoaPods treat the .xcodeproj / .xcworkspace as a build
591
+ # product rather than a tracked file, so a fresh worktree has nothing for
592
+ # xcodebuild to open. Generate it here — and fail naming the missing generator,
593
+ # instead of letting xcodebuild report an opaque "scheme not found" later.
594
+
595
+ # First project file of a kind in the work dir. Runs from inside the dir so a dot
596
+ # in the worktree path itself cannot trip the dotfile filter, and skips both the
597
+ # project.xcworkspace every .xcodeproj carries inside it and whatever CocoaPods
598
+ # generates under Pods/.
599
+ har_ios_find_project_file() {
600
+ local dir="$1"
601
+ local pattern="$2"
602
+ local match=""
603
+ match="$(cd "$dir" 2>/dev/null && find . -maxdepth 2 -name "$pattern" \
604
+ ! -path "./.*" ! -path "*.xcodeproj/*" ! -path "*/Pods/*" 2>/dev/null | head -1)" || true
605
+ [ -n "$match" ] || return 1
606
+ echo "${match#./}"
607
+ }
608
+
609
+ # True when this worktree already has something xcodebuild can open. Adapt-time
610
+ # HARNESS_XCODE_WORKSPACE / HARNESS_XCODE_PROJECT decide it when set: they name
611
+ # the generated file, so a missing one means the generator still has to run.
612
+ har_ios_has_project() {
613
+ local dir="$1"
614
+
615
+ if [ -n "${HARNESS_XCODE_WORKSPACE:-}" ] || [ -n "${HARNESS_XCODE_PROJECT:-}" ]; then
616
+ if [ -n "${HARNESS_XCODE_WORKSPACE:-}" ] && [ -e "$dir/$HARNESS_XCODE_WORKSPACE" ]; then
617
+ return 0
618
+ fi
619
+ if [ -n "${HARNESS_XCODE_PROJECT:-}" ] && [ -e "$dir/$HARNESS_XCODE_PROJECT" ]; then
620
+ return 0
621
+ fi
622
+ return 1
623
+ fi
624
+
625
+ if har_ios_find_project_file "$dir" '*.xcworkspace' >/dev/null; then return 0; fi
626
+ if har_ios_find_project_file "$dir" '*.xcodeproj' >/dev/null; then return 0; fi
627
+ return 1
628
+ }
629
+
630
+ # Generator this repo declares, or empty. A Podfile counts: `pod install` is what
631
+ # writes the .xcworkspace for a CocoaPods project.
632
+ har_ios_project_generator() {
633
+ local dir="$1"
634
+ if [ -f "$dir/Project.swift" ] || [ -f "$dir/Workspace.swift" ]; then echo tuist; return; fi
635
+ if [ -f "$dir/project.yml" ] || [ -f "$dir/project.yaml" ]; then echo xcodegen; return; fi
636
+ if [ -f "$dir/Podfile" ]; then echo pod; return; fi
637
+ echo ""
638
+ }
639
+
640
+ har_ios_generate_project() {
641
+ local dir="$1"
642
+ local generator=""
643
+
644
+ if har_ios_has_project "$dir"; then
645
+ return 0
646
+ fi
647
+
648
+ generator="$(har_ios_project_generator "$dir")"
649
+ if [ -z "$generator" ]; then
650
+ pt_log "No .xcodeproj/.xcworkspace found and no generator manifest (Project.swift, project.yml, Podfile) — set HARNESS_XCODE_PROJECT/HARNESS_XCODE_WORKSPACE or HARNESS_INSTALL_CMD in harness.env"
651
+ return 0
652
+ fi
653
+
654
+ if ! command -v "$generator" >/dev/null 2>&1; then
655
+ local label="$generator"
656
+ [ "$generator" = "pod" ] && label="CocoaPods (pod)"
657
+ pt_log "ERROR: this repo generates its Xcode project with ${label}, which is not on PATH."
658
+ pt_log " Install ${label}, or set HARNESS_INSTALL_CMD in harness.env to provision the project another way."
659
+ return 1
660
+ fi
661
+
662
+ case "$generator" in
663
+ tuist)
664
+ pt_log "Generating the Xcode project with tuist..."
665
+ if ! (cd "$dir" && tuist generate --no-open); then
666
+ pt_log "tuist generate --no-open failed — retrying without the flag"
667
+ (cd "$dir" && tuist generate)
668
+ fi
669
+ ;;
670
+ xcodegen)
671
+ pt_log "Generating the Xcode project with xcodegen..."
672
+ (cd "$dir" && xcodegen generate)
673
+ ;;
674
+ pod)
675
+ # har_ios_pod_install writes the workspace right after this.
676
+ :
677
+ ;;
678
+ esac
679
+ }
680
+
681
+ # CocoaPods dependencies. Independent of project generation: a tracked
682
+ # .xcworkspace still cannot build when Pods/ is missing in a fresh worktree.
683
+ har_ios_pod_install() {
684
+ local dir="$1"
685
+
686
+ [ -f "$dir/Podfile" ] || return 0
687
+ [ -d "$dir/Pods" ] && return 0
688
+
689
+ if ! command -v pod >/dev/null 2>&1; then
690
+ pt_log "ERROR: this repo uses CocoaPods (Podfile, no Pods/) and pod is not on PATH."
691
+ pt_log " Install CocoaPods, or set HARNESS_INSTALL_CMD in harness.env to provision the project another way."
692
+ return 1
693
+ fi
694
+
695
+ pt_log "Installing CocoaPods dependencies..."
696
+ (cd "$dir" && pod install)
697
+ }
698
+
589
699
  provision_ios() {
590
700
  local dir="$1"
591
- run_install_cmd "$dir" || true
701
+
702
+ # An explicit HARNESS_INSTALL_CMD owns provisioning outright: the default
703
+ # generators stay out of the way, and a failing override fails the launch
704
+ # rather than being papered over.
705
+ if [ -n "${HARNESS_INSTALL_CMD:-}" ]; then
706
+ run_install_cmd "$dir"
707
+ else
708
+ har_ios_generate_project "$dir"
709
+ har_ios_pod_install "$dir"
710
+ fi
711
+
592
712
  append_env "HARNESS_ECOSYSTEM" "ios"
593
713
  append_env "XCODEBUILD_BIN" "$(command -v xcodebuild 2>/dev/null || echo xcodebuild)"
594
714
  if [ -n "${HARNESS_XCODE_SCHEME:-}" ]; then
@@ -586,9 +586,129 @@ provision_ruby() {
586
586
  append_path_prefix "$dir/vendor/bundle/bin"
587
587
  }
588
588
 
589
+ # ── iOS Xcode project helpers ─────────────────────────────────────────────────
590
+ # Tuist, XcodeGen and CocoaPods treat the .xcodeproj / .xcworkspace as a build
591
+ # product rather than a tracked file, so a fresh worktree has nothing for
592
+ # xcodebuild to open. Generate it here — and fail naming the missing generator,
593
+ # instead of letting xcodebuild report an opaque "scheme not found" later.
594
+
595
+ # First project file of a kind in the work dir. Runs from inside the dir so a dot
596
+ # in the worktree path itself cannot trip the dotfile filter, and skips both the
597
+ # project.xcworkspace every .xcodeproj carries inside it and whatever CocoaPods
598
+ # generates under Pods/.
599
+ har_ios_find_project_file() {
600
+ local dir="$1"
601
+ local pattern="$2"
602
+ local match=""
603
+ match="$(cd "$dir" 2>/dev/null && find . -maxdepth 2 -name "$pattern" \
604
+ ! -path "./.*" ! -path "*.xcodeproj/*" ! -path "*/Pods/*" 2>/dev/null | head -1)" || true
605
+ [ -n "$match" ] || return 1
606
+ echo "${match#./}"
607
+ }
608
+
609
+ # True when this worktree already has something xcodebuild can open. Adapt-time
610
+ # HARNESS_XCODE_WORKSPACE / HARNESS_XCODE_PROJECT decide it when set: they name
611
+ # the generated file, so a missing one means the generator still has to run.
612
+ har_ios_has_project() {
613
+ local dir="$1"
614
+
615
+ if [ -n "${HARNESS_XCODE_WORKSPACE:-}" ] || [ -n "${HARNESS_XCODE_PROJECT:-}" ]; then
616
+ if [ -n "${HARNESS_XCODE_WORKSPACE:-}" ] && [ -e "$dir/$HARNESS_XCODE_WORKSPACE" ]; then
617
+ return 0
618
+ fi
619
+ if [ -n "${HARNESS_XCODE_PROJECT:-}" ] && [ -e "$dir/$HARNESS_XCODE_PROJECT" ]; then
620
+ return 0
621
+ fi
622
+ return 1
623
+ fi
624
+
625
+ if har_ios_find_project_file "$dir" '*.xcworkspace' >/dev/null; then return 0; fi
626
+ if har_ios_find_project_file "$dir" '*.xcodeproj' >/dev/null; then return 0; fi
627
+ return 1
628
+ }
629
+
630
+ # Generator this repo declares, or empty. A Podfile counts: `pod install` is what
631
+ # writes the .xcworkspace for a CocoaPods project.
632
+ har_ios_project_generator() {
633
+ local dir="$1"
634
+ if [ -f "$dir/Project.swift" ] || [ -f "$dir/Workspace.swift" ]; then echo tuist; return; fi
635
+ if [ -f "$dir/project.yml" ] || [ -f "$dir/project.yaml" ]; then echo xcodegen; return; fi
636
+ if [ -f "$dir/Podfile" ]; then echo pod; return; fi
637
+ echo ""
638
+ }
639
+
640
+ har_ios_generate_project() {
641
+ local dir="$1"
642
+ local generator=""
643
+
644
+ if har_ios_has_project "$dir"; then
645
+ return 0
646
+ fi
647
+
648
+ generator="$(har_ios_project_generator "$dir")"
649
+ if [ -z "$generator" ]; then
650
+ pt_log "No .xcodeproj/.xcworkspace found and no generator manifest (Project.swift, project.yml, Podfile) — set HARNESS_XCODE_PROJECT/HARNESS_XCODE_WORKSPACE or HARNESS_INSTALL_CMD in harness.env"
651
+ return 0
652
+ fi
653
+
654
+ if ! command -v "$generator" >/dev/null 2>&1; then
655
+ local label="$generator"
656
+ [ "$generator" = "pod" ] && label="CocoaPods (pod)"
657
+ pt_log "ERROR: this repo generates its Xcode project with ${label}, which is not on PATH."
658
+ pt_log " Install ${label}, or set HARNESS_INSTALL_CMD in harness.env to provision the project another way."
659
+ return 1
660
+ fi
661
+
662
+ case "$generator" in
663
+ tuist)
664
+ pt_log "Generating the Xcode project with tuist..."
665
+ if ! (cd "$dir" && tuist generate --no-open); then
666
+ pt_log "tuist generate --no-open failed — retrying without the flag"
667
+ (cd "$dir" && tuist generate)
668
+ fi
669
+ ;;
670
+ xcodegen)
671
+ pt_log "Generating the Xcode project with xcodegen..."
672
+ (cd "$dir" && xcodegen generate)
673
+ ;;
674
+ pod)
675
+ # har_ios_pod_install writes the workspace right after this.
676
+ :
677
+ ;;
678
+ esac
679
+ }
680
+
681
+ # CocoaPods dependencies. Independent of project generation: a tracked
682
+ # .xcworkspace still cannot build when Pods/ is missing in a fresh worktree.
683
+ har_ios_pod_install() {
684
+ local dir="$1"
685
+
686
+ [ -f "$dir/Podfile" ] || return 0
687
+ [ -d "$dir/Pods" ] && return 0
688
+
689
+ if ! command -v pod >/dev/null 2>&1; then
690
+ pt_log "ERROR: this repo uses CocoaPods (Podfile, no Pods/) and pod is not on PATH."
691
+ pt_log " Install CocoaPods, or set HARNESS_INSTALL_CMD in harness.env to provision the project another way."
692
+ return 1
693
+ fi
694
+
695
+ pt_log "Installing CocoaPods dependencies..."
696
+ (cd "$dir" && pod install)
697
+ }
698
+
589
699
  provision_ios() {
590
700
  local dir="$1"
591
- run_install_cmd "$dir" || true
701
+
702
+ # An explicit HARNESS_INSTALL_CMD owns provisioning outright: the default
703
+ # generators stay out of the way, and a failing override fails the launch
704
+ # rather than being papered over.
705
+ if [ -n "${HARNESS_INSTALL_CMD:-}" ]; then
706
+ run_install_cmd "$dir"
707
+ else
708
+ har_ios_generate_project "$dir"
709
+ har_ios_pod_install "$dir"
710
+ fi
711
+
592
712
  append_env "HARNESS_ECOSYSTEM" "ios"
593
713
  append_env "XCODEBUILD_BIN" "$(command -v xcodebuild 2>/dev/null || echo xcodebuild)"
594
714
  if [ -n "${HARNESS_XCODE_SCHEME:-}" ]; then
@@ -21,6 +21,12 @@ structure, then make the changes listed below. Commit when done.
21
21
  | `HARNESS_SIMULATOR_NAME` | Display name exactly as shown in `xcrun simctl list devices` (e.g. `iPhone 16`). |
22
22
  | `HARNESS_BUNDLE_ID` | App bundle identifier from the Xcode target (e.g. `com.example.myapp`). |
23
23
 
24
+ If the project is generated (`Project.swift`, `project.yml`, or a `Podfile` with no
25
+ tracked `.xcworkspace`), still set the path the generator produces — launch runs
26
+ `tuist generate` / `xcodegen generate` / `pod install` when the file is absent from
27
+ a fresh worktree. Use `HARNESS_INSTALL_CMD` only when that default is wrong; it then
28
+ owns provisioning outright.
29
+
24
30
  Run `./.har/setup-infra.sh` after editing to confirm the simulator boots.
25
31
 
26
32
  ## 3 — Verify `.har/verify.sh` builds and tests correctly
@@ -21,7 +21,7 @@ Generated and maintained by [`har`](https://github.com/os-factory/har). Run `har
21
21
  | `setup-infra.sh` | Check the toolchain; start optional Docker services |
22
22
  | `simulator.sh` | Create, boot and delete one iOS Simulator per agent slot |
23
23
  | `launch.sh` | Launch one agent slot (git worktree, toolchain provisioning, simulator, env file) |
24
- | `provision-toolchain.sh` | Write Xcode/simulator paths (`XCODEBUILD_BIN`, …) to `.env.agent.<id>` |
24
+ | `provision-toolchain.sh` | Generate the Xcode project when it is a build product (Tuist / XcodeGen / CocoaPods) and write Xcode paths (`XCODEBUILD_BIN`, …) to `.env.agent.<id>` |
25
25
  | `verify.sh` | Verification pipeline (build smoke by default; --full adds tests, lint, flows) |
26
26
  | `teardown.sh` | Tear down one agent slot (worktree + env file) |
27
27
  | `agent-cli.sh` | Inspect slot status, run xcodebuild commands, install/launch app |
@@ -9,6 +9,12 @@ export HARNESS_AGENT_SLOT_MAX=3
9
9
 
10
10
  # Toolchain provisioning — launch writes Xcode/simulator paths into .env.agent.<id>
11
11
  export HARNESS_ECOSYSTEM="ios"
12
+ # Generated projects: when the worktree has no .xcodeproj/.xcworkspace, launch runs
13
+ # the generator this repo declares — tuist generate (Project.swift), xcodegen
14
+ # generate (project.yml) — plus pod install when a Podfile is present and Pods/ is
15
+ # missing. A missing generator fails the launch by name.
16
+ # HARNESS_INSTALL_CMD owns provisioning outright: set it and no default generator
17
+ # runs, and its failure fails the launch.
12
18
  # export HARNESS_INSTALL_CMD="xcodebuild -resolvePackageDependencies ..."
13
19
  # Node package manager: auto-detected from package.json "packageManager" and the
14
20
  # lockfile (bun.lock → bun, pnpm-lock.yaml → pnpm, yarn.lock → yarn, else npm),
@@ -586,9 +586,129 @@ provision_ruby() {
586
586
  append_path_prefix "$dir/vendor/bundle/bin"
587
587
  }
588
588
 
589
+ # ── iOS Xcode project helpers ─────────────────────────────────────────────────
590
+ # Tuist, XcodeGen and CocoaPods treat the .xcodeproj / .xcworkspace as a build
591
+ # product rather than a tracked file, so a fresh worktree has nothing for
592
+ # xcodebuild to open. Generate it here — and fail naming the missing generator,
593
+ # instead of letting xcodebuild report an opaque "scheme not found" later.
594
+
595
+ # First project file of a kind in the work dir. Runs from inside the dir so a dot
596
+ # in the worktree path itself cannot trip the dotfile filter, and skips both the
597
+ # project.xcworkspace every .xcodeproj carries inside it and whatever CocoaPods
598
+ # generates under Pods/.
599
+ har_ios_find_project_file() {
600
+ local dir="$1"
601
+ local pattern="$2"
602
+ local match=""
603
+ match="$(cd "$dir" 2>/dev/null && find . -maxdepth 2 -name "$pattern" \
604
+ ! -path "./.*" ! -path "*.xcodeproj/*" ! -path "*/Pods/*" 2>/dev/null | head -1)" || true
605
+ [ -n "$match" ] || return 1
606
+ echo "${match#./}"
607
+ }
608
+
609
+ # True when this worktree already has something xcodebuild can open. Adapt-time
610
+ # HARNESS_XCODE_WORKSPACE / HARNESS_XCODE_PROJECT decide it when set: they name
611
+ # the generated file, so a missing one means the generator still has to run.
612
+ har_ios_has_project() {
613
+ local dir="$1"
614
+
615
+ if [ -n "${HARNESS_XCODE_WORKSPACE:-}" ] || [ -n "${HARNESS_XCODE_PROJECT:-}" ]; then
616
+ if [ -n "${HARNESS_XCODE_WORKSPACE:-}" ] && [ -e "$dir/$HARNESS_XCODE_WORKSPACE" ]; then
617
+ return 0
618
+ fi
619
+ if [ -n "${HARNESS_XCODE_PROJECT:-}" ] && [ -e "$dir/$HARNESS_XCODE_PROJECT" ]; then
620
+ return 0
621
+ fi
622
+ return 1
623
+ fi
624
+
625
+ if har_ios_find_project_file "$dir" '*.xcworkspace' >/dev/null; then return 0; fi
626
+ if har_ios_find_project_file "$dir" '*.xcodeproj' >/dev/null; then return 0; fi
627
+ return 1
628
+ }
629
+
630
+ # Generator this repo declares, or empty. A Podfile counts: `pod install` is what
631
+ # writes the .xcworkspace for a CocoaPods project.
632
+ har_ios_project_generator() {
633
+ local dir="$1"
634
+ if [ -f "$dir/Project.swift" ] || [ -f "$dir/Workspace.swift" ]; then echo tuist; return; fi
635
+ if [ -f "$dir/project.yml" ] || [ -f "$dir/project.yaml" ]; then echo xcodegen; return; fi
636
+ if [ -f "$dir/Podfile" ]; then echo pod; return; fi
637
+ echo ""
638
+ }
639
+
640
+ har_ios_generate_project() {
641
+ local dir="$1"
642
+ local generator=""
643
+
644
+ if har_ios_has_project "$dir"; then
645
+ return 0
646
+ fi
647
+
648
+ generator="$(har_ios_project_generator "$dir")"
649
+ if [ -z "$generator" ]; then
650
+ pt_log "No .xcodeproj/.xcworkspace found and no generator manifest (Project.swift, project.yml, Podfile) — set HARNESS_XCODE_PROJECT/HARNESS_XCODE_WORKSPACE or HARNESS_INSTALL_CMD in harness.env"
651
+ return 0
652
+ fi
653
+
654
+ if ! command -v "$generator" >/dev/null 2>&1; then
655
+ local label="$generator"
656
+ [ "$generator" = "pod" ] && label="CocoaPods (pod)"
657
+ pt_log "ERROR: this repo generates its Xcode project with ${label}, which is not on PATH."
658
+ pt_log " Install ${label}, or set HARNESS_INSTALL_CMD in harness.env to provision the project another way."
659
+ return 1
660
+ fi
661
+
662
+ case "$generator" in
663
+ tuist)
664
+ pt_log "Generating the Xcode project with tuist..."
665
+ if ! (cd "$dir" && tuist generate --no-open); then
666
+ pt_log "tuist generate --no-open failed — retrying without the flag"
667
+ (cd "$dir" && tuist generate)
668
+ fi
669
+ ;;
670
+ xcodegen)
671
+ pt_log "Generating the Xcode project with xcodegen..."
672
+ (cd "$dir" && xcodegen generate)
673
+ ;;
674
+ pod)
675
+ # har_ios_pod_install writes the workspace right after this.
676
+ :
677
+ ;;
678
+ esac
679
+ }
680
+
681
+ # CocoaPods dependencies. Independent of project generation: a tracked
682
+ # .xcworkspace still cannot build when Pods/ is missing in a fresh worktree.
683
+ har_ios_pod_install() {
684
+ local dir="$1"
685
+
686
+ [ -f "$dir/Podfile" ] || return 0
687
+ [ -d "$dir/Pods" ] && return 0
688
+
689
+ if ! command -v pod >/dev/null 2>&1; then
690
+ pt_log "ERROR: this repo uses CocoaPods (Podfile, no Pods/) and pod is not on PATH."
691
+ pt_log " Install CocoaPods, or set HARNESS_INSTALL_CMD in harness.env to provision the project another way."
692
+ return 1
693
+ fi
694
+
695
+ pt_log "Installing CocoaPods dependencies..."
696
+ (cd "$dir" && pod install)
697
+ }
698
+
589
699
  provision_ios() {
590
700
  local dir="$1"
591
- run_install_cmd "$dir" || true
701
+
702
+ # An explicit HARNESS_INSTALL_CMD owns provisioning outright: the default
703
+ # generators stay out of the way, and a failing override fails the launch
704
+ # rather than being papered over.
705
+ if [ -n "${HARNESS_INSTALL_CMD:-}" ]; then
706
+ run_install_cmd "$dir"
707
+ else
708
+ har_ios_generate_project "$dir"
709
+ har_ios_pod_install "$dir"
710
+ fi
711
+
592
712
  append_env "HARNESS_ECOSYSTEM" "ios"
593
713
  append_env "XCODEBUILD_BIN" "$(command -v xcodebuild 2>/dev/null || echo xcodebuild)"
594
714
  if [ -n "${HARNESS_XCODE_SCHEME:-}" ]; then
@@ -58,12 +58,18 @@ xc_target_flags() {
58
58
  elif [ -n "${HARNESS_XCODE_PROJECT:-}" ] && [ -e "$WORK_DIR/${HARNESS_XCODE_PROJECT}" ]; then
59
59
  echo "-project $WORK_DIR/${HARNESS_XCODE_PROJECT}"
60
60
  else
61
- # Auto-detect: prefer workspace (CocoaPods), then project
61
+ # Auto-detect: prefer workspace (CocoaPods), then project. Runs from inside
62
+ # WORK_DIR so a dot in the worktree path itself cannot trip the dotfile
63
+ # filter, and skips both the project.xcworkspace every .xcodeproj carries
64
+ # inside it and the CocoaPods project under Pods/ — either would point
65
+ # xcodebuild at the wrong target.
62
66
  local ws prj
63
- ws="$(find "$WORK_DIR" -maxdepth 2 -name "*.xcworkspace" ! -path "*/\.*" 2>/dev/null | head -1 || true)"
64
- prj="$(find "$WORK_DIR" -maxdepth 2 -name "*.xcodeproj" ! -path "*/\.*" 2>/dev/null | head -1 || true)"
65
- if [ -n "$ws" ]; then echo "-workspace $ws"
66
- elif [ -n "$prj" ]; then echo "-project $prj"
67
+ ws="$(cd "$WORK_DIR" && find . -maxdepth 2 -name "*.xcworkspace" \
68
+ ! -path "./.*" ! -path "*.xcodeproj/*" ! -path "*/Pods/*" 2>/dev/null | head -1 || true)"
69
+ prj="$(cd "$WORK_DIR" && find . -maxdepth 2 -name "*.xcodeproj" \
70
+ ! -path "./.*" ! -path "*/Pods/*" 2>/dev/null | head -1 || true)"
71
+ if [ -n "$ws" ]; then echo "-workspace $WORK_DIR/${ws#./}"
72
+ elif [ -n "$prj" ]; then echo "-project $WORK_DIR/${prj#./}"
67
73
  fi
68
74
  fi
69
75
  }
@@ -586,9 +586,129 @@ provision_ruby() {
586
586
  append_path_prefix "$dir/vendor/bundle/bin"
587
587
  }
588
588
 
589
+ # ── iOS Xcode project helpers ─────────────────────────────────────────────────
590
+ # Tuist, XcodeGen and CocoaPods treat the .xcodeproj / .xcworkspace as a build
591
+ # product rather than a tracked file, so a fresh worktree has nothing for
592
+ # xcodebuild to open. Generate it here — and fail naming the missing generator,
593
+ # instead of letting xcodebuild report an opaque "scheme not found" later.
594
+
595
+ # First project file of a kind in the work dir. Runs from inside the dir so a dot
596
+ # in the worktree path itself cannot trip the dotfile filter, and skips both the
597
+ # project.xcworkspace every .xcodeproj carries inside it and whatever CocoaPods
598
+ # generates under Pods/.
599
+ har_ios_find_project_file() {
600
+ local dir="$1"
601
+ local pattern="$2"
602
+ local match=""
603
+ match="$(cd "$dir" 2>/dev/null && find . -maxdepth 2 -name "$pattern" \
604
+ ! -path "./.*" ! -path "*.xcodeproj/*" ! -path "*/Pods/*" 2>/dev/null | head -1)" || true
605
+ [ -n "$match" ] || return 1
606
+ echo "${match#./}"
607
+ }
608
+
609
+ # True when this worktree already has something xcodebuild can open. Adapt-time
610
+ # HARNESS_XCODE_WORKSPACE / HARNESS_XCODE_PROJECT decide it when set: they name
611
+ # the generated file, so a missing one means the generator still has to run.
612
+ har_ios_has_project() {
613
+ local dir="$1"
614
+
615
+ if [ -n "${HARNESS_XCODE_WORKSPACE:-}" ] || [ -n "${HARNESS_XCODE_PROJECT:-}" ]; then
616
+ if [ -n "${HARNESS_XCODE_WORKSPACE:-}" ] && [ -e "$dir/$HARNESS_XCODE_WORKSPACE" ]; then
617
+ return 0
618
+ fi
619
+ if [ -n "${HARNESS_XCODE_PROJECT:-}" ] && [ -e "$dir/$HARNESS_XCODE_PROJECT" ]; then
620
+ return 0
621
+ fi
622
+ return 1
623
+ fi
624
+
625
+ if har_ios_find_project_file "$dir" '*.xcworkspace' >/dev/null; then return 0; fi
626
+ if har_ios_find_project_file "$dir" '*.xcodeproj' >/dev/null; then return 0; fi
627
+ return 1
628
+ }
629
+
630
+ # Generator this repo declares, or empty. A Podfile counts: `pod install` is what
631
+ # writes the .xcworkspace for a CocoaPods project.
632
+ har_ios_project_generator() {
633
+ local dir="$1"
634
+ if [ -f "$dir/Project.swift" ] || [ -f "$dir/Workspace.swift" ]; then echo tuist; return; fi
635
+ if [ -f "$dir/project.yml" ] || [ -f "$dir/project.yaml" ]; then echo xcodegen; return; fi
636
+ if [ -f "$dir/Podfile" ]; then echo pod; return; fi
637
+ echo ""
638
+ }
639
+
640
+ har_ios_generate_project() {
641
+ local dir="$1"
642
+ local generator=""
643
+
644
+ if har_ios_has_project "$dir"; then
645
+ return 0
646
+ fi
647
+
648
+ generator="$(har_ios_project_generator "$dir")"
649
+ if [ -z "$generator" ]; then
650
+ pt_log "No .xcodeproj/.xcworkspace found and no generator manifest (Project.swift, project.yml, Podfile) — set HARNESS_XCODE_PROJECT/HARNESS_XCODE_WORKSPACE or HARNESS_INSTALL_CMD in harness.env"
651
+ return 0
652
+ fi
653
+
654
+ if ! command -v "$generator" >/dev/null 2>&1; then
655
+ local label="$generator"
656
+ [ "$generator" = "pod" ] && label="CocoaPods (pod)"
657
+ pt_log "ERROR: this repo generates its Xcode project with ${label}, which is not on PATH."
658
+ pt_log " Install ${label}, or set HARNESS_INSTALL_CMD in harness.env to provision the project another way."
659
+ return 1
660
+ fi
661
+
662
+ case "$generator" in
663
+ tuist)
664
+ pt_log "Generating the Xcode project with tuist..."
665
+ if ! (cd "$dir" && tuist generate --no-open); then
666
+ pt_log "tuist generate --no-open failed — retrying without the flag"
667
+ (cd "$dir" && tuist generate)
668
+ fi
669
+ ;;
670
+ xcodegen)
671
+ pt_log "Generating the Xcode project with xcodegen..."
672
+ (cd "$dir" && xcodegen generate)
673
+ ;;
674
+ pod)
675
+ # har_ios_pod_install writes the workspace right after this.
676
+ :
677
+ ;;
678
+ esac
679
+ }
680
+
681
+ # CocoaPods dependencies. Independent of project generation: a tracked
682
+ # .xcworkspace still cannot build when Pods/ is missing in a fresh worktree.
683
+ har_ios_pod_install() {
684
+ local dir="$1"
685
+
686
+ [ -f "$dir/Podfile" ] || return 0
687
+ [ -d "$dir/Pods" ] && return 0
688
+
689
+ if ! command -v pod >/dev/null 2>&1; then
690
+ pt_log "ERROR: this repo uses CocoaPods (Podfile, no Pods/) and pod is not on PATH."
691
+ pt_log " Install CocoaPods, or set HARNESS_INSTALL_CMD in harness.env to provision the project another way."
692
+ return 1
693
+ fi
694
+
695
+ pt_log "Installing CocoaPods dependencies..."
696
+ (cd "$dir" && pod install)
697
+ }
698
+
589
699
  provision_ios() {
590
700
  local dir="$1"
591
- run_install_cmd "$dir" || true
701
+
702
+ # An explicit HARNESS_INSTALL_CMD owns provisioning outright: the default
703
+ # generators stay out of the way, and a failing override fails the launch
704
+ # rather than being papered over.
705
+ if [ -n "${HARNESS_INSTALL_CMD:-}" ]; then
706
+ run_install_cmd "$dir"
707
+ else
708
+ har_ios_generate_project "$dir"
709
+ har_ios_pod_install "$dir"
710
+ fi
711
+
592
712
  append_env "HARNESS_ECOSYSTEM" "ios"
593
713
  append_env "XCODEBUILD_BIN" "$(command -v xcodebuild 2>/dev/null || echo xcodebuild)"
594
714
  if [ -n "${HARNESS_XCODE_SCHEME:-}" ]; then