To work around Xcode’s disinclination for creating new simulators, I wrote a script which deletes all the current simulators and then creates every possible simulator. It’s relatively straightforward because simctl has a decent JSON interface which makes processing the state a lot nicer:

#!/usr/bin/env fish

# Just to make it obvious when using the wrong version
printf "Using Xcode at %s\n\n" (xcode-select -p)

echo "Deleting all simulators..."
xcrun simctl shutdown all >/dev/null
xcrun simctl delete all >/dev/null
printf "...done\n\n"

echo "Creating new simulators..."

# You could also add 'appletv' to this list
for runtime in ios watch
    set -l runtimes (xcrun simctl list runtimes $runtime available -j | jq -c '.runtimes[]')
    for runtime in $runtimes
        set -l runtime_version (echo $runtime | jq -r '.version')
        set -l runtime_identifier (echo $runtime | jq -r '.identifier')
        set -l supported_devices (echo $runtime | jq -c '.supportedDeviceTypes[]')
        for device in $supported_devices
            set -l device_name (echo $device | jq -r '.name')
            set -l device_identifier (echo $device | jq -r '.identifier')
            set -l display_name "$device_name ($runtime_version)"
            printf \t%s\n $display_name
            xcrun simctl create $display_name $device_identifier $runtime_identifier >/dev/null
        end
    end
end

printf "...done\n\n"

The only thing missing here is device pairing – connecting a watch and phone together. Since there’s limitations around the number of devices which can be paired together, I find this a bit easier to still do manually.