@aarwitz/tapp 0.15.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 +123 -0
- package/Harness/OCQAHarness/AppDelegate.swift +21 -0
- package/Harness/OCQAHarness/Info.plist +26 -0
- package/Harness/OCQAHarness.xcodeproj/project.pbxproj +199 -0
- package/Harness/OCQAHarness.xcodeproj/xcshareddata/xcschemes/OCQAHarnessUITests.xcscheme +22 -0
- package/Harness/OCQAHarnessUITests/ExplorerTests.swift +4526 -0
- package/Harness/OCQAHarnessUITests/Info.plist +22 -0
- package/Harness/generate-harness-xcodeproj.rb +254 -0
- package/LICENSE +21 -0
- package/README.md +374 -0
- package/bin/tapp.js +1382 -0
- package/browser/app.css +227 -0
- package/browser/app.js +675 -0
- package/browser/index.html +195 -0
- package/browser/product-contract.js +25 -0
- package/browser/view-model.js +16 -0
- package/docs/BROWSER-PRODUCT.md +72 -0
- package/docs/PRODUCT-ENGINE.md +102 -0
- package/docs/application-model.md +276 -0
- package/docs/scenarios.md +95 -0
- package/mcp-server/src/android-driver.js +287 -0
- package/mcp-server/src/android-explorer.js +197 -0
- package/mcp-server/src/android-flow.js +89 -0
- package/mcp-server/src/application-model.js +1597 -0
- package/mcp-server/src/browser-product.js +659 -0
- package/mcp-server/src/browser-workspaces.js +234 -0
- package/mcp-server/src/ci-report.js +557 -0
- package/mcp-server/src/ci-setup.js +359 -0
- package/mcp-server/src/contract-authoring.js +10 -0
- package/mcp-server/src/enrich.js +57 -0
- package/mcp-server/src/flow-runtime.js +127 -0
- package/mcp-server/src/html-report.js +124 -0
- package/mcp-server/src/index.js +3775 -0
- package/mcp-server/src/maintenance-proposal.js +178 -0
- package/mcp-server/src/managed-operation.js +61 -0
- package/mcp-server/src/pr-selection.js +841 -0
- package/mcp-server/src/product-execution.js +155 -0
- package/mcp-server/src/product-operations.js +526 -0
- package/mcp-server/src/project-config.js +101 -0
- package/mcp-server/src/release-contract.d.ts +81 -0
- package/mcp-server/src/release-contract.js +226 -0
- package/mcp-server/src/report.js +363 -0
- package/mcp-server/src/scenario-runtime.js +139 -0
- package/mcp-server/src/static-server.js +44 -0
- package/mcp-server/src/task-runtime.js +266 -0
- package/mcp-server/src/ui-map.js +661 -0
- package/mcp-server/src/web-explorer.js +493 -0
- package/mcp-server/src/web-flow.js +238 -0
- package/package.json +82 -0
- package/scripts/android-corpus-e2e.sh +30 -0
- package/scripts/ci-gate.sh +323 -0
- package/scripts/cleanup-xcode.sh +157 -0
- package/scripts/compile-contract.js +27 -0
- package/scripts/compile-flow.js +18 -0
- package/scripts/corpus-apps.txt +9 -0
- package/scripts/corpus-sweep.sh +121 -0
- package/scripts/coverage-eval.sh +92 -0
- package/scripts/coverage_eval_parse.py +95 -0
- package/scripts/deploy-and-build.sh +99 -0
- package/scripts/flow-platform.js +18 -0
- package/scripts/flow_ai_judge.py +102 -0
- package/scripts/flow_lib.py +154 -0
- package/scripts/mutation-recall-desktop.sh +186 -0
- package/scripts/mutation-recall.sh +121 -0
- package/scripts/mutation_lib.py +128 -0
- package/scripts/mutation_operators.py +144 -0
- package/scripts/platform-gate.js +186 -0
- package/scripts/pr-plan.js +68 -0
- package/scripts/quick-capture.sh +419 -0
- package/scripts/run-android-flow.js +27 -0
- package/scripts/run-flow.sh +90 -0
- package/scripts/run-web-flow.js +28 -0
- package/scripts/run-web-scenario.js +23 -0
- package/scripts/validation-matrix.sh +146 -0
- package/scripts/vision-fp-eval.sh +206 -0
- package/scripts/vision_escalation_responder.py +147 -0
- package/scripts/vision_fp_probe.py +221 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Tapp CI gate — one command that answers "should this merge?" for iOS, Android, or web.
|
|
3
|
+
#
|
|
4
|
+
# Boots a simulator if needed, installs the app build under test, runs the autonomous exploration
|
|
5
|
+
# harness, replays every committed Flow (deterministic E2E tests), diffs the findings against a
|
|
6
|
+
# stored baseline, writes a GitHub Actions step summary, and exits non-zero when the gate fails
|
|
7
|
+
# (new high/critical findings vs. baseline, or any failed Flow). Wrapped by ../action.yml for
|
|
8
|
+
# GitHub Actions; equally usable from any other CI or locally.
|
|
9
|
+
#
|
|
10
|
+
# Usage:
|
|
11
|
+
# scripts/ci-gate.sh [--platform ios] --app <path/to/App.app> [--bundle-id <com.example.app>]
|
|
12
|
+
# scripts/ci-gate.sh --platform android --apk <path/to/app.apk> --app-id <com.example.app>
|
|
13
|
+
# scripts/ci-gate.sh --platform web [--url <http(s)://owned-app>]
|
|
14
|
+
# # omit --url with --project-dir to detect/build/start/stop one owned web target
|
|
15
|
+
# # bundle id is detected from the .app when omitted
|
|
16
|
+
# [--actions N] # exploration budget (default 40)
|
|
17
|
+
# [--timeout S] # exploration watchdog (default 600)
|
|
18
|
+
# [--flows <glob>] # Flow YAMLs to replay (default: <app repo>/.autotap/flows/*.yml if --project-dir given)
|
|
19
|
+
# [--scenarios <glob>] # Multi-actor Scenario YAMLs (web; default: <app repo>/.autotap/scenarios/*.yml)
|
|
20
|
+
# [--contracts <glob>] # TypeScript release contracts (default: <app repo>/.autotap/contracts/*.contract.ts)
|
|
21
|
+
# [--project-dir <dir>] # the app repo checkout (for flows + baseline defaults)
|
|
22
|
+
# [--pr-base <git-ref>] # select critical + diff-relevant contracts from base...head
|
|
23
|
+
# [--pr-head <git-ref>] # default HEAD
|
|
24
|
+
# [--changed-files-file <json|newline file>] # CI-provided PR paths; includes renamed paths
|
|
25
|
+
# [--pr-plan-out <file.json>] # persist the reviewable selection plan
|
|
26
|
+
# [--baseline <file.json>] # prior report to diff against (skipped if absent)
|
|
27
|
+
# [--target-key <stable-id>] # isolates target-specific baselines in monorepos
|
|
28
|
+
# [--fail-on gate|blocked|any] # gate policy (default gate; see ci-report.js)
|
|
29
|
+
# [--json-out <file.json>] # write the full report (use as the next baseline)
|
|
30
|
+
# [--md-out <file.md>] # write the rendered markdown report (for a PR comment)
|
|
31
|
+
# [--device <name>] # simulator device to boot if none is (default "iPhone 16 Pro")
|
|
32
|
+
#
|
|
33
|
+
# The app must be a SIMULATOR build (xcodebuild ... -destination 'generic/platform=iOS Simulator').
|
|
34
|
+
set -uo pipefail
|
|
35
|
+
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
36
|
+
|
|
37
|
+
usage() {
|
|
38
|
+
sed -n '2,30p' "$0" | sed 's/^# \{0,1\}//'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
PLATFORM="ios" APP_PATH="" BUNDLE_ID="" APK_PATH="" APP_ID="" URL="" WEB_TARGET="" TARGET_KEY="" SERIAL="" ACTIONS=40 TIMEOUT=600 FLOWS="" SCENARIOS="" CONTRACTS="" PROJECT_DIR="" BASELINE="" FAIL_ON="gate" JSON_OUT="" MD_OUT="" DEVICE="iPhone 16 Pro" PR_BASE="" PR_HEAD="HEAD" CHANGED_FILES_FILE="" PR_PLAN_OUT=""
|
|
42
|
+
IOS_PR_TARGET_JSON=""
|
|
43
|
+
FLOWS_EXPLICIT=false SCENARIOS_EXPLICIT=false CONTRACTS_EXPLICIT=false
|
|
44
|
+
while [[ $# -gt 0 ]]; do
|
|
45
|
+
case "$1" in
|
|
46
|
+
--platform) PLATFORM="$2"; shift 2 ;;
|
|
47
|
+
--app) APP_PATH="$2"; shift 2 ;;
|
|
48
|
+
--bundle-id) BUNDLE_ID="$2"; shift 2 ;;
|
|
49
|
+
--apk) APK_PATH="$2"; shift 2 ;;
|
|
50
|
+
--app-id) APP_ID="$2"; shift 2 ;;
|
|
51
|
+
--url) URL="$2"; shift 2 ;;
|
|
52
|
+
--web-target) WEB_TARGET="$2"; shift 2 ;;
|
|
53
|
+
--target-key) TARGET_KEY="$2"; shift 2 ;;
|
|
54
|
+
--serial) SERIAL="$2"; shift 2 ;;
|
|
55
|
+
--actions) ACTIONS="$2"; shift 2 ;;
|
|
56
|
+
--timeout) TIMEOUT="$2"; shift 2 ;;
|
|
57
|
+
--flows) FLOWS="$2"; FLOWS_EXPLICIT=true; shift 2 ;;
|
|
58
|
+
--scenarios) SCENARIOS="$2"; SCENARIOS_EXPLICIT=true; shift 2 ;;
|
|
59
|
+
--contracts) CONTRACTS="$2"; CONTRACTS_EXPLICIT=true; shift 2 ;;
|
|
60
|
+
--project-dir) PROJECT_DIR="$2"; shift 2 ;;
|
|
61
|
+
--pr-base) PR_BASE="$2"; shift 2 ;;
|
|
62
|
+
--pr-head) PR_HEAD="$2"; shift 2 ;;
|
|
63
|
+
--changed-files-file) CHANGED_FILES_FILE="$2"; shift 2 ;;
|
|
64
|
+
--pr-plan-out) PR_PLAN_OUT="$2"; shift 2 ;;
|
|
65
|
+
--baseline) BASELINE="$2"; shift 2 ;;
|
|
66
|
+
--fail-on) FAIL_ON="$2"; shift 2 ;;
|
|
67
|
+
--json-out) JSON_OUT="$2"; shift 2 ;;
|
|
68
|
+
--md-out) MD_OUT="$2"; shift 2 ;;
|
|
69
|
+
--device) DEVICE="$2"; shift 2 ;;
|
|
70
|
+
--help|-h) usage; exit 0 ;;
|
|
71
|
+
*) echo "Unknown argument: $1" >&2; exit 2 ;;
|
|
72
|
+
esac
|
|
73
|
+
done
|
|
74
|
+
[[ "$PLATFORM" == "ios" || "$PLATFORM" == "android" || "$PLATFORM" == "web" ]] || { echo "❌ --platform must be ios|android|web" >&2; exit 2; }
|
|
75
|
+
[[ "$ACTIONS" =~ ^[1-9][0-9]*$ ]] || { echo "❌ --actions must be a positive integer" >&2; exit 2; }
|
|
76
|
+
[[ "$TIMEOUT" =~ ^[1-9][0-9]*$ ]] || { echo "❌ --timeout must be a positive integer" >&2; exit 2; }
|
|
77
|
+
[[ "$FAIL_ON" == "gate" || "$FAIL_ON" == "blocked" || "$FAIL_ON" == "any" ]] || { echo "❌ --fail-on must be gate|blocked|any" >&2; exit 2; }
|
|
78
|
+
if [[ -n "$PROJECT_DIR" ]]; then
|
|
79
|
+
[[ -d "$PROJECT_DIR" ]] || { echo "❌ Project directory not found: $PROJECT_DIR" >&2; exit 2; }
|
|
80
|
+
PROJECT_DIR="$(cd "$PROJECT_DIR" && pwd)"
|
|
81
|
+
fi
|
|
82
|
+
[[ -z "$FLOWS" && -n "$PROJECT_DIR" && -d "$PROJECT_DIR/.autotap/flows" ]] && FLOWS="$PROJECT_DIR/.autotap/flows/*.yml"
|
|
83
|
+
[[ "$PLATFORM" == "web" && -z "$SCENARIOS" && -n "$PROJECT_DIR" && -d "$PROJECT_DIR/.autotap/scenarios" ]] && SCENARIOS="$PROJECT_DIR/.autotap/scenarios/*.yml"
|
|
84
|
+
[[ -z "$CONTRACTS" && -n "$PROJECT_DIR" && -d "$PROJECT_DIR/.autotap/contracts" ]] && CONTRACTS="$PROJECT_DIR/.autotap/contracts/*.contract.ts"
|
|
85
|
+
[[ -z "$BASELINE" && -n "$PROJECT_DIR" && -f "$PROJECT_DIR/.autotap/baseline.json" ]] && BASELINE="$PROJECT_DIR/.autotap/baseline.json"
|
|
86
|
+
[[ -z "$BASELINE" || -f "$BASELINE" ]] || { echo "❌ Baseline not found: $BASELINE" >&2; exit 2; }
|
|
87
|
+
if [[ -n "$BASELINE" ]]; then
|
|
88
|
+
python3 -c 'import json,sys; json.load(open(sys.argv[1]))' "$BASELINE" 2>/dev/null \
|
|
89
|
+
|| { echo "❌ Baseline is not valid JSON: $BASELINE" >&2; exit 2; }
|
|
90
|
+
fi
|
|
91
|
+
CONTRACT_FILES=()
|
|
92
|
+
if [[ -n "$CONTRACTS" ]]; then
|
|
93
|
+
shopt -s nullglob
|
|
94
|
+
for contract in $CONTRACTS; do CONTRACT_FILES+=("$contract"); done
|
|
95
|
+
shopt -u nullglob
|
|
96
|
+
if [[ "${#CONTRACT_FILES[@]}" -eq 0 && "$CONTRACTS_EXPLICIT" == true ]]; then
|
|
97
|
+
echo "❌ --contracts matched no files: $CONTRACTS" >&2
|
|
98
|
+
exit 2
|
|
99
|
+
fi
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
validate_json_env() {
|
|
103
|
+
local name="$1" expected="$2" value="${!1:-}"
|
|
104
|
+
[[ -z "$value" ]] && return 0
|
|
105
|
+
TAPP_JSON_VALUE="$value" TAPP_JSON_EXPECTED="$expected" python3 -c '
|
|
106
|
+
import json, os, sys
|
|
107
|
+
value = json.loads(os.environ["TAPP_JSON_VALUE"])
|
|
108
|
+
expected = list if os.environ["TAPP_JSON_EXPECTED"] == "array" else dict
|
|
109
|
+
sys.exit(0 if isinstance(value, expected) else 1)
|
|
110
|
+
' || { echo "❌ $name must be a valid JSON $expected" >&2; exit 2; }
|
|
111
|
+
}
|
|
112
|
+
validate_json_env OCQA_APP_LAUNCH_ARGS_JSON array
|
|
113
|
+
validate_json_env OCQA_APP_LAUNCH_ENV_JSON object
|
|
114
|
+
validate_json_env OCQA_LOGIN_STEPS_JSON array
|
|
115
|
+
|
|
116
|
+
FLOW_FILES=()
|
|
117
|
+
if [[ -n "$FLOWS" ]]; then
|
|
118
|
+
shopt -s nullglob
|
|
119
|
+
for flow in $FLOWS; do FLOW_FILES+=("$flow"); done
|
|
120
|
+
shopt -u nullglob
|
|
121
|
+
if [[ "${#FLOW_FILES[@]}" -eq 0 && "$FLOWS_EXPLICIT" == true ]]; then
|
|
122
|
+
echo "❌ --flows matched no files: $FLOWS" >&2
|
|
123
|
+
exit 2
|
|
124
|
+
fi
|
|
125
|
+
fi
|
|
126
|
+
SCENARIO_FILES=()
|
|
127
|
+
if [[ -n "$SCENARIOS" ]]; then
|
|
128
|
+
shopt -s nullglob
|
|
129
|
+
for scenario in $SCENARIOS; do SCENARIO_FILES+=("$scenario"); done
|
|
130
|
+
shopt -u nullglob
|
|
131
|
+
if [[ "${#SCENARIO_FILES[@]}" -eq 0 && "$SCENARIOS_EXPLICIT" == true ]]; then
|
|
132
|
+
echo "❌ --scenarios matched no files: $SCENARIOS" >&2
|
|
133
|
+
exit 2
|
|
134
|
+
fi
|
|
135
|
+
if [[ "$PLATFORM" != "web" && "${#SCENARIO_FILES[@]}" -gt 0 ]]; then
|
|
136
|
+
echo "❌ Multi-actor Scenarios currently require --platform web" >&2
|
|
137
|
+
exit 2
|
|
138
|
+
fi
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
# Platform-filter both default and explicitly supplied suites before any real
|
|
142
|
+
# surface is launched. Default discovery may legitimately find only another
|
|
143
|
+
# target's Flows in a monorepo; an explicit all-mismatched suite is a config
|
|
144
|
+
# error. Legacy platform-less Flows resolve to iOS, never "all platforms".
|
|
145
|
+
if [[ "${#FLOW_FILES[@]}" -gt 0 ]]; then
|
|
146
|
+
PLATFORM_FLOW_FILES=()
|
|
147
|
+
PLATFORM_FLOW_COUNT=0
|
|
148
|
+
for flow in "${FLOW_FILES[@]}"; do
|
|
149
|
+
flow_platform="$(node "$ROOT/scripts/flow-platform.js" "$flow")" || exit 2
|
|
150
|
+
if [[ "$flow_platform" == "$PLATFORM" ]]; then
|
|
151
|
+
PLATFORM_FLOW_FILES+=("$flow")
|
|
152
|
+
PLATFORM_FLOW_COUNT=$((PLATFORM_FLOW_COUNT + 1))
|
|
153
|
+
fi
|
|
154
|
+
done
|
|
155
|
+
if [[ "$PLATFORM_FLOW_COUNT" -eq 0 && "$FLOWS_EXPLICIT" == true ]]; then
|
|
156
|
+
echo "❌ None of the supplied Flows target platform '$PLATFORM'" >&2
|
|
157
|
+
exit 2
|
|
158
|
+
fi
|
|
159
|
+
FLOW_FILES=()
|
|
160
|
+
[[ "$PLATFORM_FLOW_COUNT" -gt 0 ]] && FLOW_FILES=("${PLATFORM_FLOW_FILES[@]}")
|
|
161
|
+
fi
|
|
162
|
+
|
|
163
|
+
# A PR plan is an execution manifest, not advisory prose: only critical/always
|
|
164
|
+
# and reviewed diff-relevant contracts advance to replay. Unknown files and
|
|
165
|
+
# mapped-but-uncovered UI states remain explicit in the report.
|
|
166
|
+
PR_PLAN_PATH=""
|
|
167
|
+
if [[ -n "$PR_BASE" || -n "$CHANGED_FILES_FILE" ]]; then
|
|
168
|
+
[[ -n "$PROJECT_DIR" && -d "$PROJECT_DIR" ]] || { echo "❌ PR selection requires --project-dir <repository>" >&2; exit 2; }
|
|
169
|
+
[[ -z "$PR_BASE" || -z "$CHANGED_FILES_FILE" ]] || { echo "❌ Use either --pr-base or --changed-files-file, not both" >&2; exit 2; }
|
|
170
|
+
[[ -z "$CHANGED_FILES_FILE" || -f "$CHANGED_FILES_FILE" ]] || { echo "❌ Changed-files file not found: $CHANGED_FILES_FILE" >&2; exit 2; }
|
|
171
|
+
PR_PLAN_PATH="$PR_PLAN_OUT"
|
|
172
|
+
[[ -n "$PR_PLAN_PATH" ]] || PR_PLAN_PATH="$(mktemp "${TMPDIR:-/tmp}/tapp-pr-plan.XXXXXX")"
|
|
173
|
+
PR_SELECTION_PATH="$(mktemp "${TMPDIR:-/tmp}/tapp-pr-selection.XXXXXX")"
|
|
174
|
+
PR_TARGET_PATH="$(mktemp "${TMPDIR:-/tmp}/tapp-pr-target.XXXXXX")"
|
|
175
|
+
PR_ARGS=(--project-dir "$PROJECT_DIR" --platform "$PLATFORM" --head "$PR_HEAD" --json-out "$PR_PLAN_PATH" --selection-out "$PR_SELECTION_PATH" --exploration-target-out "$PR_TARGET_PATH")
|
|
176
|
+
[[ -n "$PR_BASE" ]] && PR_ARGS+=(--base "$PR_BASE")
|
|
177
|
+
[[ -n "$CHANGED_FILES_FILE" ]] && PR_ARGS+=(--changed-files-file "$CHANGED_FILES_FILE")
|
|
178
|
+
for contract in ${CONTRACT_FILES[@]+"${CONTRACT_FILES[@]}"}; do PR_ARGS+=(--contract "$contract"); done
|
|
179
|
+
node "$ROOT/scripts/pr-plan.js" "${PR_ARGS[@]}" || exit 2
|
|
180
|
+
SELECTED_CONTRACT_FILES=()
|
|
181
|
+
SELECTED_CONTRACT_COUNT=0
|
|
182
|
+
while IFS= read -r -d '' contract; do SELECTED_CONTRACT_FILES+=("$contract"); SELECTED_CONTRACT_COUNT=$((SELECTED_CONTRACT_COUNT + 1)); done < <(
|
|
183
|
+
python3 -c 'import json,sys; [sys.stdout.buffer.write(str(p).encode()+b"\0") for p in json.load(open(sys.argv[1]))]' "$PR_SELECTION_PATH"
|
|
184
|
+
)
|
|
185
|
+
CONTRACT_FILES=()
|
|
186
|
+
for contract in ${SELECTED_CONTRACT_FILES[@]+"${SELECTED_CONTRACT_FILES[@]}"}; do CONTRACT_FILES+=("$contract"); done
|
|
187
|
+
if [[ "$PLATFORM" == "ios" ]]; then
|
|
188
|
+
IOS_PR_TARGET_JSON="$(python3 -c 'import json,sys; value=json.load(open(sys.argv[1])); print(json.dumps(value, separators=(",", ":")) if isinstance(value, dict) else "")' "$PR_TARGET_PATH")" || exit 2
|
|
189
|
+
fi
|
|
190
|
+
echo "PR selection: $SELECTED_CONTRACT_COUNT release contract(s); plan: $PR_PLAN_PATH"
|
|
191
|
+
fi
|
|
192
|
+
|
|
193
|
+
# Web and Android share the report/gate with iOS but do not need the Xcode
|
|
194
|
+
# simulator orchestration below. Pass already-expanded Flow paths as argv—not a
|
|
195
|
+
# shell expression—to keep CI inputs inert.
|
|
196
|
+
if [[ "$PLATFORM" != "ios" ]]; then
|
|
197
|
+
PLATFORM_ARGS=(--platform "$PLATFORM" --actions "$ACTIONS" --timeout "$TIMEOUT" --fail-on "$FAIL_ON")
|
|
198
|
+
[[ -n "$URL" ]] && PLATFORM_ARGS+=(--url "$URL")
|
|
199
|
+
[[ -n "$PROJECT_DIR" ]] && PLATFORM_ARGS+=(--project-dir "$PROJECT_DIR")
|
|
200
|
+
[[ -n "$WEB_TARGET" ]] && PLATFORM_ARGS+=(--web-target "$WEB_TARGET")
|
|
201
|
+
[[ -n "$TARGET_KEY" ]] && PLATFORM_ARGS+=(--target-key "$TARGET_KEY")
|
|
202
|
+
[[ -n "$APP_ID" ]] && PLATFORM_ARGS+=(--app-id "$APP_ID")
|
|
203
|
+
[[ -n "$APK_PATH" ]] && PLATFORM_ARGS+=(--apk "$APK_PATH")
|
|
204
|
+
[[ -n "$SERIAL" ]] && PLATFORM_ARGS+=(--serial "$SERIAL")
|
|
205
|
+
[[ -n "$BASELINE" ]] && PLATFORM_ARGS+=(--baseline "$BASELINE")
|
|
206
|
+
[[ -n "$JSON_OUT" ]] && PLATFORM_ARGS+=(--json-out "$JSON_OUT")
|
|
207
|
+
[[ -n "$MD_OUT" ]] && PLATFORM_ARGS+=(--md-out "$MD_OUT")
|
|
208
|
+
[[ -n "$PR_PLAN_PATH" ]] && PLATFORM_ARGS+=(--pr-plan "$PR_PLAN_PATH")
|
|
209
|
+
for flow in ${FLOW_FILES[@]+"${FLOW_FILES[@]}"}; do PLATFORM_ARGS+=(--flow "$flow"); done
|
|
210
|
+
for scenario in ${SCENARIO_FILES[@]+"${SCENARIO_FILES[@]}"}; do PLATFORM_ARGS+=(--scenario "$scenario"); done
|
|
211
|
+
for contract in ${CONTRACT_FILES[@]+"${CONTRACT_FILES[@]}"}; do PLATFORM_ARGS+=(--contract "$contract"); done
|
|
212
|
+
exec node "$ROOT/scripts/platform-gate.js" "${PLATFORM_ARGS[@]}"
|
|
213
|
+
fi
|
|
214
|
+
|
|
215
|
+
# Compile only iOS-applicable contracts before spending simulator time. The
|
|
216
|
+
# compiler exits 3 for reviewed contracts that target another platform.
|
|
217
|
+
CONTRACT_COMPILED_FILES=()
|
|
218
|
+
if [[ "${#CONTRACT_FILES[@]}" -gt 0 ]]; then
|
|
219
|
+
CONTRACT_BUILD_DIR="$(mktemp -d /tmp/tapp-ci-contracts.XXXXXX)"
|
|
220
|
+
for contract in "${CONTRACT_FILES[@]}"; do
|
|
221
|
+
compiled="$CONTRACT_BUILD_DIR/$(basename "$contract" .contract.ts).json"
|
|
222
|
+
set +e
|
|
223
|
+
node "$ROOT/scripts/compile-contract.js" "$contract" ios "$compiled"
|
|
224
|
+
contract_status=$?
|
|
225
|
+
set -e
|
|
226
|
+
[[ "$contract_status" -eq 3 ]] && continue
|
|
227
|
+
[[ "$contract_status" -eq 0 ]] || { echo "❌ Could not compile release contract: $contract" >&2; exit 2; }
|
|
228
|
+
CONTRACT_COMPILED_FILES+=("$compiled")
|
|
229
|
+
done
|
|
230
|
+
fi
|
|
231
|
+
|
|
232
|
+
[[ -n "$APP_PATH" && -d "$APP_PATH" ]] || { echo "❌ Required: --app <path/to/App.app> (a simulator build)" >&2; exit 2; }
|
|
233
|
+
if [[ -z "$BUNDLE_ID" ]]; then
|
|
234
|
+
BUNDLE_ID="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$APP_PATH/Info.plist" 2>/dev/null || true)"
|
|
235
|
+
[[ -n "$BUNDLE_ID" ]] || { echo "❌ Could not detect CFBundleIdentifier from $APP_PATH/Info.plist; pass --bundle-id" >&2; exit 2; }
|
|
236
|
+
echo "Detected bundle id: $BUNDLE_ID"
|
|
237
|
+
fi
|
|
238
|
+
|
|
239
|
+
step() { echo ""; echo "━━━ $1"; }
|
|
240
|
+
|
|
241
|
+
# ── Simulator: reuse a booted one, else boot (creating from the newest runtime if necessary).
|
|
242
|
+
step "Simulator"
|
|
243
|
+
UDID="$(xcrun simctl list devices booted -j | python3 -c 'import sys,json; d=json.load(sys.stdin); print(next((x["udid"] for v in d["devices"].values() for x in v if x.get("state")=="Booted"), ""))')"
|
|
244
|
+
if [[ -z "$UDID" ]]; then
|
|
245
|
+
UDID="$(xcrun simctl list devices available -j | python3 -c "
|
|
246
|
+
import sys, json
|
|
247
|
+
d = json.load(sys.stdin)
|
|
248
|
+
want = '''$DEVICE'''
|
|
249
|
+
cands = [(rt, x) for rt, v in d['devices'].items() for x in v if x.get('isAvailable', True)]
|
|
250
|
+
named = [x['udid'] for rt, x in cands if x['name'] == want]
|
|
251
|
+
iphones = [x['udid'] for rt, x in sorted(cands, key=lambda p: p[0], reverse=True) if x['name'].startswith('iPhone')]
|
|
252
|
+
print(named[0] if named else (iphones[0] if iphones else ''))
|
|
253
|
+
")"
|
|
254
|
+
if [[ -z "$UDID" ]]; then
|
|
255
|
+
RUNTIME="$(xcrun simctl list runtimes -j | python3 -c 'import sys,json; rts=[r for r in json.load(sys.stdin)["runtimes"] if r.get("isAvailable") and r["platform"]=="iOS"]; print(rts[-1]["identifier"] if rts else "")')"
|
|
256
|
+
[[ -n "$RUNTIME" ]] || { echo "❌ No iOS simulator runtime available" >&2; exit 1; }
|
|
257
|
+
UDID="$(xcrun simctl create "AutoTap CI" "$DEVICE" "$RUNTIME")" || { echo "❌ Could not create simulator" >&2; exit 1; }
|
|
258
|
+
fi
|
|
259
|
+
echo "Booting $UDID …"
|
|
260
|
+
xcrun simctl boot "$UDID" || true
|
|
261
|
+
xcrun simctl bootstatus "$UDID" -b || { echo "❌ Simulator failed to boot" >&2; exit 1; }
|
|
262
|
+
fi
|
|
263
|
+
echo "Simulator: $UDID"
|
|
264
|
+
|
|
265
|
+
# ── Install the app build under test.
|
|
266
|
+
step "Install $BUNDLE_ID"
|
|
267
|
+
xcrun simctl install "$UDID" "$APP_PATH" || { echo "❌ simctl install failed — is $APP_PATH a SIMULATOR build?" >&2; exit 1; }
|
|
268
|
+
|
|
269
|
+
# ── Autonomous exploration (quick-capture builds the harness itself if needed).
|
|
270
|
+
step "Explore ($ACTIONS actions, ${TIMEOUT}s watchdog)"
|
|
271
|
+
set +e
|
|
272
|
+
CAPTURE_ROOT="${AUTOTAP_HOME:-$ROOT}/captures"
|
|
273
|
+
mkdir -p "$CAPTURE_ROOT"
|
|
274
|
+
CAPTURE_DIR="$(mktemp -d "$CAPTURE_ROOT/ci.XXXXXX")"
|
|
275
|
+
TAPP_CAPTURE_DIR="$CAPTURE_DIR" OCQA_PR_TARGET_JSON="$IOS_PR_TARGET_JSON" "$ROOT/scripts/quick-capture.sh" explore "$BUNDLE_ID" --actions "$ACTIONS" --timeout "$TIMEOUT"
|
|
276
|
+
set -e
|
|
277
|
+
MARKERS="$CAPTURE_DIR/ocqa-markers.txt"
|
|
278
|
+
[[ -f "$MARKERS" ]] || { echo "❌ Exploration produced no markers ($MARKERS)" >&2; exit 1; }
|
|
279
|
+
echo "Markers: $MARKERS"
|
|
280
|
+
|
|
281
|
+
# ── Replay committed Flows (each failure becomes a gate reason).
|
|
282
|
+
FLOW_LOG_ARGS=()
|
|
283
|
+
if [[ "${#FLOW_FILES[@]}" -gt 0 ]]; then
|
|
284
|
+
step "Flows"
|
|
285
|
+
FLOW_LOG_DIR="$(mktemp -d /tmp/autotap-ci-flows.XXXXXX)"
|
|
286
|
+
for flow in "${FLOW_FILES[@]}"; do
|
|
287
|
+
name="$(basename "$flow" .yml)"
|
|
288
|
+
log="$FLOW_LOG_DIR/$name.log"
|
|
289
|
+
echo "▶️ $name"
|
|
290
|
+
FLOW_LOG="$log" "$ROOT/scripts/run-flow.sh" "$flow" "$BUNDLE_ID" || true # verdict comes from the log
|
|
291
|
+
FLOW_LOG_ARGS+=(--flow-log "$log")
|
|
292
|
+
done
|
|
293
|
+
elif [[ -n "$FLOWS" ]]; then
|
|
294
|
+
echo "No committed Flows found; continuing with autonomous exploration only."
|
|
295
|
+
fi
|
|
296
|
+
|
|
297
|
+
if [[ "${#CONTRACT_COMPILED_FILES[@]}" -gt 0 ]]; then
|
|
298
|
+
step "Release Contracts"
|
|
299
|
+
CONTRACT_LOG_DIR="$(mktemp -d /tmp/tapp-ci-contract-logs.XXXXXX)"
|
|
300
|
+
for contract in "${CONTRACT_COMPILED_FILES[@]}"; do
|
|
301
|
+
name="$(basename "$contract" .json)"
|
|
302
|
+
log="$CONTRACT_LOG_DIR/$name.log"
|
|
303
|
+
echo "▶️ $name"
|
|
304
|
+
FLOW_LOG="$log" "$ROOT/scripts/run-flow.sh" "$contract" "$BUNDLE_ID" || true
|
|
305
|
+
FLOW_LOG_ARGS+=(--contract-log "$log")
|
|
306
|
+
done
|
|
307
|
+
fi
|
|
308
|
+
|
|
309
|
+
# ── Report + gate.
|
|
310
|
+
step "Gate"
|
|
311
|
+
BASELINE_ARGS=()
|
|
312
|
+
[[ -n "$BASELINE" ]] && BASELINE_ARGS=(--baseline "$BASELINE")
|
|
313
|
+
JSON_ARGS=()
|
|
314
|
+
[[ -n "$JSON_OUT" ]] && JSON_ARGS=(--json-out "$JSON_OUT")
|
|
315
|
+
MD_ARGS=()
|
|
316
|
+
[[ -n "$MD_OUT" ]] && MD_ARGS=(--md-out "$MD_OUT")
|
|
317
|
+
PR_PLAN_ARGS=()
|
|
318
|
+
[[ -n "$PR_PLAN_PATH" ]] && PR_PLAN_ARGS=(--pr-plan "$PR_PLAN_PATH")
|
|
319
|
+
TARGET_KEY_ARGS=()
|
|
320
|
+
[[ -n "$TARGET_KEY" ]] && TARGET_KEY_ARGS=(--target-key "$TARGET_KEY")
|
|
321
|
+
node "$ROOT/mcp-server/src/ci-report.js" --markers "$MARKERS" --fail-on "$FAIL_ON" \
|
|
322
|
+
--html-dir "$CAPTURE_DIR" --label "$BUNDLE_ID" \
|
|
323
|
+
${BASELINE_ARGS[@]+"${BASELINE_ARGS[@]}"} ${JSON_ARGS[@]+"${JSON_ARGS[@]}"} ${MD_ARGS[@]+"${MD_ARGS[@]}"} ${PR_PLAN_ARGS[@]+"${PR_PLAN_ARGS[@]}"} ${TARGET_KEY_ARGS[@]+"${TARGET_KEY_ARGS[@]}"} ${FLOW_LOG_ARGS[@]+"${FLOW_LOG_ARGS[@]}"}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Reclaim disk space consumed by Xcode build artifacts.
|
|
3
|
+
#
|
|
4
|
+
# Default run (no flags): deletes unavailable simulators and stale DerivedData
|
|
5
|
+
# from unrelated projects. Flags unlock more aggressive passes.
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# scripts/cleanup-xcode.sh # safe defaults
|
|
9
|
+
# scripts/cleanup-xcode.sh --dry-run # preview only, nothing deleted
|
|
10
|
+
# scripts/cleanup-xcode.sh --device-support # also wipe iOS DeviceSupport caches
|
|
11
|
+
# scripts/cleanup-xcode.sh --all # everything above + Xcode caches
|
|
12
|
+
#
|
|
13
|
+
# AutoTap-related DerivedData (OCQAHarness, DemoApp, LoginDemo, etc.) is always
|
|
14
|
+
# preserved. The local build/ directory in this repo is never touched.
|
|
15
|
+
set -uo pipefail
|
|
16
|
+
|
|
17
|
+
DRY_RUN=0
|
|
18
|
+
CLEAN_DEVICE_SUPPORT=0
|
|
19
|
+
CLEAN_ALL=0
|
|
20
|
+
|
|
21
|
+
for arg in "$@"; do
|
|
22
|
+
case "$arg" in
|
|
23
|
+
--dry-run) DRY_RUN=1 ;;
|
|
24
|
+
--device-support) CLEAN_DEVICE_SUPPORT=1 ;;
|
|
25
|
+
--all) CLEAN_DEVICE_SUPPORT=1; CLEAN_ALL=1 ;;
|
|
26
|
+
-h|--help)
|
|
27
|
+
sed -n '/^# /s/^# //p' "$0" | head -15
|
|
28
|
+
exit 0 ;;
|
|
29
|
+
*) echo "Unknown flag: $arg (try --help)"; exit 1 ;;
|
|
30
|
+
esac
|
|
31
|
+
done
|
|
32
|
+
|
|
33
|
+
DERIVED="$HOME/Library/Developer/Xcode/DerivedData"
|
|
34
|
+
SIMS="$HOME/Library/Developer/CoreSimulator/Devices"
|
|
35
|
+
DEVICE_SUPPORT="$HOME/Library/Developer/Xcode/iOS DeviceSupport"
|
|
36
|
+
XCODE_CACHES="$HOME/Library/Caches/com.apple.dt.Xcode"
|
|
37
|
+
|
|
38
|
+
# Projects to preserve in DerivedData (all AutoTap-related build products).
|
|
39
|
+
KEEP_PREFIXES=("AutoTap" "OCQAHarness" "DemoApp" "LoginDemo" "WizardDemo"
|
|
40
|
+
"ShopDemo" "RestaurantDemo" "SymbolCache")
|
|
41
|
+
|
|
42
|
+
hr() { printf '%0.s─' {1..60}; echo; }
|
|
43
|
+
human() { du -sh "$1" 2>/dev/null | cut -f1; }
|
|
44
|
+
|
|
45
|
+
run() {
|
|
46
|
+
if [[ $DRY_RUN -eq 1 ]]; then
|
|
47
|
+
echo " [dry-run] $*"
|
|
48
|
+
else
|
|
49
|
+
eval "$@"
|
|
50
|
+
fi
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
echo ""
|
|
54
|
+
hr
|
|
55
|
+
echo " Xcode Cleanup $(date '+%Y-%m-%d %H:%M')"
|
|
56
|
+
[[ $DRY_RUN -eq 1 ]] && echo " DRY RUN — nothing will be deleted"
|
|
57
|
+
hr
|
|
58
|
+
|
|
59
|
+
# ── Before snapshot ──────────────────────────────────────────────────────────
|
|
60
|
+
echo ""
|
|
61
|
+
echo "Before:"
|
|
62
|
+
printf " %-38s %s\n" "CoreSimulator/Devices" "$(human "$SIMS")"
|
|
63
|
+
printf " %-38s %s\n" "Xcode/DerivedData" "$(human "$DERIVED")"
|
|
64
|
+
printf " %-38s %s\n" "Xcode/iOS DeviceSupport" "$(human "$DEVICE_SUPPORT")"
|
|
65
|
+
printf " %-38s %s\n" "Xcode caches" "$(human "$XCODE_CACHES")"
|
|
66
|
+
echo ""
|
|
67
|
+
|
|
68
|
+
# ── 1. Unavailable simulators ─────────────────────────────────────────────────
|
|
69
|
+
echo "1/3 Removing unavailable simulators …"
|
|
70
|
+
UNAVAIL=$(xcrun simctl list devices --json 2>/dev/null | \
|
|
71
|
+
python3 -c '
|
|
72
|
+
import sys, json
|
|
73
|
+
d = json.load(sys.stdin)
|
|
74
|
+
n = sum(1 for v in d["devices"].values() for x in v if not x.get("isAvailable", True))
|
|
75
|
+
print(n)
|
|
76
|
+
')
|
|
77
|
+
echo " Found $UNAVAIL unavailable device(s)."
|
|
78
|
+
if [[ "$UNAVAIL" -gt 0 ]]; then
|
|
79
|
+
run "xcrun simctl delete unavailable"
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
# ── 2. Unrelated DerivedData ──────────────────────────────────────────────────
|
|
83
|
+
echo ""
|
|
84
|
+
echo "2/3 Scanning DerivedData for unrelated projects …"
|
|
85
|
+
STALE=()
|
|
86
|
+
if [[ -d "$DERIVED" ]]; then
|
|
87
|
+
while IFS= read -r -d '' entry; do
|
|
88
|
+
base="$(basename "$entry")"
|
|
89
|
+
keep=0
|
|
90
|
+
for prefix in "${KEEP_PREFIXES[@]}"; do
|
|
91
|
+
if [[ "$base" == "$prefix"* ]]; then
|
|
92
|
+
keep=1; break
|
|
93
|
+
fi
|
|
94
|
+
done
|
|
95
|
+
[[ $keep -eq 0 ]] && STALE+=("$entry")
|
|
96
|
+
done < <(find "$DERIVED" -maxdepth 1 -mindepth 1 -type d -print0)
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
if [[ ${#STALE[@]} -eq 0 ]]; then
|
|
100
|
+
echo " Nothing to remove."
|
|
101
|
+
else
|
|
102
|
+
echo " Will remove ${#STALE[@]} unrelated build folder(s):"
|
|
103
|
+
for entry in "${STALE[@]}"; do
|
|
104
|
+
printf " %-8s %s\n" "$(human "$entry")" "$(basename "$entry")"
|
|
105
|
+
done
|
|
106
|
+
for entry in "${STALE[@]}"; do
|
|
107
|
+
run "rm -rf \"$entry\""
|
|
108
|
+
done
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
# ── 3. iOS DeviceSupport (optional) ──────────────────────────────────────────
|
|
112
|
+
echo ""
|
|
113
|
+
echo "3/3 iOS DeviceSupport …"
|
|
114
|
+
if [[ $CLEAN_DEVICE_SUPPORT -eq 1 ]]; then
|
|
115
|
+
if [[ -d "$DEVICE_SUPPORT" ]]; then
|
|
116
|
+
echo " Removing $(human "$DEVICE_SUPPORT") of device symbol caches …"
|
|
117
|
+
echo " (Xcode re-downloads these automatically when a device is connected.)"
|
|
118
|
+
run "rm -rf \"$DEVICE_SUPPORT\""
|
|
119
|
+
else
|
|
120
|
+
echo " Directory not found — nothing to do."
|
|
121
|
+
fi
|
|
122
|
+
else
|
|
123
|
+
SIZE=$(human "$DEVICE_SUPPORT")
|
|
124
|
+
echo " Skipped ($SIZE on disk). Pass --device-support to remove."
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
# ── 4. Xcode caches (--all only) ─────────────────────────────────────────────
|
|
128
|
+
if [[ $CLEAN_ALL -eq 1 ]]; then
|
|
129
|
+
echo ""
|
|
130
|
+
echo "4/4 Xcode caches …"
|
|
131
|
+
if [[ -d "$XCODE_CACHES" ]]; then
|
|
132
|
+
echo " Removing $(human "$XCODE_CACHES") …"
|
|
133
|
+
run "rm -rf \"$XCODE_CACHES\""
|
|
134
|
+
fi
|
|
135
|
+
fi
|
|
136
|
+
|
|
137
|
+
# ── Simulator data cache (always safe) ───────────────────────────────────────
|
|
138
|
+
SIM_CACHES="$HOME/Library/Developer/CoreSimulator/Caches"
|
|
139
|
+
if [[ -d "$SIM_CACHES" ]] && [[ "$(du -sk "$SIM_CACHES" 2>/dev/null | cut -f1)" -gt 0 ]]; then
|
|
140
|
+
echo ""
|
|
141
|
+
echo " Removing simulator caches ($(human "$SIM_CACHES")) …"
|
|
142
|
+
run "rm -rf \"$SIM_CACHES\""
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
# ── After snapshot ─────────────────────────────────────────────────────────────
|
|
146
|
+
echo ""
|
|
147
|
+
if [[ $DRY_RUN -eq 0 ]]; then
|
|
148
|
+
hr
|
|
149
|
+
echo "After:"
|
|
150
|
+
printf " %-38s %s\n" "CoreSimulator/Devices" "$(human "$SIMS")"
|
|
151
|
+
printf " %-38s %s\n" "Xcode/DerivedData" "$(human "$DERIVED")"
|
|
152
|
+
printf " %-38s %s\n" "Xcode/iOS DeviceSupport" "$(human "$DEVICE_SUPPORT")"
|
|
153
|
+
printf " %-38s %s\n" "Xcode caches" "$(human "$XCODE_CACHES")"
|
|
154
|
+
echo ""
|
|
155
|
+
fi
|
|
156
|
+
|
|
157
|
+
echo "Done."
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Compile one TypeScript release contract for a concrete CI platform. Exit 3
|
|
3
|
+
// means the reviewed contract does not apply to this platform and should be
|
|
4
|
+
// skipped; every other non-zero exit is a configuration error.
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { compileReleaseContract, loadReleaseContractFile } from "../mcp-server/src/release-contract.js";
|
|
8
|
+
|
|
9
|
+
const [source, platform, output] = process.argv.slice(2);
|
|
10
|
+
if (!source || !platform || !output) {
|
|
11
|
+
console.error("usage: compile-contract.js <name.contract.ts> <ios|android|web> <output.json>");
|
|
12
|
+
process.exit(2);
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
const contract = await loadReleaseContractFile(source);
|
|
16
|
+
if (!contract.platforms.includes(platform)) {
|
|
17
|
+
console.log(`skip ${contract.name}: not applicable to ${platform}`);
|
|
18
|
+
process.exit(3);
|
|
19
|
+
}
|
|
20
|
+
const compiled = compileReleaseContract(contract, { platform, sourcePath: path.resolve(source) });
|
|
21
|
+
fs.mkdirSync(path.dirname(path.resolve(output)), { recursive: true });
|
|
22
|
+
fs.writeFileSync(path.resolve(output), JSON.stringify(compiled, null, 2) + "\n");
|
|
23
|
+
console.log(`compiled ${contract.name}: ${compiled.steps.length} deterministic steps`);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.error(error.message || String(error));
|
|
26
|
+
process.exit(2);
|
|
27
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Expand repository-native Task calls into the shared deterministic Flow JSON
|
|
3
|
+
// consumed by every platform driver. No model or customer secret is involved.
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { loadFlowFile } from "../mcp-server/src/flow-runtime.js";
|
|
6
|
+
|
|
7
|
+
const input = process.argv[2] ? path.resolve(process.argv[2]) : "";
|
|
8
|
+
const platform = String(process.argv[3] || "").toLowerCase();
|
|
9
|
+
if (!input) {
|
|
10
|
+
console.error("usage: compile-flow.js <flow.yml|flow.json> [ios|android|web]");
|
|
11
|
+
process.exit(2);
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
process.stdout.write(JSON.stringify(loadFlowFile(input, { platform })));
|
|
15
|
+
} catch (error) {
|
|
16
|
+
console.error(error.message || String(error));
|
|
17
|
+
process.exit(2);
|
|
18
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Corpus sweep list — curated from dkhamsing/open-source-ios-apps across backend classes.
|
|
2
|
+
# Class coverage: self-hostable-service client (login wall), public API (no key),
|
|
3
|
+
# public API (keyed), no-backend/local-first, heavyweight real-world.
|
|
4
|
+
https://github.com/Dimillian/IceCubesApp
|
|
5
|
+
https://github.com/weiran/Hackers
|
|
6
|
+
https://github.com/Ranchero-Software/NetNewsWire
|
|
7
|
+
https://github.com/yattee/yattee
|
|
8
|
+
https://github.com/wikimedia/wikipedia-ios
|
|
9
|
+
https://github.com/mastodon/mastodon-ios
|