nativeapptemplate-agent 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 (106) hide show
  1. package/CLAUDE.md +137 -0
  2. package/LICENSE +21 -0
  3. package/README.md +192 -0
  4. package/dist/adb.d.ts +2 -0
  5. package/dist/adb.d.ts.map +1 -0
  6. package/dist/adb.js +31 -0
  7. package/dist/adb.js.map +1 -0
  8. package/dist/agents/contract-extract.d.ts +26 -0
  9. package/dist/agents/contract-extract.d.ts.map +1 -0
  10. package/dist/agents/contract-extract.js +205 -0
  11. package/dist/agents/contract-extract.js.map +1 -0
  12. package/dist/agents/judge.d.ts +21 -0
  13. package/dist/agents/judge.d.ts.map +1 -0
  14. package/dist/agents/judge.js +119 -0
  15. package/dist/agents/judge.js.map +1 -0
  16. package/dist/agents/planner.d.ts +3 -0
  17. package/dist/agents/planner.d.ts.map +1 -0
  18. package/dist/agents/planner.js +154 -0
  19. package/dist/agents/planner.js.map +1 -0
  20. package/dist/agents/reviewer.d.ts +9 -0
  21. package/dist/agents/reviewer.d.ts.map +1 -0
  22. package/dist/agents/reviewer.js +108 -0
  23. package/dist/agents/reviewer.js.map +1 -0
  24. package/dist/agents/types.d.ts +54 -0
  25. package/dist/agents/types.d.ts.map +1 -0
  26. package/dist/agents/types.js +2 -0
  27. package/dist/agents/types.js.map +1 -0
  28. package/dist/agents/workers/android.d.ts +3 -0
  29. package/dist/agents/workers/android.d.ts.map +1 -0
  30. package/dist/agents/workers/android.js +116 -0
  31. package/dist/agents/workers/android.js.map +1 -0
  32. package/dist/agents/workers/ios.d.ts +3 -0
  33. package/dist/agents/workers/ios.d.ts.map +1 -0
  34. package/dist/agents/workers/ios.js +110 -0
  35. package/dist/agents/workers/ios.js.map +1 -0
  36. package/dist/agents/workers/rails.d.ts +3 -0
  37. package/dist/agents/workers/rails.d.ts.map +1 -0
  38. package/dist/agents/workers/rails.js +145 -0
  39. package/dist/agents/workers/rails.js.map +1 -0
  40. package/dist/dispatch.d.ts +3 -0
  41. package/dist/dispatch.d.ts.map +1 -0
  42. package/dist/dispatch.js +38 -0
  43. package/dist/dispatch.js.map +1 -0
  44. package/dist/env.d.ts +3 -0
  45. package/dist/env.d.ts.map +1 -0
  46. package/dist/env.js +48 -0
  47. package/dist/env.js.map +1 -0
  48. package/dist/index.d.ts +3 -0
  49. package/dist/index.d.ts.map +1 -0
  50. package/dist/index.js +26 -0
  51. package/dist/index.js.map +1 -0
  52. package/dist/ruby.d.ts +2 -0
  53. package/dist/ruby.d.ts.map +1 -0
  54. package/dist/ruby.js +21 -0
  55. package/dist/ruby.js.map +1 -0
  56. package/dist/slug.d.ts +3 -0
  57. package/dist/slug.d.ts.map +1 -0
  58. package/dist/slug.js +11 -0
  59. package/dist/slug.js.map +1 -0
  60. package/dist/stub.d.ts +3 -0
  61. package/dist/stub.d.ts.map +1 -0
  62. package/dist/stub.js +6 -0
  63. package/dist/stub.js.map +1 -0
  64. package/dist/trace.d.ts +3 -0
  65. package/dist/trace.d.ts.map +1 -0
  66. package/dist/trace.js +13 -0
  67. package/dist/trace.js.map +1 -0
  68. package/dist/validation/capture.d.ts +15 -0
  69. package/dist/validation/capture.d.ts.map +1 -0
  70. package/dist/validation/capture.js +141 -0
  71. package/dist/validation/capture.js.map +1 -0
  72. package/dist/validation/discover.d.ts +11 -0
  73. package/dist/validation/discover.d.ts.map +1 -0
  74. package/dist/validation/discover.js +113 -0
  75. package/dist/validation/discover.js.map +1 -0
  76. package/dist/validation/index.d.ts +17 -0
  77. package/dist/validation/index.d.ts.map +1 -0
  78. package/dist/validation/index.js +9 -0
  79. package/dist/validation/index.js.map +1 -0
  80. package/dist/validation/launch.d.ts +21 -0
  81. package/dist/validation/launch.d.ts.map +1 -0
  82. package/dist/validation/launch.js +109 -0
  83. package/dist/validation/launch.js.map +1 -0
  84. package/dist/validation/layer1.d.ts +16 -0
  85. package/dist/validation/layer1.d.ts.map +1 -0
  86. package/dist/validation/layer1.js +98 -0
  87. package/dist/validation/layer1.js.map +1 -0
  88. package/dist/validation/layer2.d.ts +17 -0
  89. package/dist/validation/layer2.d.ts.map +1 -0
  90. package/dist/validation/layer2.js +168 -0
  91. package/dist/validation/layer2.js.map +1 -0
  92. package/dist/validation/layer3.d.ts +22 -0
  93. package/dist/validation/layer3.d.ts.map +1 -0
  94. package/dist/validation/layer3.js +96 -0
  95. package/dist/validation/layer3.js.map +1 -0
  96. package/dist/validation/stage1.d.ts +17 -0
  97. package/dist/validation/stage1.d.ts.map +1 -0
  98. package/dist/validation/stage1.js +71 -0
  99. package/dist/validation/stage1.js.map +1 -0
  100. package/dist/validation/visual-judge.d.ts +25 -0
  101. package/dist/validation/visual-judge.d.ts.map +1 -0
  102. package/dist/validation/visual-judge.js +89 -0
  103. package/dist/validation/visual-judge.js.map +1 -0
  104. package/package.json +69 -0
  105. package/scripts/ruby/.gitkeep +0 -0
  106. package/scripts/ruby/rename.rb +221 -0
