
Init Xcode App
- 23 installs
- 339 repo stars
- Updated August 4, 2026
- glebis/claude-skills
Scaffold a standalone SwiftUI app via XcodeGen for macOS, iOS, or iOS+watchOS with house conventions, a Swift Testing target, and opt-in modules.
About
Generates a native SwiftUI project from project.yml (committed .xcodeproj) with swiftformat/swiftlint, optional CloudKit/CI/Release/Push modules, and a build+test gate. A developer uses it to start a new Mac/iOS/watchOS app with the loop-closer toolchain preloaded.
- XcodeGen-based with per-shape unsigned build and test gate
- Opt-in CloudKit, CI, Release, Push modules plus JTBD context
Init Xcode App by the numbers
- 23 all-time installs (skills.sh)
- Ranked #731 of 1,039 Mobile Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/glebis/claude-skills --skill init-xcode-appAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 23 |
|---|---|
| repo stars | ★ 339 |
| Last updated | August 4, 2026 |
| Repository | glebis/claude-skills ↗ |
What it does
Scaffold a standalone SwiftUI app via XcodeGen for macOS, iOS, or iOS+watchOS with house conventions, a Swift Testing target, and opt-in modules.
Files
init-xcode-app
Scaffolds a native SwiftUI app with XcodeGen (project.yml → generated, committed .xcodeproj), pre-loaded with the conventions and loop-closer toolchain (xcsift, the Xcode MCP, swiftformat/lint). The lighter native sibling of init-tauri-app.
When to use
- "init an xcode project", "scaffold a swiftui app", "new macOS/iOS app".
Prerequisites (verify before scaffolding)
xcodegenon PATH (xcodegen --version;brew install xcodegenif missing).- Xcode + CLT (
xcodebuild -version).
Procedure
1. Gather inputs (AskUserQuestion)
- App name (PascalCase, e.g.
Tidepool) →<Name>; lowercase →<name>. - Bundle-id prefix (default
com.glebkalinin) →<bundlePrefix>. - DEVELOPMENT_TEAM (optional — blank allowed) →
<TEAM>. - Target shape:
ios|macos|ios-watchos. - Modules (multi-select): CloudKit · CI · Release · Push.
- JTBD artifact (optional): path to a
jtbd.json(else auto-discover./jtbd.json,~/jtbd/<name>/jtbd.json). See 1.5. - Target directory (default
~/ai_projects/<name>). Abort if non-empty.
1.5 Ingest JTBD (optional, additive)
Identical contract to init-tauri-app: resolve (explicit → ./jtbd.json → ~/jtbd/<name>/jtbd.json), confirm via hook, validate (render-jtbd.sh exits 3 → skip), pre-fill name. Artifacts written in Step 3.
2. Render core
Copy assets/core/common/* into the project (renames: gitignore→.gitignore, swiftformat→.swiftformat, CLAUDE.md→.claude/CLAUDE.md). Copy assets/core/shapes/<shape>/* into the project root. For macos, also copy assets/core/shapes/ios/Sources + Tests (macos reuses the same SwiftUI sources). Substitute <Name>/<name>/<bundlePrefix>/<TEAM> in every copied file. Blank <TEAM> → leave the placeholder line DEVELOPMENT_TEAM: "".
If a JTBD artifact was confirmed: render assets/jtbd/PRODUCT.md.template→docs/PRODUCT.md, guardrails-check.md.template→docs/internal/guardrails-check.md, insert agents-product-section.md.template into AGENTS.md after the first heading, copy the artifact to project-root jtbd.json. See assets/jtbd/jtbd-map.md.
3. Compose selected modules
For each selected module open assets/modules/<m>/INSERT.md and follow it: it lists source files to copy and a project-fragment.yml to merge into project.yml (merge into the existing targets.<Name> and entitlements: maps — never overwrite). CloudKit + Push both extend the same entitlements: map.
4. Generate + verify (the gate)
xcodegen generateThen the unsigned build gate by shape:
- ios / ios-watchos:
xcodebuild build -scheme <Name> -destination 'generic/platform=iOS Simulator' - macos:
xcodebuild build -scheme <Name> -destination 'platform=macOS' CODE_SIGNING_ALLOWED=NO
Then xcodebuild test against the same destination for the test target. Both must pass.
5. Handoff
Offer git init && git add -A && git commit. Print summary: shape, modules, signing note (device/release signing is a human step), next command (open <Name>.xcodeproj).
Agent guide — Xcode / Swift project
Canonical instructions for any coding agent (Claude Code, Codex, Cursor) working in this repo. CLAUDE.md just points here.
Project files: this app uses XcodeGen — edit project.yml, then xcodegen generate. Never hand-edit <Name>.xcodeproj/project.pbxproj (it's regenerated).
Native code from Rust? If you need a Swift static lib callable from a Rust/Tauri backend, use the swift-sidecar module in init-tauri-app, not this skill.
The loop (how you verify your own work)
1. Edit Swift. 2. Build + read errors as structured JSON — never eyeball raw xcodebuild. 3. Fix from file:line:col. Repeat until clean. 4. Run tests. Then, for UI, look at the result.
Build & test
Two paths — prefer the Xcode MCP when Xcode is open, fall back to CLI:
- Xcode MCP (already wired at user scope, `xcrun mcpbridge`) —
BuildProject,RunAllTests/RunSomeTests,GetBuildLog, and `RenderPreview` which returns a real SwiftUI screenshot so you can see UI iterations. This is the primary loop when Xcode is running. - CLI (headless / CI / Xcode closed) — always pipe through
xcsiftfor token-lean JSON:
xcodebuild -scheme <Scheme> -destination 'platform=iOS Simulator,name=iPhone 16' build 2>&1 | xcsift
xcodebuild -scheme <Scheme> -destination 'platform=iOS Simulator,name=iPhone 16' test 2>&1 | xcsift
swift test # for SPM packages — clean output, no wrapper neededSimulator (UI automation)
xcrun simctl is fully scriptable; for tap/swipe/screenshot loops add the XcodeBuildMCP server (see .mcp.json).
xcrun simctl boot "iPhone 16"
xcrun simctl install booted <App.app>
xcrun simctl launch booted <bundle.id>
xcrun simctl io booted screenshot /tmp/sim.png # feed back to yourself to "see" the UILint / format (deterministic gates)
swiftformat .
swiftlint --strictHard rules / human gates
- Never hand-edit `.pbxproj`. Adding files to targets is unreliable. If this project uses XcodeGen/Tuist, edit the YAML/Swift spec and regenerate. Otherwise flag target-membership changes for the human.
- Code signing, provisioning, App Store submission stay human steps. Validate, don't sign.
- Storyboards/XIBs are opaque — prefer SwiftUI for anything you author.
- Simulator ≠ device (unlimited memory, ideal network). Necessary, not sufficient.
Recommended skills
Community Swift skills (Apple's mcpbridge skills export was empty on this machine):
- Paul Hudson's
swift-agent-skills—swiftui-pro,swift-concurrency,swift-testing.
npx skills add https://github.com/twostraws/swiftui-agent-skill --skill swiftui-pro
Project instructions
See [AGENTS.md](../AGENTS.md) — the canonical agent guide for this repo. Follow it exactly.
Contributing
- Read AGENTS.md first.
- Edit
project.yml, thenxcodegen generate— never touch.pbxproj. - Build loop:
xcodebuild build -scheme <Name> | xcsift. Format:swiftformat .. Lint:swiftlint. - Signing for device/release is a manual step (see AGENTS.md).
# macOS
.DS_Store
# Xcode
build/
**/xcuserdata/
**/DerivedData/
*.xcuserstate
# SwiftPM
.build/
# Agent/tooling caches
.enzyme/
.enzyme-embeddings/
docs/internal/
# NOTE: the XcodeGen-generated <Name>.xcodeproj IS committed (deterministic output).
<Name>
A native SwiftUI app (XcodeGen). See AGENTS.md for the developer/agent guide.
Develop
xcodegen generate
open <Name>.xcodeproj--swiftversion 6.0
--indent 4
--maxwidth 100
--self remove
--commas inline
disabled_rules:
- trailing_whitespace
opt_in_rules:
- empty_count
- closure_spacing
excluded:
- build
- DerivedData
line_length:
warning: 120
error: 200
name: <Name>
options:
bundleIdPrefix: <bundlePrefix>
deploymentTarget:
iOS: "17.0"
watchOS: "10.0"
settings:
DEVELOPMENT_TEAM: "<TEAM>"
CODE_SIGN_STYLE: Automatic
targets:
<Name>Shared:
type: framework
platform: iOS
sources: [{ path: Sources/Shared }]
settings:
PRODUCT_BUNDLE_IDENTIFIER: <bundlePrefix>.<name>.shared
GENERATE_INFOPLIST_FILE: YES
<Name>SharedWatch:
type: framework
platform: watchOS
sources: [{ path: Sources/Shared }]
settings:
PRODUCT_BUNDLE_IDENTIFIER: <bundlePrefix>.<name>.shared.watch
GENERATE_INFOPLIST_FILE: YES
<Name>:
type: application
platform: iOS
sources: [{ path: Sources/iPhone }]
dependencies:
- target: <Name>Shared
embed: true
- target: <Name>Watch
embed: true
settings:
PRODUCT_BUNDLE_IDENTIFIER: <bundlePrefix>.<name>
GENERATE_INFOPLIST_FILE: YES
INFOPLIST_KEY_UILaunchScreen_Generation: YES
<Name>Watch:
type: application
platform: watchOS
sources: [{ path: Sources/Watch }]
dependencies:
- target: <Name>SharedWatch
embed: true
info:
path: Sources/Watch/Info.plist
properties:
WKApplication: true
WKCompanionAppBundleIdentifier: <bundlePrefix>.<name>
settings:
PRODUCT_BUNDLE_IDENTIFIER: <bundlePrefix>.<name>.watch
LD_RUNPATH_SEARCH_PATHS: ["$(inherited)", "@executable_path/Frameworks"]
<Name>Tests:
type: bundle.unit-test
platform: iOS
sources: [{ path: Tests }]
dependencies:
- target: <Name>Shared
settings:
GENERATE_INFOPLIST_FILE: YES
schemes:
<Name>:
build:
targets:
<Name>: all
<Name>Shared: all
<Name>Tests: [test]
run: { config: Debug }
test: { config: Debug, targets: [<Name>Tests] }
<Name>Watch:
build:
targets:
<Name>Watch: all
<Name>SharedWatch: all
run: { config: Debug }
import SwiftUI
import <Name>Shared
@main
struct <Name>App: App {
@StateObject private var sync = SyncService()
var body: some Scene {
WindowGroup { ContentView().environmentObject(sync) }
}
}
import SwiftUI
import <Name>Shared
struct ContentView: View {
@EnvironmentObject private var sync: SyncService
var body: some View {
NavigationStack {
List(sync.items) { item in
VStack(alignment: .leading) {
Text(item.title).font(.headline)
Text(item.detail).font(.subheadline).foregroundStyle(.secondary)
}
}
.navigationTitle("<Name>")
}
}
}
import Foundation
public struct Item: Identifiable, Codable, Hashable {
public let id: UUID
public let title: String
public let detail: String
public init(id: UUID = UUID(), title: String, detail: String) {
self.id = id
self.title = title
self.detail = detail
}
public static let samples = [
Item(title: "First", detail: "A sample item."),
Item(title: "Second", detail: "Another sample item."),
]
}
import Foundation
public final class SyncService: ObservableObject {
@Published public private(set) var items: [Item]
public init(items: [Item] = Item.samples) { self.items = items }
public func reload() { items = Item.samples }
}
import SwiftUI
import <Name>SharedWatch
@main
struct <Name>WatchApp: App {
@StateObject private var sync = SyncService()
var body: some Scene {
WindowGroup { WatchContentView().environmentObject(sync) }
}
}
import SwiftUI
import <Name>SharedWatch
struct WatchContentView: View {
@EnvironmentObject private var sync: SyncService
var body: some View {
List(sync.items) { Text($0.title) }
}
}
import Testing
@testable import <Name>Shared
struct ItemTests {
@Test func samplesArePresent() {
#expect(Item.samples.count == 2)
#expect(Item.samples.first?.title == "First")
}
}
name: <Name>
options:
bundleIdPrefix: <bundlePrefix>
deploymentTarget:
iOS: "17.0"
settings:
DEVELOPMENT_TEAM: "<TEAM>"
CODE_SIGN_STYLE: Automatic
targets:
<Name>:
type: application
platform: iOS
sources:
- path: Sources/App
settings:
PRODUCT_BUNDLE_IDENTIFIER: <bundlePrefix>.<name>
GENERATE_INFOPLIST_FILE: YES
INFOPLIST_KEY_UILaunchScreen_Generation: YES
<Name>Tests:
type: bundle.unit-test
platform: iOS
sources:
- path: Tests
dependencies:
- target: <Name>
settings:
GENERATE_INFOPLIST_FILE: YES
schemes:
<Name>:
build:
targets:
<Name>: all
<Name>Tests: [test]
run:
config: Debug
test:
config: Debug
targets: [<Name>Tests]
import SwiftUI
@main
struct <Name>App: App {
var body: some Scene {
WindowGroup { ContentView() }
}
}
import SwiftUI
struct ContentView: View {
private let items = Item.samples
var body: some View {
NavigationStack {
List(items) { item in
NavigationLink(item.title) { DetailView(item: item) }
}
.navigationTitle("<Name>")
}
}
}
import SwiftUI
struct DetailView: View {
let item: Item
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text(item.title).font(.title)
Text(item.detail).foregroundStyle(.secondary)
Spacer()
}
.padding()
.navigationTitle(item.title)
}
}
import Foundation
struct Item: Identifiable, Codable, Hashable {
let id: UUID
let title: String
let detail: String
init(id: UUID = UUID(), title: String, detail: String) {
self.id = id
self.title = title
self.detail = detail
}
static let samples = [
Item(title: "First", detail: "A sample item."),
Item(title: "Second", detail: "Another sample item."),
]
}
import Testing
@testable import <Name>
struct ItemTests {
@Test func samplesArePresent() {
#expect(Item.samples.count == 2)
#expect(Item.samples.first?.title == "First")
}
}
name: <Name>
options:
bundleIdPrefix: <bundlePrefix>
deploymentTarget:
macOS: "14.0"
settings:
DEVELOPMENT_TEAM: "<TEAM>"
CODE_SIGN_STYLE: Automatic
targets:
<Name>:
type: application
platform: macOS
sources:
- path: Sources/App
settings:
PRODUCT_BUNDLE_IDENTIFIER: <bundlePrefix>.<name>
GENERATE_INFOPLIST_FILE: YES
<Name>Tests:
type: bundle.unit-test
platform: macOS
sources:
- path: Tests
dependencies:
- target: <Name>
settings:
GENERATE_INFOPLIST_FILE: YES
schemes:
<Name>:
build:
targets:
<Name>: all
<Name>Tests: [test]
run:
config: Debug
test:
config: Debug
targets: [<Name>Tests]
## Product context
> {{hook}}
**For** {{jtbd.situation}} — **so that** {{jtbd.outcome}}.
**Must NOT do (from JTBD guardrails):**
<!-- each:guardrails -->
- {{item}}
<!-- /each -->
Full brief: [docs/PRODUCT.md](./docs/PRODUCT.md) · Source JTBD: {{__source_path__}}
{ "hook": "no name or jtbd block here", "needs": { "functional": ["x"] }
{
"name": "tidepool",
"hook": "Capture a fleeting idea by voice and get a buildable spec before the moment fades.",
"jtbd": {
"situation": "a founder has an idea away from their desk",
"motivation": "articulate it clearly enough for agents to act on",
"outcome": "a structured spec exists within minutes, no re-explaining"
},
"problem": { "what_hurts": "static tools don't feed forward", "cost_today": "hours of re-explanation per project" },
"needs": {
"functional": ["voice-friendly capture", "structured JSON output"],
"emotional": ["feel like talking to a sharp thinker"],
"social": ["share a credible one-pager"]
},
"switch_forces": {
"push": "the gap between idea and spec kills momentum",
"pull": "specs that feed forward into code",
"habit": "Figma/Docs muscle memory",
"anxiety": "trusting an AI-framed spec over real empathy"
},
"before_after": { "before": "idea lives in your head", "after": "a spec that IS the handoff" },
"scenarios": [ { "title": "Bike commute", "vignette": "speaks the idea while riding; parks with a spec" } ],
"guardrails": ["one project per session", "never fabricate switch forces"],
"evidence": { "source": "interview", "quotes": ["\"I want to do it while riding the bike.\""], "weaknesses": [] },
"open_questions": ["is /plan needed between brainstorm and code?"],
"version": 1
}
# Guardrails review checklist
Manual pre-release pass. One box per JTBD guardrail — confirm the release does not violate it.
<!-- each:guardrails -->
- [ ] {{item}}
<!-- /each -->
Source JTBD: {{__source_path__}}
JTBD → project mapping
Shared by all init-* skills. Source schema: solopreneur-vault/references/jtbd-schema.md.
Field → destination
| Source field(s) | Destination |
|---|---|
name, hook | README tagline; PRODUCT.md title |
jtbd.{situation,motivation,outcome} | PRODUCT.md "The Job"; AGENTS.md product section |
problem.{what_hurts,cost_today} | PRODUCT.md "Problem" |
needs.{functional,emotional,social} | PRODUCT.md "Needs" |
switch_forces.* | PRODUCT.md "Switch forces" |
before_after.* | PRODUCT.md "Before / After" |
scenarios[] | PRODUCT.md "Scenarios" |
guardrails[] | AGENTS.md "Must NOT do"; PRODUCT.md "Guardrails"; guardrails-check.md |
evidence.quotes[] | PRODUCT.md "Evidence" |
open_questions[] | PRODUCT.md "Open questions" |
source path, version | PRODUCT.md + AGENTS.md footers (Source JTBD: <path>) |
| (whole file) | copied verbatim to project-root jtbd.json |
Rendering rules
- Scalars:
{{field}}(dotted, e.g.{{jtbd.situation}}). - Arrays: a block delimited by
<!-- each:FIELD -->...<!-- /each -->containing one{{item}}
line; the renderer repeats it per element. scenarios[] items are objects: use {{item.title}} and {{item.vignette}}.
- Missing optional field → render an empty string; an empty array → omit the whole
eachblock. - Required fields (
name,hook,jtbd) — if absent, ingestion is skipped entirely (see SKILL.md 1.5).
This asset group is duplicated ininit-tauri-appandinit-xcode-appand must be kept
byte-identical. The only per-skill difference is where agents-product-section.md.templateis injected into that skill's AGENTS.md (after the first heading).
# {{name}} — Product Brief
> {{hook}}
## The Job
- **When** {{jtbd.situation}}
- **I want to** {{jtbd.motivation}}
- **So I can** {{jtbd.outcome}}
## Problem
- **What hurts:** {{problem.what_hurts}}
- **Cost today:** {{problem.cost_today}}
## Needs
**Functional**
<!-- each:needs.functional -->
- {{item}}
<!-- /each -->
**Emotional**
<!-- each:needs.emotional -->
- {{item}}
<!-- /each -->
**Social**
<!-- each:needs.social -->
- {{item}}
<!-- /each -->
## Switch forces
- **Push:** {{switch_forces.push}}
- **Pull:** {{switch_forces.pull}}
- **Habit:** {{switch_forces.habit}}
- **Anxiety:** {{switch_forces.anxiety}}
## Before / After
- **Before:** {{before_after.before}}
- **After:** {{before_after.after}}
## Scenarios
<!-- each:scenarios -->
- **{{item.title}}** — {{item.vignette}}
<!-- /each -->
## Guardrails (must NOT do)
<!-- each:guardrails -->
- {{item}}
<!-- /each -->
## Evidence
<!-- each:evidence.quotes -->
- {{item}}
<!-- /each -->
## Open questions
<!-- each:open_questions -->
- {{item}}
<!-- /each -->
---
Source JTBD: {{__source_path__}} (schema v{{version}})
name: ci
on: [push, pull_request]
jobs:
build:
runs-on: macos-15
steps:
- uses: actions/checkout@v4
- run: brew install xcodegen swiftlint
- run: xcodegen generate
- run: swiftlint
- run: xcodebuild build -scheme <Name> -destination 'generic/platform=iOS Simulator' CODE_SIGNING_ALLOWED=NO
- run: xcodebuild test -scheme <Name> -destination 'platform=iOS Simulator,name=iPhone 16' CODE_SIGNING_ALLOWED=NO
Insert: xcodebuild CI
1. Copy ci.yml → .github/workflows/ci.yml, substitute <Name>. 2. For a macOS app, change both -destination values to 'platform=macOS'. 3. Verify locally: bash -n is N/A (YAML); run xcodegen generate && xcodebuild build -scheme <Name> -destination 'generic/platform=iOS Simulator' CODE_SIGNING_ALLOWED=NO.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.icloud-container-identifiers</key>
<array><string>iCloud.<bundlePrefix>.<name></string></array>
<key>com.apple.developer.icloud-services</key>
<array><string>CloudKit</string></array>
</dict>
</plist>
import CloudKit
import Foundation
/// Minimal CloudKit store stub. Container id must match the entitlement.
public final class CloudKitStore {
private let container: CKContainer
public init(identifier: String = "iCloud.<bundlePrefix>.<name>") {
self.container = CKContainer(identifier: identifier)
}
/// Fetches the account status; expand with record fetch/save as needed.
public func accountStatus() async throws -> CKAccountStatus {
try await container.accountStatus()
}
}
Insert: CloudKit + entitlements
1. Copy App.entitlements → <Name>.entitlements (substitute <Name>/<bundlePrefix>/<name>). 2. Copy CloudKitStore.swift → the app's source dir (Sources/App or Sources/Shared for ios-watchos). 3. Merge project-fragment.yml into targets.<Name> in project.yml (add the entitlements: and dependencies: keys; if a dependencies: list already exists, append). 4. Requires a real DEVELOPMENT_TEAM to build signed; the unsigned simulator gate still compiles. 5. Verify: xcodegen generate && xcodebuild build -scheme <Name> -destination 'generic/platform=iOS Simulator'.
# Merge into targets.<Name>:
entitlements:
path: <Name>.entitlements
properties:
com.apple.developer.icloud-container-identifiers:
- iCloud.<bundlePrefix>.<name>
com.apple.developer.icloud-services:
- CloudKit
dependencies:
- sdk: CloudKit.framework
Insert: Push notifications
1. Copy registration.swift → the app source dir. Add @UIApplicationDelegateAdaptor(PushDelegate.self) var delegate to the App struct. 2. Merge project-fragment.yml into targets.<Name> — add aps-environment to the existing entitlements.properties map (do not overwrite a CloudKit entitlements block if present; merge keys), and add INFOPLIST_KEY_UIBackgroundModes to the target's settings map. Do NOT add a top-level info: block: with GENERATE_INFOPLIST_FILE: YES XcodeGen requires info.path and will fail to decode the spec — UIBackgroundModes must go through the INFOPLIST_KEY_* build setting instead. 3. iOS only (no aps-environment on macOS without extra setup). Requires a real team to build signed. 4. Verify: xcodegen generate && xcodebuild build -scheme <Name> -destination 'generic/platform=iOS Simulator'.
# Merge into targets.<Name>:
entitlements:
properties:
aps-environment: development
settings:
# With GENERATE_INFOPLIST_FILE: YES the synthesized Info.plist is configured via
# INFOPLIST_KEY_* build settings — do NOT add a top-level `info:` block (XcodeGen
# requires `info.path`, which conflicts with the generated plist and fails decoding).
INFOPLIST_KEY_UIBackgroundModes: remote-notification
import SwiftUI
/// Registers for remote notifications on launch. Attach via @UIApplicationDelegateAdaptor.
final class PushDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
application.registerForRemoteNotifications()
return true
}
func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
// Forward the token to your server / CloudKit subscription.
}
}
#!/usr/bin/env bash
set -euo pipefail
SCHEME="<Name>"
ARCHIVE="build/<Name>.xcarchive"
xcodegen generate
xcodebuild archive -scheme "$SCHEME" -archivePath "$ARCHIVE" -destination 'generic/platform=iOS'
xcodebuild -exportArchive -archivePath "$ARCHIVE" \
-exportOptionsPlist ExportOptions.plist -exportPath build/export
echo "Archive + export complete: build/export"
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key><string>app-store-connect</string>
<key>teamID</key><string><TEAM></string>
<key>signingStyle</key><string>automatic</string>
</dict>
</plist>
Insert: Release / archive
1. Copy archive.sh → scripts/archive.sh (chmod +x), ExportOptions.plist → project root. Substitute <Name>/<TEAM>. 2. macOS apps: change -destination 'generic/platform=iOS' to 'generic/platform=macOS' and add a notarization step (xcrun notarytool submit); set method to developer-id. 3. Requires a real DEVELOPMENT_TEAM + signing assets — this is a human-run release step, not the build gate. 4. Verify syntax only: bash -n scripts/archive.sh.
#!/usr/bin/env bash
# Render a jtbd template by substituting fields from a jtbd.json.
# Usage: render-jtbd.sh <jtbd.json> <template> <source_path_label>
# Prints the rendered text to stdout. Exit 3 = invalid/missing required fields.
set -euo pipefail
JSON="$1"; TPL="$2"; SRC="${3:-$1}"
node - "$JSON" "$TPL" "$SRC" <<'NODE'
const fs = require('fs');
const [json, tpl, src] = process.argv.slice(2);
let data;
try { data = JSON.parse(fs.readFileSync(json, 'utf8')); }
catch (e) { console.error('invalid JSON'); process.exit(3); }
if (!data.name || !data.hook || !data.jtbd) { console.error('missing required fields'); process.exit(3); }
data.__source_path__ = src;
const get = (obj, path) => path.split('.').reduce((o,k)=> (o==null?undefined:o[k]), obj);
let t = fs.readFileSync(tpl, 'utf8');
// expand each-blocks
t = t.replace(/<!-- each:([\w.]+) -->\n([\s\S]*?)<!-- \/each -->\n?/g, (_, field, body) => {
const arr = get(data, field);
if (!Array.isArray(arr) || arr.length === 0) return '';
return arr.map(el => body.replace(/\{\{item(?:\.([\w]+))?\}\}/g,
(_, k) => String(k ? (el?.[k] ?? '') : el))).join('');
});
// scalars
t = t.replace(/\{\{([\w.]+)\}\}/g, (_, f) => { const v = get(data, f); return v==null ? '' : String(v); });
process.stdout.write(t);
NODE
#!/usr/bin/env bash
# Scaffolds a throwaway project for one shape and verifies xcodegen + unsigned xcodebuild build.
# Usage: smoke-xcode.sh <ios|macos|ios-watchos>
set -euo pipefail
SHAPE="$1"; SKILL=~/ai_projects/claude-skills/init-xcode-app
NAME="Smoke"; LOWER="smoke"; PREFIX="com.example"; TEAM=""
TMP="$(mktemp -d)"; cd "$TMP"
echo "=== $SHAPE in $TMP ==="
# core common
cp "$SKILL"/assets/core/common/AGENTS.md AGENTS.md
mkdir -p .claude && cp "$SKILL"/assets/core/common/CLAUDE.md .claude/CLAUDE.md
cp "$SKILL"/assets/core/common/gitignore .gitignore
cp "$SKILL"/assets/core/common/swiftformat .swiftformat
cp "$SKILL"/assets/core/common/swiftlint.yml .swiftlint.yml
# shape
cp -R "$SKILL/assets/core/shapes/$SHAPE/." .
# macos reuses ios sources
if [ "$SHAPE" = "macos" ]; then cp -R "$SKILL"/assets/core/shapes/ios/Sources .; cp -R "$SKILL"/assets/core/shapes/ios/Tests .; fi
# substitute placeholders in all text files (rename <Name>* files too)
find . -type f \( -name "*.swift" -o -name "*.yml" -o -name "*.md" -o -name "*.plist" \) -print0 \
| xargs -0 sed -i '' -e "s/<Name>/$NAME/g" -e "s/<name>/$LOWER/g" -e "s/<bundlePrefix>/$PREFIX/g" -e "s/<TEAM>/$TEAM/g"
# rename files that carried <Name> in their filename
find . -depth -name "*<Name>*" | while read -r f; do mv "$f" "$(echo "$f" | sed "s/<Name>/$NAME/g")"; done
# generate + build
xcodegen generate
if [ "$SHAPE" = "macos" ]; then
xcodebuild build -scheme "$NAME" -destination 'platform=macOS' CODE_SIGNING_ALLOWED=NO -quiet
else
xcodebuild build -scheme "$NAME" -destination 'generic/platform=iOS Simulator' CODE_SIGNING_ALLOWED=NO -quiet
fi
echo "=== PASS: $SHAPE === ($TMP)"