package/CLAUDE.md ADDED
@@ -0,0 +1,137 @@
1
+ # CLAUDE.md
2
+
3
+ Project-wide Claude Code instructions. Keep this file short — it's loaded into context every session. For deep context, read the linked docs on demand.
4
+
5
+ ## What this project is
6
+
7
+ `nativeapptemplate-agent` — a Claude Code agent that turns a natural-language SaaS spec (e.g. "a walk-in queue for a barbershop") into a working three-platform implementation: Rails 8.1 API + native SwiftUI iOS + native Jetpack Compose Android. Built during the Built with Opus 4.7 Claude Code Hackathon, April 21–27, 2026.
8
+
9
+ **Read `docs/SPEC.md` before making non-trivial changes.** It's the source of truth for architecture, three-layer validation, operation paths (adapt vs. replace), and hackathon success criteria.
10
+
11
+ ## Model
12
+
13
+ - Use `claude-opus-4-7` for all agent invocations.
14
+ - Requires Claude Agent SDK `>= 0.2.111`.
15
+ - Do not set `thinking.type.enabled` — use `thinking: {type: "adaptive"}` instead (Opus 4.7 uses adaptive thinking).
16
+
17
+ ## Stack
18
+
19
+ - **Host language:** TypeScript (Claude Agent SDK `@anthropic-ai/claude-agent-sdk`)
20
+ - **Ruby subprocesses** for Rails AST / ERB / migration work (called from TypeScript)
21
+ - **External MCP:** [`mobile-next/mobile-mcp`](https://github.com/mobile-next/mobile-mcp) for iOS Simulator + Android Emulator UI automation (`npx -y @mobilenext/mobile-mcp@latest`)
22
+ - **Node 22+ required.**
23
+
24
+ ## Coding conventions
25
+
26
+ - TypeScript `strict: true`; no implicit `any`.
27
+ - Prefer functions over classes — introduce a class only when state + lifecycle justify it (e.g. a long-lived MCP client wrapper). Stateless transforms are functions.
28
+ - Ruby subprocesses: shell out via `execFile("ruby", ["scripts/ruby/<script>.rb", ...])` from a thin wrapper in `src/ruby.ts`. Pass structured data as JSON on stdin/stdout, not positional CLI args. Each script must be self-contained and re-entrant.
29
+ - No comments explaining *what* the code does — name things well instead. Reserve comments for non-obvious *why*.
30
+
31
+ ## Substrate (what the agent operates on)
32
+
33
+ The agent works on either the free (MIT-licensed) edition or the paid edition — the same code path handles both, validated end-to-end. Pick which substrate to use by pointing the env vars at the corresponding repos:
34
+
35
+ - `$NATIVEAPPTEMPLATE_API` — Rails 8.1 API repo (`nativeapptemplateapi`, single repo serves both editions; multi-tenancy + invitations are conditional features)
36
+ - `$NATIVEAPPTEMPLATE_IOS` — SwiftUI iOS repo. Free: `NativeAppTemplate-Free-iOS` (Swift 15,311 LOC). Paid: `NativeAppTemplate` (Swift 23,003 LOC, adds org switching, invitations, role permissions).
37
+ - `$NATIVEAPPTEMPLATE_ANDROID` — Jetpack Compose Android repo. Free: `NativeAppTemplate-Free-Android` (Kotlin 19,521 LOC). Paid: `NativeAppTemplate` (Kotlin 28,401 LOC).
38
+
39
+ The renamer's substitution rules (Shop / Shopkeeper / ItemTag / NativeAppTemplate) apply identically to both editions. Paid-only concepts (Account, Member, Invitation) aren't in the rename plan, so paid features survive the generation intact. Both editions hit the same Rails OpenAPI server, so the reviewer's three-way diff works against either.
40
+
41
+ **Substrate repos are read-only from the agent's perspective.** The agent never commits, pushes, edits, or runs destructive commands inside the substrate directories. To customize, the agent copies the substrate into `./out/<spec-slug>/{rails,ios,android}/` as fresh git-initialized project directories, and works exclusively inside `./out/` thereafter. Treat the substrate paths the same way you'd treat a read-only mount.
42
+
43
+ ## Domain model (do not misread)
44
+
45
+ - `Shop` → per-tenant primary resource (not "store", not "shop" in e-commerce sense — a physical location with a walk-in queue)
46
+ - `Shopkeeper` → the authenticated user/owner of a Shop
47
+ - `ItemTag` (aka Number Tag) → a queue entry, attached to a Shop
48
+ - **ItemTag has TWO states: `Idled` ↔ `Completed`.** It's a toggle, not a three-state machine. If a user's spec wants three states (e.g. "in-service"), extend the machine rather than assume it.
49
+ - Free edition is single-organization (personal account is transparent). Paid-edition features (multi-tenancy, invitations, roles, org switching) are out of scope for the hackathon.
50
+
51
+ ## Repository state
52
+
53
+ Pre-implementation as of this writing: `src/` and `scripts/ruby/` are empty, and there is no `package.json`. The npm commands below are the target interface — don't try to run them until the scaffolding lands. Check `src/` before assuming.
54
+
55
+ ## Commands
56
+
57
+ ```bash
58
+ # Build & test the agent (run from this repo root — once package.json exists)
59
+ npm run build
60
+ npm run test
61
+ npm run dev -- "your spec here"
62
+
63
+ # Substrate commands — run from the matching generated project dir in ./out/<slug>/
64
+ bin/dev # Rails API
65
+ xcodebuild -scheme NativeAppTemplate -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build # iOS
66
+ ./gradlew assembleDebug && ./gradlew installDebug # Android
67
+
68
+ # Validation (run from this repo root)
69
+ rg -n "Shop|Shopkeeper|ItemTag" ./out/<slug>/ # Layer 1 rename completeness check
70
+ cloc --vcs=git "$NATIVEAPPTEMPLATE_API" # LOC measurement for a specific substrate repo
71
+
72
+ # mobile-mcp — invoked via natural language through Claude Code
73
+ # "Take a screenshot", "Tap on Sign Up", "List elements on screen", etc.
74
+ ```
75
+
76
+ ## Operations the agent performs
77
+
78
+ Three clean operations on the substrate (details in `docs/SPEC.md` section 4):
79
+
80
+ 1. **Rename the skeleton** — Shop → Clinic, Shopkeeper → Staff, propagated coherently across Rails + Swift + Kotlin.
81
+ 2. **Adapt or replace the domain module** — keep ItemTag for queue variants; strip and insert a new resource for non-queue SaaS.
82
+ 3. **Drive the build green** — Rails tests + xcodebuild + gradlew must all pass.
83
+
84
+ ## Validation (three layers, gated)
85
+
86
+ 1. **Structural** — ripgrep for leftover domain tokens + OpenAPI contract parity (Rails ↔ iOS ↔ Android).
87
+ 2. **Runtime** — Rails boots, iOS builds & launches, Android builds & launches (Stage 1); UI-driven CRUD scenario via mobile-mcp (Stage 2).
88
+ 3. **Semantic** — Opus 4.7 as judge (text + vision) scoring against a structured rubric. Three runs per screenshot, take the median.
89
+
90
+ ## Guardrails
91
+
92
+ - **Self-repair loop hard-capped at 5 iterations** per generated project. On exceed, surface residuals and exit.
93
+ - Known-cryptic failure modes: Jetpack Compose compilation, Hilt DI. Slow down and verify rather than pattern-match on those.
94
+ - **Do not invent tests for the generated code.** The substrate already has tests; use them.
95
+ - **Never modify the substrate repos** — clone them fresh into `./out/<slug>/{rails,ios,android}` before editing.
96
+ - Token budget: keep per-sub-agent context under 500k tokens to leave headroom for self-repair loops.
97
+
98
+ ## Output layout
99
+
100
+ ```
101
+ ./out/<spec-slug>/
102
+ ├── rails/ # customized Rails API (git-initialized)
103
+ ├── ios/ # customized iOS project (git-initialized)
104
+ └── android/ # customized Android project (git-initialized)
105
+ ```
106
+
107
+ Each is an independent, buildable git repo.
108
+
109
+ ## Packaging
110
+
111
+ Ships as:
112
+ - `npx nativeapptemplate-agent "your spec"` — standalone CLI
113
+ - A Claude Code plugin (slash commands + skills)
114
+
115
+ ## Hackathon success criteria (quick reminder — details in docs/SPEC.md section 11)
116
+
117
+ **Must-have for the demo video:**
118
+ - Clinic queue spec produces 3 projects passing Layer 1 + Layer 2 Stage 1
119
+ - Layer 3 rubric scores above threshold for captured home-screen shots
120
+ - 90s demo video of end-to-end run with validation report
121
+
122
+ **Stretch:** restaurant waitlist + task tracker also pass; self-repair loop fixes at least one real failure in the video.
123
+
124
+ ## Sibling docs
125
+
126
+ - `docs/SPEC.md` — full technical specification (read first for any non-trivial change)
127
+ - `ROADMAP.md` — post-hackathon direction, OSS vs hosted tracks, durable scope boundaries
128
+ - `docs-private/GTM.md` — business strategy (relevant when discussing product surface, pricing, or Phase A/B transitions). Gitignored, not for publication.
129
+ - `docs-private/PREP-CHECKLIST.md` — pre-hackathon setup (should already be all-green by the time development starts). Gitignored.
130
+
131
+ ## Anti-patterns to avoid
132
+
133
+ - **Don't reimplement mobile UI automation.** Use mobile-mcp. The hackathon is not about inventing device automation.
134
+ - **Don't extend scope beyond the queue / simple-CRUD-SaaS family** during the hackathon week. See `docs/SPEC.md` section 9 for the full non-goals list.
135
+ - **Don't target the paid edition.** The deliverable operates only on the MIT-licensed free edition so judges can reproduce it end-to-end.
136
+ - **Don't skip the validation layers to save time.** They are the demo story. A run that green-builds without passing Layer 3 is a failed run.
137
+ - **Don't edit, commit, push, or run `git clean` / `rm` inside `$NATIVEAPPTEMPLATE_API`, `$NATIVEAPPTEMPLATE_IOS`, or `$NATIVEAPPTEMPLATE_ANDROID`.** Those are the developer's working copies of the free-edition substrate, possibly shared with other projects on the same machine. Copy them into `./out/<slug>/` first; change nothing in place. If unsure whether a command is safe, ask.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Daisuke Adachi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,192 @@
1
+ # NativeAppTemplate Agent
2
+
3
+ A Claude Code agent that turns a natural-language spec — something as informal as `"a walk-in queue for a barbershop"` — into a working three-platform implementation:
4
+
5
+ - **Rails 8.1 API**
6
+ - **native SwiftUI iOS**
7
+ - **native Jetpack Compose Android**
8
+
9
+ Coherent across all three, in under an hour.
10
+
11
+ > **Status:** Hackathon build. Developed during [Built with Opus 4.7: a Claude Code Hackathon](https://cerebralvalley.ai/e/built-with-4-7-hackathon) (April 21–27, 2026). Active development in progress — expect breaking changes through the end of April.
12
+
13
+ ---
14
+
15
+ ## Why this exists
16
+
17
+ Most "AI builds an app" tools stop at a single web frontend. The real pain for anyone shipping a mobile product is that the *same* domain has to be implemented three times — multi-tenant Rails API, native iOS, native Android — each with its own idioms, and keeping them consistent is where weeks disappear.
18
+
19
+ Classic mobile boilerplates sell "save 12–16 weeks of setup." AI coding tools have compressed that value to 2–3 weeks of AI-assisted work. The durable problem that remains — even with AI — is **cross-platform coherence**: keeping a Rails API, a native iOS client, and a native Android client all consistent under iteration, with no contract drift, no forgotten rename, no divergent localized copy.
20
+
21
+ This agent is an answer to that: turn a boilerplate into a generator that produces coherent three-platform implementations on demand, with structural and semantic validation built in.
22
+
23
+ ## What it does
24
+
25
+ Point the agent at a natural-language spec:
26
+
27
+ ```
28
+ a walk-in clinic queue for small veterinary practices
29
+ ```
30
+
31
+ It will:
32
+
33
+ 1. **Parse the spec** into a structured domain (entities, fields, relationships, state machines).
34
+ 2. **Copy the free-edition substrate** (three MIT-licensed repos covering Rails + iOS + Android) into `./out/<spec-slug>/{rails,ios,android}/`.
35
+ 3. **Rename the skeleton** — `Shop → Clinic`, `Shopkeeper → Vet`, etc. — consistently across Ruby migrations, Swift models, Kotlin data classes, policies, tests, and localized copy.
36
+ 4. **Adapt or replace the domain module** — keep `ItemTag` for walk-in queue variants; strip and insert a new resource for non-queue SaaS.
37
+ 5. **Drive the build green** — `bin/rails test`, `xcodebuild test`, `./gradlew test` must all pass before the agent exits.
38
+ 6. **Validate the output** across three layers (structural, runtime, semantic). Details in [`docs/SPEC.md`](./docs/SPEC.md) section 6.
39
+
40
+ ## Demo
41
+
42
+ Three demo specs, both adapt and replace paths, all four validation layers green end-to-end:
43
+
44
+ | Spec | Domain entity (post-rename) | Path | Result |
45
+ |---|---|---|---|
46
+ | `"a walk-in clinic queue for small veterinary practices"` | `ItemTag → Patient`, `Shop → Clinic`, `Shopkeeper → Vet` | adapt | Layer 1 3/3 · Layer 2 3/3 · Layer 3 2/2 · Reviewer PASS |
47
+ | `"a restaurant waitlist for casual dining"` | `Shop → Restaurant`, `Shopkeeper → Host` | adapt | Layer 1 3/3 · Layer 2 3/3 · Layer 3 2/2 · Reviewer PASS |
48
+ | `"a personal task tracker with due dates"` | `ItemTag → Todo` (replaces queue entry entirely) | replace | Layer 1 3/3 · Layer 2 3/3 · Layer 3 2/2 · Reviewer PASS |
49
+
50
+ Layer 2 ran in build mode — real `xcodebuild build` and `./gradlew assembleDebug`, full app builds installed on iPhone 17 simulator and Android emulator. Layer 3 captured the home-screen via `xcrun simctl io booted screenshot` / `adb exec-out screencap` and judged against the rubric using Opus 4.7 vision (median of 3 samples per criterion).
51
+
52
+ The agent works on either the **free (MIT) edition** or the **paid edition** without code changes — the same pipeline handles both substrates; multi-tenant features (org switching, invitations, role permissions) survive the rename pipeline when targeting paid.
53
+
54
+ <table>
55
+ <tr>
56
+ <td align="center"><img src="docs/images/demo-ios-vet-clinic-queue.png" width="300"><br><sub>iOS — Vet Clinic Queue welcome screen</sub></td>
57
+ <td align="center"><img src="docs/images/demo-android-vet-clinic-queue.png" width="300"><br><sub>Android — Vet Clinic Queue welcome screen</sub></td>
58
+ </tr>
59
+ </table>
60
+
61
+ Both screenshots are real captures from the booted iOS Simulator and Android emulator post-`./gradlew assembleDebug` / `xcodebuild build`, after the agent installed and launched the generated app.
62
+
63
+ ## Architecture
64
+
65
+ ```mermaid
66
+ flowchart LR
67
+ Spec["Natural-language spec<br/>(e.g. walk-in clinic queue)"] --> Agent
68
+ subgraph Agent["Claude Code Agent · Opus 4.7"]
69
+ Planner --> Workers
70
+ Workers --> Reviewer
71
+ Reviewer --> Judge
72
+ end
73
+ Substrate[("Free-edition substrate<br/>Rails · iOS · Android<br/>READ-ONLY")] -. copy .-> Out
74
+ Agent --> Out["./out/[slug]/<br/>rails / ios / android"]
75
+ Out --> L1["Layer 1 — Structural"]
76
+ L1 --> L2["Layer 2 — Runtime + mobile-mcp"]
77
+ L2 --> L3["Layer 3 — Vision judge"]
78
+ ```
79
+
80
+ ## Substrate
81
+
82
+ The agent operates on the free, MIT-licensed edition of NativeAppTemplate — three public repos:
83
+
84
+ | Repo | Stack | LOC |
85
+ |---|---|---|
86
+ | [`nativeapptemplateapi`](https://github.com/nativeapptemplate/nativeapptemplateapi) | Rails 8.1, PostgreSQL, `devise_token_auth`, `pundit`, `acts_as_tenant` | 7,687 (Ruby) |
87
+ | [`NativeAppTemplate-Free-iOS`](https://github.com/nativeapptemplate/NativeAppTemplate-Free-iOS) | 100% SwiftUI, `@Observable`, MVVM, Liquid Glass design, iOS 26.2+ | 15,311 (Swift) |
88
+ | [`NativeAppTemplate-Free-Android`](https://github.com/nativeapptemplate/NativeAppTemplate-Free-Android) | 100% Kotlin, 100% Jetpack Compose, Hilt, Retrofit2, API 26+ | 19,521 (Kotlin) |
89
+
90
+ Combined ~42.5k LOC. Extracted from [MyTurnTag Creator](https://myturntag.com), a walk-in queue-management SaaS live on both app stores since 2024.
91
+
92
+ ## Usage (target interface)
93
+
94
+ > **Not yet functional — hackathon build in progress.** The interface below is the target; `npx` won't work until v0.1 ships at the end of hackathon week.
95
+
96
+ ```bash
97
+ # Standalone CLI — must-have for the hackathon demo
98
+ npx nativeapptemplate-agent "a walk-in clinic queue for small veterinary practices"
99
+
100
+ # Stretch specs the agent is also designed to handle
101
+ npx nativeapptemplate-agent "a restaurant waitlist for casual dining"
102
+ npx nativeapptemplate-agent "a personal task tracker with due dates"
103
+
104
+ # Generated output appears under ./out/<slug>/
105
+ tree ./out/clinic-queue/
106
+ # ├── rails/ ← Rails 8.1 API, git-initialized, buildable
107
+ # ├── ios/ ← SwiftUI iOS project, buildable
108
+ # └── android/ ← Jetpack Compose Android project, buildable
109
+ ```
110
+
111
+ The agent will also be available as a Claude Code plugin.
112
+
113
+ ## Requirements
114
+
115
+ - [Node.js](https://nodejs.org) 22+
116
+ - [Claude Agent SDK](https://github.com/anthropics/claude-agent-sdk-typescript) v0.2.111 or later (needed for Opus 4.7)
117
+ - An [Anthropic API key](https://console.anthropic.com/settings/keys) with access to `claude-opus-4-7`, exported as `ANTHROPIC_API_KEY`:
118
+ ```bash
119
+ export ANTHROPIC_API_KEY="sk-ant-..."
120
+ ```
121
+ The Anthropic SDK reads this env var automatically; no other config is required. See [Security](#security) below for storage recommendations.
122
+ - Local checkouts of the three substrate repos, referenced via environment variables:
123
+ ```bash
124
+ export NATIVEAPPTEMPLATE_API="/path/to/nativeapptemplateapi"
125
+ export NATIVEAPPTEMPLATE_IOS="/path/to/NativeAppTemplate-Free-iOS"
126
+ export NATIVEAPPTEMPLATE_ANDROID="/path/to/NativeAppTemplate-Free-Android"
127
+ ```
128
+ A starter [`/.env.example`](./.env.example) lists all the variables in one place.
129
+ - For runtime validation (Layer 2 onwards): Xcode 26.3+ with iOS 26.2+ simulator, Android SDK with API 26+ emulator
130
+ - For UI automation: [`mobile-next/mobile-mcp`](https://github.com/mobile-next/mobile-mcp) (installed automatically as a Claude Code MCP server)
131
+
132
+ ### Optional flags
133
+
134
+ - `NATIVEAPPTEMPLATE_VISUAL=1` — opts the run into Stage 1 visual judging (Layer 3). When set, Layer 2 runs in **build mode** instead of fast mode (full `xcodebuild build` + `./gradlew assembleDebug`), then for each platform the agent installs the app on the booted sim/emulator, captures the home screen, and judges it with Opus 4.7 vision against `DEFAULT_STAGE1_RUBRIC`. Adds 60-180s per platform depending on cold-build time. Requires a sim/emulator booted for each platform you want judged. Off by default — `npm run dev` keeps the existing fast path.
135
+ - `NATIVEAPPTEMPLATE_AGENT_ANTHROPIC_KEY` — dedicated workspace key, see [Security](#security).
136
+ - `ANDROID_SERIAL` — when more than one Android device/emulator is attached (e.g. a physical device plus a running emulator), `adb` standard practice is to set `ANDROID_SERIAL=<serial>` to disambiguate. The agent honors this transparently because it runs `adb` directly. Run `adb devices` to list serials. Visual-judge runs with multiple Android targets attached will error with `more than one device/emulator` if this isn't set.
137
+
138
+ The agent resolves `adb` to a known-good binary in this priority order: `$ANDROID_HOME/platform-tools/adb`, `$ANDROID_SDK_ROOT/platform-tools/adb`, `~/Library/Android/sdk/platform-tools/adb` (Android Studio default), `/Applications/android-sdk-macosx/platform-tools/adb`, `/opt/homebrew/bin/adb`, `/usr/local/bin/adb`, then PATH. This avoids surprises like a stale `~/.apportable/SDK/bin/adb` (i386, won't exec on Apple Silicon) shadowing a working `adb` on PATH.
139
+
140
+ ## Validation (three layers)
141
+
142
+ The agent doesn't just generate code and exit — it validates the output.
143
+
144
+ 1. **Structural.** `ripgrep` for leftover domain tokens; OpenAPI contract parity check between Rails, iOS networking, and Android repository layers. A silent rename inconsistency fails the run before any tests execute.
145
+ 2. **Runtime.** Boot the generated Rails server; build and launch iOS and Android apps; drive a scripted CRUD scenario via [`mobile-next/mobile-mcp`](https://github.com/mobile-next/mobile-mcp). Any 4xx/5xx or unhandled client error fails the run.
146
+ 3. **Semantic.** Opus 4.7 as judge — scores whether the generated code and rendered UI actually express the intended domain. Vision judges read simulator/emulator screenshots directly.
147
+
148
+ See [`docs/SPEC.md`](./docs/SPEC.md) for the full design.
149
+
150
+ ## Security
151
+
152
+ `ANTHROPIC_API_KEY` is the only sensitive secret the agent needs.
153
+
154
+ **Workspace isolation (optional but recommended).** If you also use Claude Code or other Anthropic SDK apps, create a [dedicated workspace](https://console.anthropic.com/settings/workspaces) with its own key + spend cap and export it as `NATIVEAPPTEMPLATE_AGENT_ANTHROPIC_KEY`. The agent prefers that var over `ANTHROPIC_API_KEY` when set, so a runaway loop hits the workspace cap instead of your overall tier limit, and revoking it doesn't break Claude Code login.
155
+
156
+ **Recommended storage** (best to most convenient):
157
+
158
+ - **macOS Keychain via 1Password CLI** — `op read "op://Personal/Anthropic/key"` resolved at session start; no key on disk in plaintext.
159
+ - **[`direnv`](https://direnv.net/)** — per-project `.envrc`, loaded only when you `cd` in. Keep `.envrc` outside any git-tracked dotfiles repo, or `.gitignore` it.
160
+ - **A gitignored secrets file sourced from your shell rc** — e.g. `[ -r ~/.config/zsh/secrets.zsh ] && source ~/.config/zsh/secrets.zsh`. Set `chmod 600` on the file.
161
+ - **`.env` next to the project** — copy [`.env.example`](./.env.example) to `.env` (already gitignored along with `.env*.local`) and the agent loads it on startup. Shell exports take precedence over `.env`, so you can override per-run with `FOO=x npm run dev`. Lowest friction; easiest to leak — avoid in shared repos. `chmod 600 .env` on shared machines.
162
+
163
+ **Don't** paste a real key into shell history (`HISTFILE` captures it), commit a `.env`, or echo the key into a non-private channel.
164
+
165
+ The agent strips `ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN`, and `NATIVEAPPTEMPLATE_AGENT_ANTHROPIC_KEY` from the environment of every subprocess it spawns — Ruby scripts, `git`, `psql`, `xcodebuild`, `gradlew`, the future mobile-mcp client. Keys are only seen by the Anthropic SDK in the Node process. Set spend limits on your API workspace as a backstop, and rotate the key if you suspect leak.
166
+
167
+ ## Project docs
168
+
169
+ - [`docs/SPEC.md`](./docs/SPEC.md) — full technical specification
170
+ - [`ROADMAP.md`](./ROADMAP.md) — where this project is headed, OSS vs hosted, what stays out of scope
171
+ - [`CLAUDE.md`](./CLAUDE.md) — Claude Code project instructions (read if you're running Claude Code against this repo)
172
+
173
+ ## Contributing
174
+
175
+ During hackathon week (April 21–27, 2026) the repository moves quickly and breaking changes are expected. After that, contributions are welcome via standard GitHub issues and PRs. A `CONTRIBUTING.md` with detailed guidelines will land once the hackathon dust settles.
176
+
177
+ If you spot something broken before then, feel free to open an issue — just don't be surprised if the fix lands as part of a larger rewrite.
178
+
179
+ ## License
180
+
181
+ MIT. See [`LICENSE`](./LICENSE).
182
+
183
+ ## Acknowledgments
184
+
185
+ - [Anthropic](https://www.anthropic.com) for Claude Opus 4.7 and Claude Code
186
+ - [Cerebral Valley](https://cerebralvalley.ai) for running the hackathon
187
+ - [`mobile-next/mobile-mcp`](https://github.com/mobile-next/mobile-mcp) for making mobile UI automation actually workable from an agent
188
+ - [MyTurnTag Creator](https://myturntag.com) users, whose real-world queue management taught me which abstractions survive and which don't
189
+
190
+ ---
191
+
192
+ *Built solo in Tokyo.*
package/dist/adb.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export declare function resolveAdbPath(): string;
2
+ //# sourceMappingURL=adb.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adb.d.ts","sourceRoot":"","sources":["../src/adb.ts"],"names":[],"mappings":"AAgBA,wBAAgB,cAAc,IAAI,MAAM,CAavC"}
package/dist/adb.js ADDED
@@ -0,0 +1,31 @@
1
+ import { existsSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ // Some dev machines have a stale `adb` shadowing the modern one on PATH —
5
+ // the canonical case is /Users/<u>/.apportable/SDK/bin/adb, an i386 binary
6
+ // from 2014 that fails to exec on Apple Silicon with "Unknown system error
7
+ // -86". Resolve to a known-good adb instead of trusting PATH order:
8
+ //
9
+ // 1. $ANDROID_HOME/platform-tools/adb (canonical env var)
10
+ // 2. $ANDROID_SDK_ROOT/platform-tools/adb (legacy spelling)
11
+ // 3. ~/Library/Android/sdk/platform-tools/adb (Android Studio default on macOS)
12
+ // 4. /Applications/android-sdk-macosx/platform-tools/adb (older macOS standalone install)
13
+ // 5. /opt/homebrew/bin/adb (Homebrew on Apple Silicon)
14
+ // 6. /usr/local/bin/adb (Homebrew on Intel)
15
+ // 7. "adb" (fall back to PATH lookup)
16
+ export function resolveAdbPath() {
17
+ const candidates = [
18
+ process.env['ANDROID_HOME'] ? join(process.env['ANDROID_HOME'], "platform-tools", "adb") : null,
19
+ process.env['ANDROID_SDK_ROOT'] ? join(process.env['ANDROID_SDK_ROOT'], "platform-tools", "adb") : null,
20
+ join(homedir(), "Library", "Android", "sdk", "platform-tools", "adb"),
21
+ "/Applications/android-sdk-macosx/platform-tools/adb",
22
+ "/opt/homebrew/bin/adb",
23
+ "/usr/local/bin/adb",
24
+ ];
25
+ for (const candidate of candidates) {
26
+ if (candidate && existsSync(candidate))
27
+ return candidate;
28
+ }
29
+ return "adb";
30
+ }
31
+ //# sourceMappingURL=adb.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adb.js","sourceRoot":"","sources":["../src/adb.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,0EAA0E;AAC1E,2EAA2E;AAC3E,2EAA2E;AAC3E,oEAAoE;AACpE,EAAE;AACF,sEAAsE;AACtE,oEAAoE;AACpE,oFAAoF;AACpF,6FAA6F;AAC7F,8EAA8E;AAC9E,sEAAsE;AACtE,6EAA6E;AAC7E,MAAM,UAAU,cAAc;IAC5B,MAAM,UAAU,GAAG;QACjB,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,EAAE,gBAAgB,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI;QAC/F,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,EAAE,gBAAgB,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI;QACvG,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,SAAS,EAAE,KAAK,EAAE,gBAAgB,EAAE,KAAK,CAAC;QACrE,qDAAqD;QACrD,uBAAuB;QACvB,oBAAoB;KACrB,CAAC;IACF,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,IAAI,SAAS,IAAI,UAAU,CAAC,SAAS,CAAC;YAAE,OAAO,SAAS,CAAC;IAC3D,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -0,0 +1,26 @@
1
+ export type Endpoint = {
2
+ method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
3
+ path: string;
4
+ };
5
+ export type ContractDiff = {
6
+ railsOnly: Endpoint[];
7
+ iosOrphan: Endpoint[];
8
+ androidOrphan: Endpoint[];
9
+ iosOnly: Endpoint[];
10
+ androidOnly: Endpoint[];
11
+ };
12
+ export type CanonicalizationContext = {
13
+ role: string;
14
+ };
15
+ export declare function canonicalizeEndpoint(endpoint: Endpoint, ctx: CanonicalizationContext): Endpoint;
16
+ export declare function diffContracts(rails: readonly Endpoint[], ios: readonly Endpoint[], android: readonly Endpoint[], ctx: CanonicalizationContext): ContractDiff;
17
+ export type RailsContract = {
18
+ openapiVersion: string;
19
+ title: string;
20
+ endpoints: Endpoint[];
21
+ schemaCount: number;
22
+ };
23
+ export declare function extractRailsContract(railsDir: string): Promise<RailsContract | null>;
24
+ export declare function extractAndroidEndpoints(androidDir: string): Promise<Endpoint[]>;
25
+ export declare function extractIosEndpoints(iosDir: string): Promise<Endpoint[]>;
26
+ //# sourceMappingURL=contract-extract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract-extract.d.ts","sourceRoot":"","sources":["../../src/agents/contract-extract.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,QAAQ,GAAG;IACrB,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,CAAC;IACpD,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IAIzB,SAAS,EAAE,QAAQ,EAAE,CAAC;IAGtB,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,aAAa,EAAE,QAAQ,EAAE,CAAC;IAG1B,OAAO,EAAE,QAAQ,EAAE,CAAC;IACpB,WAAW,EAAE,QAAQ,EAAE,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IAIpC,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAYF,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,QAAQ,EAClB,GAAG,EAAE,uBAAuB,GAC3B,QAAQ,CAsBV;AAID,wBAAgB,aAAa,CAC3B,KAAK,EAAE,SAAS,QAAQ,EAAE,EAC1B,GAAG,EAAE,SAAS,QAAQ,EAAE,EACxB,OAAO,EAAE,SAAS,QAAQ,EAAE,EAC5B,GAAG,EAAE,uBAAuB,GAC3B,YAAY,CA8Bd;AAED,MAAM,MAAM,aAAa,GAAG;IAC1B,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAMF,wBAAsB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAsC1F;AAMD,wBAAsB,uBAAuB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAmBrF;AAKD,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAgB7E"}
@@ -0,0 +1,205 @@
1
+ import { readFile, readdir } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { parse as parseYaml } from "yaml";
4
+ const TENANT_PLACEHOLDER = "{account_id}";
5
+ const UUID_RE = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i;
6
+ const OPENAPI_PARAM_RE = /^\{[^}]+\}$/;
7
+ const SWIFT_INTERPOLATION_RE = /^\\\([^)]+\)$/;
8
+ // Strip tenant prefix, /api/v1, role segment, and normalize path parameter
9
+ // names to {*} so the three platforms' encodings reduce to the same
10
+ // comparable string. Rails OpenAPI server URLs (e.g. `/api/v1/vet`) are
11
+ // already factored out by extractRailsContract, so Rails endpoints arrive
12
+ // here with just the per-operation path.
13
+ export function canonicalizeEndpoint(endpoint, ctx) {
14
+ const role = ctx.role.toLowerCase();
15
+ const segments = endpoint.path.split("/").filter(Boolean);
16
+ // Drop tenant segment (literal placeholder or runtime UUID).
17
+ if (segments[0] === TENANT_PLACEHOLDER || (segments[0] && UUID_RE.test(segments[0]))) {
18
+ segments.shift();
19
+ }
20
+ // Drop /api/v<N>/.
21
+ if (segments[0] === "api") {
22
+ segments.shift();
23
+ if (segments[0] && /^v\d+$/.test(segments[0]))
24
+ segments.shift();
25
+ }
26
+ // Drop role segment.
27
+ if (segments[0] === role)
28
+ segments.shift();
29
+ // Normalize path-param names to {*} so {shopId} === {id} === \(id).
30
+ const normalized = segments.map((s) => OPENAPI_PARAM_RE.test(s) || SWIFT_INTERPOLATION_RE.test(s) ? "{*}" : s);
31
+ return { method: endpoint.method, path: "/" + normalized.join("/") };
32
+ }
33
+ // Three-way diff. Endpoints are first canonicalized so cross-platform
34
+ // encoding differences don't show up as drift.
35
+ export function diffContracts(rails, ios, android, ctx) {
36
+ const canon = (e) => canonicalizeEndpoint(e, ctx);
37
+ const key = (e) => `${e.method} ${e.path}`;
38
+ const railsSet = new Map(rails.map(canon).map((e) => [key(e), e]));
39
+ const iosSet = new Map(ios.map(canon).map((e) => [key(e), e]));
40
+ const androidSet = new Map(android.map(canon).map((e) => [key(e), e]));
41
+ const railsOnly = [];
42
+ for (const [k, e] of railsSet) {
43
+ if (!iosSet.has(k) && !androidSet.has(k))
44
+ railsOnly.push(e);
45
+ }
46
+ const iosOrphan = [];
47
+ for (const [k, e] of iosSet) {
48
+ if (!railsSet.has(k))
49
+ iosOrphan.push(e);
50
+ }
51
+ const androidOrphan = [];
52
+ for (const [k, e] of androidSet) {
53
+ if (!railsSet.has(k))
54
+ androidOrphan.push(e);
55
+ }
56
+ const iosOnly = [];
57
+ for (const [k, e] of iosSet) {
58
+ if (railsSet.has(k) && !androidSet.has(k))
59
+ iosOnly.push(e);
60
+ }
61
+ const androidOnly = [];
62
+ for (const [k, e] of androidSet) {
63
+ if (railsSet.has(k) && !iosSet.has(k))
64
+ androidOnly.push(e);
65
+ }
66
+ return { railsOnly, iosOrphan, androidOrphan, iosOnly, androidOnly };
67
+ }
68
+ const HTTP_METHODS = new Set(["GET", "POST", "PUT", "PATCH", "DELETE"]);
69
+ // Rails: parse out/<slug>/rails/docs/openapi.yaml. Returns null if the file
70
+ // is missing or doesn't have the canonical OpenAPI 3.x shape.
71
+ export async function extractRailsContract(railsDir) {
72
+ const specPath = join(railsDir, "docs", "openapi.yaml");
73
+ let raw;
74
+ try {
75
+ raw = await readFile(specPath, "utf8");
76
+ }
77
+ catch {
78
+ return null;
79
+ }
80
+ let doc;
81
+ try {
82
+ doc = parseYaml(raw);
83
+ }
84
+ catch {
85
+ return null;
86
+ }
87
+ if (!isRecord(doc))
88
+ return null;
89
+ const openapiVersion = typeof doc["openapi"] === "string" ? doc["openapi"] : null;
90
+ if (!openapiVersion)
91
+ return null;
92
+ const info = isRecord(doc["info"]) ? doc["info"] : {};
93
+ const title = typeof info["title"] === "string" ? info["title"] : "";
94
+ const paths = isRecord(doc["paths"]) ? doc["paths"] : {};
95
+ const endpoints = [];
96
+ for (const [path, value] of Object.entries(paths)) {
97
+ if (!isRecord(value))
98
+ continue;
99
+ for (const verb of Object.keys(value)) {
100
+ const upper = verb.toUpperCase();
101
+ if (HTTP_METHODS.has(upper)) {
102
+ endpoints.push({ method: upper, path });
103
+ }
104
+ }
105
+ }
106
+ const components = isRecord(doc["components"]) ? doc["components"] : {};
107
+ const schemas = isRecord(components["schemas"]) ? components["schemas"] : {};
108
+ const schemaCount = Object.keys(schemas).length;
109
+ return { openapiVersion, title, endpoints, schemaCount };
110
+ }
111
+ // Android: walk app/src/main/kotlin/**/data/**/*Api.kt, extract Retrofit
112
+ // annotations (@GET("path"), @POST("path"), etc.). The substrate's auth
113
+ // data layer also lives under data/login/, so capture all *Api.kt under
114
+ // data/.
115
+ export async function extractAndroidEndpoints(androidDir) {
116
+ const dataDir = join(androidDir, "app", "src", "main", "kotlin");
117
+ const apiFiles = await collectFiles(dataDir, (path) => /\/data\/.*Api\.kt$/.test(path));
118
+ const endpoints = [];
119
+ const re = /@(GET|POST|PUT|PATCH|DELETE)\(\s*"([^"]+)"\s*\)/g;
120
+ for (const file of apiFiles) {
121
+ let raw;
122
+ try {
123
+ raw = await readFile(file, "utf8");
124
+ }
125
+ catch {
126
+ continue;
127
+ }
128
+ let match;
129
+ while ((match = re.exec(raw)) !== null) {
130
+ endpoints.push({ method: match[1], path: match[2] });
131
+ }
132
+ }
133
+ return endpoints;
134
+ }
135
+ // iOS: walk **/Networking/**/*Request.swift and Login/*Request.swift, where
136
+ // each struct has matched `var method: HTTPMethod { .METHOD }` and
137
+ // `var path: String { "..." }` getters. Pair them by struct order.
138
+ export async function extractIosEndpoints(iosDir) {
139
+ const appDir = await findAppRoot(iosDir);
140
+ if (!appDir)
141
+ return [];
142
+ const requestFiles = await collectFiles(appDir, (path) => /Request\.swift$/.test(path));
143
+ const endpoints = [];
144
+ for (const file of requestFiles) {
145
+ let raw;
146
+ try {
147
+ raw = await readFile(file, "utf8");
148
+ }
149
+ catch {
150
+ continue;
151
+ }
152
+ endpoints.push(...parseSwiftRequests(raw));
153
+ }
154
+ return endpoints;
155
+ }
156
+ function parseSwiftRequests(raw) {
157
+ // Split on `struct ... {` boundaries; within each chunk, find the first
158
+ // `.METHOD` enum reference and the first `"..."` string literal that follows
159
+ // a `var path` declaration. Crude but robust for the substrate's layout.
160
+ const structs = raw.split(/\nstruct\s+\w+\b/).slice(1);
161
+ const out = [];
162
+ for (const chunk of structs) {
163
+ const methodMatch = chunk.match(/var\s+method:\s*HTTPMethod\s*\{\s*\.(GET|POST|PUT|PATCH|DELETE)\s*\}/);
164
+ const pathMatch = chunk.match(/var\s+path:\s*String\s*\{\s*"([^"]+)"\s*\}/);
165
+ if (methodMatch && pathMatch) {
166
+ out.push({ method: methodMatch[1], path: pathMatch[1] });
167
+ }
168
+ }
169
+ return out;
170
+ }
171
+ async function findAppRoot(iosDir) {
172
+ const entries = await readdir(iosDir, { withFileTypes: true }).catch(() => []);
173
+ for (const e of entries) {
174
+ if (e.isDirectory() && !e.name.endsWith(".xcodeproj") && !e.name.endsWith("Tests") && !e.name.startsWith(".")) {
175
+ const candidate = join(iosDir, e.name);
176
+ const sub = await readdir(candidate).catch(() => []);
177
+ if (sub.some((n) => n === "Networking" || n === "Login"))
178
+ return candidate;
179
+ }
180
+ }
181
+ return null;
182
+ }
183
+ async function collectFiles(root, predicate) {
184
+ const out = [];
185
+ async function walk(dir) {
186
+ const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
187
+ for (const e of entries) {
188
+ const full = join(dir, e.name);
189
+ if (e.isDirectory()) {
190
+ if (e.name === ".git" || e.name === "build" || e.name === ".gradle" || e.name === "Pods")
191
+ continue;
192
+ await walk(full);
193
+ }
194
+ else if (predicate(full)) {
195
+ out.push(full);
196
+ }
197
+ }
198
+ }
199
+ await walk(root);
200
+ return out;
201
+ }
202
+ function isRecord(v) {
203
+ return typeof v === "object" && v !== null && !Array.isArray(v);
204
+ }
205
+ //# sourceMappingURL=contract-extract.js.map