A Swift CLI that scaffolds iOS apps, Swift Packages, and Swift CLIs. Generates Swift 6.2 strict concurrency, design-system tokens, privacy manifests, app-icon alpha checks, and a 22-color theme pipeline. No hand-wiring project.yml for tabs, widgets, CloudKit, or Mac Catalyst.
- Apps on the App Store: Petfolio (pet care, health, food, vet, Family Sharing, 20 app icons, 3 locales).
- Swift Packages: Prism (AVFoundation camera pipeline with actor-isolated session, manual exposure / Live Photo / Portrait / burst / night, Metal-backed filter chain).
- Who scaffolded with Monolith
- Requirements
- Installation
- Quick Start
- Usage
- Shared Flags
- Package Wiring
- Presets
- App Features (25)
- Package Features
- CLI Features
- License Types
- Architecture
- Build & Test
- Integration Test Coverage
- Dependencies
- TODO
- License
- Changelog
- Swift 6.2+
- macOS 14+
git clone https://github.com/Luminoid/Monolith.git
cd Monolith
swift build -c release
cp .build/release/monolith /usr/local/bin/# Interactive wizard with step progress, back navigation, and confirmation
monolith new app
# Non-interactive: all options via flags (great for CI/scripting)
monolith new app --name MyApp --preset standard --no-interactive
# Save config for reuse
monolith new app --name MyApp --preset standard --save-config myapp.json --no-interactive
monolith new app --load-config myapp.jsonEvery command supports interactive (full-page wizard with step progress, back navigation, and confirmation) and non-interactive (all options via flags) modes.
Git author name is read from git config user.name for LICENSE and README generation.
monolith new app \
--name MyApp \
--bundle-id com.company.myapp \
--deployment-target 18.0 \
--platforms iPhone,iPad \
--project-system xcodeproj \
--primary-color "#4CAF7D" \
--features swiftData,darkMode,combine,devTooling \
--use-packages SnapKit,Lottie \
--tabs "Home:house.fill,Settings:gear" \
--git \
--no-interactive| Option | Default | Description |
|---|---|---|
--name |
(required) | App name (letter start, alphanumeric/hyphens/underscores, max 50 chars) |
--bundle-id |
com.example.<name> |
Bundle identifier in reverse-DNS format |
--deployment-target |
18.0 |
Minimum iOS version (major.minor, >= 18.0) |
--platforms |
iPhone |
Comma-separated: iPhone, iPad, macCatalyst |
--project-system |
xcodeproj |
xcodeproj (default) or xcodegen |
--primary-color |
#007AFF |
Hex color (#RRGGBB); derives a 22-color theme palette |
--features |
(none) | Comma-separated feature flags (see App Features) |
--use-packages |
(none) | Registered packages: "SnapKit,Lottie:5.0.0,LookinServer" (see Package Wiring) |
--external-packages |
(none) | Arbitrary SPM packages: "Name=url:requirement[:package];..." or "Name=path[:package]" (see Package Wiring) |
--target-deps |
(none) | Products to link into the app target, comma-separated (e.g., Prism,CauseWayCore) |
--tabs |
(none) | Tab definitions as Name:sf.symbol pairs, comma-separated |
--license |
proprietary |
License type: mit, apache2, proprietary (see License Types) |
--git / --no-git |
(prompted) | Initialize git repository with initial commit |
Plus all shared flags.
Auto-derived features: tabs auto-enables when --tabs is provided. macCatalyst auto-enables when --platforms includes macCatalyst. darkMode auto-enables when lumiKit is selected. coreDataAuditHook auto-enables when coreData or swiftData is combined with cloudKit and gitHooks.
Generated app structure
MyApp/
Package.swift # or project.yml (XcodeGen)
ExportOptions.plist
MyApp/
Info.plist
App/
AppDelegate.swift
SceneDelegate.swift
MainTabBarController.swift # if --tabs
Core/
AppConstants.swift
Models/SampleItem.swift # if swiftData (else placeholder .gitkeep)
Services/DataPublisher.swift # if combine
Services/AsyncService.swift # if combine
L10n.swift # if localization
Features/ # placeholder .gitkeep if no --tabs
Home/HomeViewController.swift # one per tab
Settings/SettingsViewController.swift
Shared/
Design/DesignSystem.swift
Design/MyAppTheme.swift # if lumiKit (or AppTheme.swift if darkMode)
Components/LottieHelper.swift # if lottie
AppGroup.swift # if widget
MacCatalyst/MacWindowConfig.swift # if macCatalyst
Resources/
Assets.xcassets/
Localizable.xcstrings # if localization
PrivacyInfo.xcprivacy # if privacyManifest
MyAppWidget/ # if widget
Info.plist
MyAppWidget.entitlements
MyAppWidgetBundle.swift
MyAppWidget.swift
PrivacyInfo.xcprivacy # always (every shipped bundle needs one)
MyAppTests/
MyAppTests.swift
Helpers/TestContext.swift # if swiftData or coreData
Helpers/TestDataFactory.swift # if swiftData or coreData
.gitignore
README.md
.swiftlint.yml # if devTooling
.swiftformat # if devTooling
Makefile # if devTooling
Brewfile # if devTooling
Scripts/git-hooks/pre-commit # if gitHooks
Scripts/localization/audit_strings.py # if localization
Scripts/validate-app-icon.sh # if appIconValidation
.claude/CLAUDE.md # if claudeMD
LICENSE # if licenseChangelog
CHANGELOG.md # if licenseChangelog
fastlane/Appfile # if fastlane
fastlane/Fastfile # if fastlane
Gemfile # if fastlane
Mintfile # if rSwift
monolith new package \
--name MyLib \
--targets Core,UI \
--target-deps "UI:Core" \
--platforms "iOS 18.0,macOS 15.0" \
--features devTooling \
--main-actor-targets UI \
--git \
--no-interactive| Option | Default | Description |
|---|---|---|
--name |
(required) | Package name |
--targets |
<name> |
Comma-separated target names |
--target-deps |
(none) | Dependencies: "TargetB:TargetA;TargetC:TargetA,Other" (semicolon-separated entries, colon separates target from its deps) |
--package-deps |
(none) | Cross-cutting deps auto-merged into every target's dependencies (comma-separated). Resolved like --target-deps. |
--test-helper-targets |
(none) | Test-helper library targets, comma-separated. Generates a Swift Testing stub (import Testing) instead of the plain library placeholder, and skips the auto Tests/<name>Tests/ fixture. For *Testing siblings consumed by adopter test targets (e.g., MultiLibTesting). XCTest interop is opt-in (add import XCTest; swift test links it on demand). |
--target-resources |
(none) | Per-target resource directories: "Target:dir1,dir2;Target2:Resources". Emits resources: [.process(...)] on each listed target. |
--external-packages |
(none) | Arbitrary SPM packages (URL or local path); must be consumed by some target's --target-deps or --package-deps. Use --external-packages 'SnapKit=https://github.com/SnapKit/SnapKit.git:from "5.7.0"' for registry packages (the --use-packages shorthand is on new app only). |
--platforms |
iOS 18.0 |
Comma-separated: "iOS 18.0,macOS 15.0" |
--features |
(none) | Comma-separated feature flags (see Package Features) |
--main-actor-targets |
(none) | Targets with defaultIsolation: MainActor (requires defaultIsolation feature) |
--license |
mit |
License type: mit, apache2, proprietary (see License Types) |
--git / --no-git |
(prompted) | Initialize git repository |
Plus all shared flags.
Multi-target framework example (five-product package with a shared LumiKit dep, debug-only resources, and a Swift Testing helper library; the kind of layout used for an SDK whose adopters need a *Testing sibling target to write tests against):
monolith new package \
--name MultiLib \
--targets MultiLib,MultiLibAdapters,MultiLibDebug,MultiLibTesting,MultiLibReporting \
--target-deps "MultiLibAdapters:MultiLib;MultiLibDebug:MultiLib;MultiLibTesting:MultiLib;MultiLibReporting:MultiLib" \
--platforms "iOS 18.0" \
--features defaultIsolation,devTooling,gitHooks,claudeMD,licenseChangelog \
--main-actor-targets MultiLib,MultiLibAdapters,MultiLibDebug \
--package-deps LumiKitUI \
--test-helper-targets MultiLibTesting \
--target-resources "MultiLibDebug:Resources" \
--license mit \
--git \
--no-interactiveGenerated package structure
MyLib/
Package.swift
Sources/
Core/Core.swift
UI/UI.swift
Tests/
CoreTests/CoreTests.swift
UITests/UITests.swift
.gitignore
README.md
.swiftlint.yml # if devTooling
.swiftformat # if devTooling
Makefile # if devTooling
Brewfile # if devTooling
Scripts/git-hooks/pre-commit # if gitHooks
.claude/CLAUDE.md # if claudeMD
LICENSE # if licenseChangelog
CHANGELOG.md # if licenseChangelog
monolith new cli \
--name mytool \
--features argumentParser,devTooling,claudeMD \
--git \
--no-interactive| Option | Default | Description |
|---|---|---|
--name |
(required) | CLI name |
--features |
(none) | Comma-separated feature flags (see CLI Features) |
--license |
apache2 |
License type: mit, apache2, proprietary (see License Types) |
--git / --no-git |
(prompted) | Initialize git repository |
Plus all shared flags.
Generated CLI structure
mytool/
Package.swift
Sources/
mytool/mytool.swift
Tests/
mytoolTests/mytoolTests.swift
.gitignore
README.md
.swiftlint.yml # if devTooling
.swiftformat # if devTooling
Makefile # if devTooling
Brewfile # if devTooling
Scripts/git-hooks/pre-commit # if gitHooks
.claude/CLAUDE.md # if claudeMD
LICENSE # if licenseChangelog
CHANGELOG.md # if licenseChangelog
# List features (all or filtered by type)
monolith list features
monolith list features --type app
# Add a feature to an existing project
monolith add devTooling
monolith add claudeMD --path ~/Projects/MyApp
monolith add widget --bundle-id com.acme.myapp --dry-run
# Check tool availability
monolith doctor
# Shell completions
monolith completions zsh > ~/.zfunc/_monolith
# Version
monolith versionadd retrofit features (10 total), split into two tiers:
- Tier 1 (pure file writes, any project system):
devTooling,gitHooks,claudeMD,licenseChangelog,privacyManifest,appIconValidation - Tier 2 (app projects only):
localization,macCatalyst,lottie,widget
On XcodeGen projects, Tier 2 edits project.yml in place (idempotent; re-running is a no-op); re-run xcodegen generate afterward. On .xcodeproj projects, the source files are written but the user must perform manual integration steps (target membership, Add Package, entitlements) which the command prints. widget accepts --bundle-id <prefix> to compute the App Group identifier; without it, defaults to com.example.<appname>.
The other 15 app features (swiftData, coreData, cloudKit, cloudKitSharing, lumiKit, darkMode, combine, tabs, notifications, deepLinks, spotlight, deferredLaunchWork, coreDataAuditHook, strictConcurrency, defaultIsolation) require editing existing AppDelegate.swift / entitlements / Info.plist / Package.swift in ways that depend on user-modified content. Best path: re-scaffold with the new feature set into a temp dir and cherry-pick the diff.
doctor checks: swift (required), git, swiftlint, swiftformat, xcodegen, mint, fastlane.
These flags are available on all new commands (new app, new package, new cli):
| Flag | Default | Description |
|---|---|---|
--preset |
(none) | minimal, standard, or full; pre-selects features |
--force |
false |
Overwrite existing project directory without prompting |
--open |
false |
Open project in Xcode after generation |
--resolve |
false |
Run swift package resolve (SPM) or xcodebuild -resolvePackageDependencies (app) after generation |
--save-config |
(none) | Save configuration to JSON file for reuse |
--load-config |
(none) | Load configuration from JSON file |
--output |
current directory | Output directory for generated project |
--dry-run |
false |
Preview generated files without writing |
--no-interactive |
false |
Skip prompts (--name becomes required) |
A SIGINT (Ctrl-C) mid-generation removes the partial output directory if the directory didn't exist before the run. Pre-existing directories under --force are left in place to avoid blowing away unrelated content.
Three flags cover the spectrum from registered well-known packages → arbitrary SPM repos with versions → local-path development. They work on both new app and new package with identical syntax.
Currently registered: SnapKit, Lottie, LookinServer. Bare identifier uses the registry's default version; optional :version overrides per call.
monolith new app --name MyApp --use-packages 'SnapKit,Lottie:5.0.0,LookinServer'Synthesizes ExternalPackage entries from KnownPackages.registry in Config/DependencyVersion.swift. Adding a new well-known package is a registry entry, not a generator change. Unknown identifier produces a config-time error with a "did you mean…?" message. Platform conditionals come from the registry: LookinServer is iOS-only, so it emits condition: .when(platforms: [.iOS]) in Package.swift and platforms: [iOS] in XcodeGen YAML.
URL form: "Name=url:requirement[:packageName];..." where requirement is verbatim SPM (from: "0.1.0", branch: "main", exact: "1.0.0", etc.).
Path form: "Name=path[:packageName]"; no requirement segment (paths are unversioned).
# URL form
monolith new app --name MyApp \
--external-packages 'Prism=https://github.com/Luminoid/Prism.git:from: "0.1.0"' \
--target-deps Prism
# Local-path form for parallel development
monolith new app --name MyApp \
--external-packages 'LumiKit=path:../LumiKit' \
--target-deps LumiKitUIExternals override built-ins: --external-packages 'LumiKit=path:../LumiKit' replaces Monolith's default GitHub URL with the local path.
For new app, this is the product list to link into the main app target (comma-separated). Resolution is four-tier: direct match against an external's name, then longest-prefix match (PrismCore resolves to Prism for multi-product packages), then single-external fallback, then product=package fallback. De-dupes against built-in feature wirings.
For new package, the format is "Target:Dep1,Dep2;Target2:Dep1" (per-target).
Validation: every --external-packages entry must be referenced by --target-deps (or --package-deps on a package). Single-external + non-empty target-deps passes (multi-product case); multi-external requires each external by name. Unreferenced entries would be silently dropped from the emitted Package.swift, so the validator surfaces the typo at config time.
| Preset | Features |
|---|---|
minimal |
No features |
standard |
devTooling, gitHooks, claudeMD |
full |
All features (excludes legacy rSwift, fastlane) |
| Feature | Flag | Description |
|---|---|---|
| SwiftData | swiftData |
Sample @Model, ModelContainer setup, test helpers |
| Core Data | coreData |
NSManagedObject scaffold + persistent container with fatalError on load failure |
| CloudKit | cloudKit |
NSPersistentCloudKitContainer wiring + registerForRemoteNotifications() |
| CloudKit Sharing | cloudKitSharing |
CKShare acceptance hooks in SceneDelegate |
| Feature | Flag | Description |
|---|---|---|
| LumiKit | lumiKit |
LumiKit dependency with 22-color theme generation from primary color |
| Lottie | lottie |
Lottie animation dependency, optional LumiKitLottie integration |
| Dark Mode | darkMode |
Standalone AppTheme with adaptive UIColor patterns (auto-derived from LumiKit) |
| Combine | combine |
Publisher/subscriber boilerplate, async Task patterns |
For SnapKit and LookinServer, use --use-packages (see Package Wiring). They're no longer code-shaping features (they only added a dep), so they live in the registry instead.
| Feature | Flag | Description |
|---|---|---|
| Notifications | notifications |
UNUserNotificationCenter wiring + permission request |
| Deep Links | deepLinks |
URL scheme handler with route dispatch |
| Spotlight | spotlight |
CSSearchable item handler + continueUserActivity |
| Deferred Launch | deferredLaunchWork |
Post-activation work scheduler (off the launch critical path) |
| Widget | widget |
WidgetKit extension target + App Group entitlements; widget bundle always includes its own PrivacyInfo.xcprivacy |
| Localization | localization |
String Catalog + L10n helper + make audit-strings audit script (catches the silent-fail \(...) interpolation bug) |
| Feature | Flag | Description |
|---|---|---|
| Privacy Manifest | privacyManifest |
PrivacyInfo.xcprivacy on app target (widget extension always gets its own regardless of this flag) |
| App Icon Validation | appIconValidation |
Build-phase script flagging icons with alpha channel before submission |
| Feature | Flag | Description |
|---|---|---|
| Dev Tooling | devTooling |
SwiftLint, SwiftFormat, Makefile, Brewfile |
| Git Hooks | gitHooks |
Pre-commit hook (lint + format check on staged files) |
| Core Data Audit Hook | coreDataAuditHook |
Pre-commit reminder when .xcdatamodel changes (auto-enabled with coreData/swiftData + cloudKit + gitHooks) |
| CLAUDE.md | claudeMD |
Project-specific Claude Code guide |
| License + Changelog | licenseChangelog |
License file (configurable type) and Keep a Changelog template |
| Feature | Flag | Description |
|---|---|---|
| R.swift | rSwift |
R.swift code generation + Mintfile (inactive development; Xcode 15+ has native type-safe resources) |
| Fastlane | fastlane |
Gemfile, Appfile, Fastfile (prefer Makefile or Xcode Cloud) |
| Feature | Flag | Description |
|---|---|---|
| Tabs | auto | Tab bar controller; auto-enabled when --tabs is provided |
| Mac Catalyst | auto | Window config, menu bar; auto-enabled when --platforms includes macCatalyst |
| Old flag | Replacement |
|---|---|
--features snapKit |
--use-packages SnapKit |
--features lookin |
--use-packages LookinServer |
Both moved to the KnownPackages registry in v0.3.0. The auto-translating shim was removed in v0.4 — the CLI now raises a ValidationError listing the migration if these tokens show up in --features. The principle: --features is for code-shaping integrations (LumiKit's theme + LMKNavigationController + LMKLogger; Lottie's LottieHelper.swift template); the registry is for "just wire the dep" cases.
| Feature | Flag | Description |
|---|---|---|
| Strict Concurrency | strictConcurrency |
No-op at swift-tools-version 6.2 (strict concurrency is the language default). Flag accepted for backwards-compat; generates no swiftSettings entry. |
| Default Isolation | defaultIsolation |
defaultIsolation: MainActor on selected targets |
| Dev Tooling | devTooling |
SwiftLint, SwiftFormat, Makefile, Brewfile |
| Git Hooks | gitHooks |
Pre-commit hook (lint + format check on staged files) |
| CLAUDE.md | claudeMD |
Project-specific Claude Code guide |
| License + Changelog | licenseChangelog |
License file (configurable type) and Keep a Changelog template |
| Feature | Flag | Description |
|---|---|---|
| ArgumentParser | argumentParser |
Swift ArgumentParser dependency |
| Strict Concurrency | strictConcurrency |
No-op at swift-tools-version 6.2 (strict concurrency is the language default). Flag accepted for backwards-compat; generates no swiftSettings entry. |
| Dev Tooling | devTooling |
SwiftLint, SwiftFormat, Makefile, Brewfile |
| Git Hooks | gitHooks |
Pre-commit hook (lint + format check on staged files) |
| CLAUDE.md | claudeMD |
Project-specific Claude Code guide |
| License + Changelog | licenseChangelog |
License file (configurable type) and Keep a Changelog template |
The --license flag controls which license is generated when licenseChangelog is enabled. Each project type has a different default:
| Type | --license value |
Default for | Description |
|---|---|---|---|
| MIT | mit |
Package | Permissive, minimal restrictions. Most common for Swift packages |
| Apache 2.0 | apache2 |
CLI | Permissive with patent grant. Standard for developer tooling |
| Proprietary | proprietary |
App | All rights reserved. Standard for commercial iOS apps |
# Override default
monolith new app --name MyApp --license mit --features licenseChangelog --no-interactive
monolith new package --name MyLib --license apache2 --features licenseChangelog --no-interactive
# Add license to existing project (auto-detects project type for default)
monolith add licenseChangelog --license mitAll source code lives in a MonolithLib library target. A thin monolith executable calls Monolith.main(). This enables @testable import MonolithLib in tests.
Monolith/
Package.swift
Sources/
CEditLine/ # System library module for macOS editline (arrow key support)
MonolithLib/
Monolith.swift # @main ParsableCommand
Commands/ # NewCommand (router) + New{App,Package,CLI},
# NewCommandRunner (shared post-config orchestration),
# AddCommand, AddFeatureHandlers, List, Doctor,
# Completions, Version, ValidationErrorBridge
Config/ # AppConfig, PackageConfig, CLIConfig, Feature, Platform,
# Preset, ConfigFile, AddableFeature, DependencyVersion
# (incl. KnownPackages registry)
Prompts/ # PromptEngine (readline), WizardEngine, WizardStep, Validators
Generators/
App/ # 27 generators (incl. ColorCodeGenerator)
Package/ # 3 generators
CLI/ # 3 generators
Shared/ # 10 generators (SwiftLint, SwiftFormat, Makefile, etc.)
Utilities/ # FileWriter (path-traversal guarded), ShellRunner, SignalHandler,
# UISymbols, ColorDeriver, StringExtensions, ToolChecker,
# OverwriteProtection, ProjectDetector, ProjectOpener,
# ProjectYamlEditor, XcodeGenRunner, PackageResolver
monolith/
main.swift
Tests/MonolithTests/ # 794 tests, 71 suites; mirrors source structure
82 source files, 63 test files, 794 tests (Swift Testing), all passing.
- Pure function generators: each generator is
(Config) -> Stringwith no side effects - Feature flags drive generation:
resolvedFeaturesauto-derivestabs,macCatalyst,darkMode,coreDataAuditHook NewCommandRunner: shared post-config orchestration (dry-run → overwrite-check → signal-install → generate → git init → resolve → open). The threenewcommands diverge only in config-building.KnownPackagesregistry: data-driven catalog of well-known third-party packages. Adding one is a registry entry, not a generator change.ColorDeriver: HSB manipulation from 1 hex color to 22LMKThemecolors- Shell-out centralized: all
Process()calls route throughShellRunner. Surfaceserror.localizedDescriptionand stderr on failure SignalHandler: SIGINT mid-generation removes the partial output directory; the wizard's raw-mode0x03pathraise(SIGINT)s into the same handlerFileWriterpath-traversal guard: rejects absolute paths and..segments with a typedFileWriterError- Synchronous
ParsableCommand: no async; all readline, FileManager, string ops
- iOS app
Makefile:-parallel-testing-enabled NOis auto-added to thetest:target when the app has Core Data or SwiftData (matches Petfolio's documentedPetRepository.sharedrace with Swift Testing's in-process scheduler) - iOS app
PrivacyInfo.xcprivacy: always emitted for the widget bundle whenwidgetis enabled (every shipped bundle needs its own per App Store), separately gated on the app-levelprivacyManifestfeature for the app bundle - XcodeGen YAML:
GENERATE_INFOPLIST_FILE,SWIFT_APPROACHABLE_CONCURRENCY,SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY,MARKETING_VERSION,CURRENT_PROJECT_VERSION; SwiftLint aspostCompileScripts, SwiftFormat aspreBuildScriptswith ARM64 Homebrew PATH detection
swift build # Build
swift test # Run all 794 tests (71 suites)
swift run monolith version # Smoke test
make check # SwiftLint + SwiftFormat lintIntegration tests live in three suites under Tests/MonolithTests/, all nested under MonolithIntegrationSuite (an @Suite(.serialized) enum) so .serialized propagates downward. Required because every integration test mutates currentDirectoryPath via withTempDir, and Swift Testing's .serialized is per-suite, not global.
| File | Purpose |
|---|---|
IntegrationTests.swift |
Baseline smoke tests (one per project type), negative tests (feature deliberately OFF), output-dir flag, ecosystem color sanity. |
AppFeatureIntegrationTests.swift |
One test per AppFeature in isolation + the recommended-everything-on combo + isolated combination tests. |
PackageCLIIntegrationTests.swift |
Per-PackageFeature and per-CLIFeature coverage + license variants. |
Every option appears in exactly one focused test (plus the everything-on combo for interaction stability). Combinations with output distinct from the sum of parts get their own dedicated test.
| Option | Test |
|---|---|
swiftData |
App with all features generates expected files (also exercised in App with every recommended option enabled stays self-consistent) |
coreData |
Core Data without CloudKit emits NSPersistentContainer stack and non-CloudKit model |
cloudKit |
CloudKit auto-derives Core Data and registers for remote notifications |
cloudKitSharing |
CloudKit Sharing implies CloudKit and emits CKSharingSupported plus accept handler |
coreDataAuditHook (auto-derived) |
coreDataAuditHook is auto-derived when persistence + cloudKit + gitHooks coexist |
lumiKit (auto-derives darkMode) |
LumiKit auto-enables darkMode and emits theme file plus LMK wiring |
--use-packages SnapKit |
SnapKit is wired into project.yml dependencies |
lottie |
Lottie emits helper and wires SPM dependency |
--use-packages LookinServer |
Lookin is gated to iOS-only platforms in project.yml |
darkMode (standalone, no LumiKit) |
App with all features generates expected files baseline (emits AppTheme.swift); per-color theme correctness in all ecosystem primary colors generate valid themes |
combine |
App with all features generates expected files baseline |
notifications |
notifications wires UNUserNotificationCenterDelegate and import |
deepLinks |
deepLinks emit URL scheme and SceneDelegate handlers |
spotlight |
spotlight emits NSUserActivity handler in SceneDelegate |
deferredLaunchWork |
deferredLaunchWork emits helper in SceneDelegate |
widget |
widget extension emits target files, App Group, and entitlements |
privacyManifest |
privacyManifest writes PrivacyInfo file even without widget |
appIconValidation |
appIconValidation writes executable build-phase script |
localization |
App with all features generates expected files baseline (also SPM app project writes Package_swift…) |
tabs (auto-derived from non-empty tabs array) |
App with all features generates expected files baseline |
macCatalyst (auto-derived from platform) |
App with all features generates expected files baseline (also Lookin test) |
devTooling |
CLI project generates all expected files + baseline App with all features |
gitHooks |
Pre-commit hook has executable permissions + baseline |
claudeMD |
CLI project generates all expected files + Package/CLI all-feature tests |
licenseChangelog |
each LicenseType generates a matching LICENSE file (covers all 3 license bodies) |
rSwift |
rSwift emits Mintfile and surfaces deprecation warning |
fastlane |
fastlane emits Gemfile, Appfile, Fastfile and surfaces deprecation warning |
| Option | Test |
|---|---|
xcodeProj |
App project generates core files baseline; content check in generated project.yml is valid for xcodeProj app |
xcodeGen |
App with all features generates expected files baseline |
spm (app) |
SPM app project writes Package_swift with iOS platform |
| Option | Test |
|---|---|
iPhone |
every app test |
iPad |
App with every recommended option enabled stays self-consistent |
macCatalyst |
baseline App with all features + Lookin + tabs combined with macCatalyst + everything-on combo |
| Option | Test |
|---|---|
strictConcurrency |
Package with every PackageFeature generates expected files + CLI with every CLIFeature generates expected files |
defaultIsolation + mainActorTargets |
Package with every PackageFeature generates expected files (only BigLibUI is in mainActorTargets; verifies per-target opt-in) |
devTooling / gitHooks / claudeMD / licenseChangelog |
Package with every PackageFeature generates expected files |
packageDeps (cross-cutting) |
Package with packageDeps, testHelperTargets, targetResources, and externalPackages wires them in |
testHelperTargets (Swift Testing stub, no auto-test sibling) |
same test |
targetResources (.process(...)) |
same test |
externalPackages (registry override) |
same test |
| Bare package (zero features) | Package with no features omits tooling and docs |
| Option | Test |
|---|---|
argumentParser ON |
generated CLI main has ArgumentParser structure + CLI with every CLIFeature generates expected files |
argumentParser OFF |
CLI without ArgumentParser omits dependency from Package_swift |
strictConcurrency / devTooling / gitHooks / claudeMD / licenseChangelog |
CLI with every CLIFeature generates expected files |
| Option | Test |
|---|---|
mit / apache2 / proprietary |
each LicenseType generates a matching LICENSE file |
| Behavior | Test |
|---|---|
| Hook script present without Makefile | Git hooks without devTooling generates hook but no Makefile |
| Makefile present without hook script | DevTooling without gitHooks generates no hook script |
Pre-commit script is 0o755 executable |
Pre-commit hook has executable permissions |
| Bare package skips tooling and docs | Package with no features omits tooling and docs |
| CLI without ArgumentParser skips dep | CLI without ArgumentParser omits dependency from Package_swift |
Widget alone (without privacyManifest) emits widget PrivacyInfo but no app PrivacyInfo |
widget extension emits target files, App Group, and entitlements |
Persistence apps emit -parallel-testing-enabled NO; non-persistence apps don't |
disableTestParallelism adds -parallel-testing-enabled NO to test target only + disableTestParallelism off by default |
FileWriter rejects .. and absolute paths |
writeFile rejects relative paths containing .. + writeFile rejects absolute paths in the relative arg |
The per-feature tests can't catch behaviors that emerge from interactions. These combinations produce output that neither feature alone would emit:
| Combination | Distinct behavior | Test |
|---|---|---|
widget + privacyManifest |
Emits two PrivacyInfo.xcprivacy files (app bundle + widget bundle), not one. App-Store-required: every shipped bundle needs its own manifest. |
widget plus privacyManifest emits manifest in widget bundle too |
widget alone (no privacyManifest) |
Widget bundle still gets its own PrivacyInfo.xcprivacy (every shipped bundle needs one); the app bundle skips its manifest. |
widget extension emits target files, App Group, and entitlements |
tabs + macCatalyst |
AppDelegate's buildMenu(with:) block gains per-tab ⌘1, ⌘2, …⌘N UIKeyCommand entries inside a UIMenu(title: "Tabs"…). Neither feature alone emits these. |
tabs combined with macCatalyst emit per-tab UIKeyCommand entries |
coreData + cloudKit + gitHooks |
Auto-derives coreDataAuditHook, appending the Core Data model-change reminder to the pre-commit script. Triple-condition rule that no single feature triggers. |
coreDataAuditHook is auto-derived when persistence + cloudKit + gitHooks coexist |
cloudKit alone (no coreData/swiftData) |
Auto-inserts coreData so CloudKit has a backing store; also flips Info.plist UIBackgroundModes: remote-notification and registers for remote notifications in AppDelegate. |
CloudKit auto-derives Core Data and registers for remote notifications |
cloudKitSharing alone |
Auto-derives cloudKit → coreData; emits CKSharingSupported = true in Info.plist + userDidAcceptCloudKitShareWith in SceneDelegate. |
CloudKit Sharing implies CloudKit and emits CKSharingSupported plus accept handler |
lumiKit alone |
Auto-derives darkMode but replaces standalone AppTheme.swift with <App>Theme.swift (LumiKit owns full theming). Standalone darkMode emits the inverse file. Transitive SnapKit wiring also derived: LumiKitUI already pulls SnapKit, so the generated ViewController.swift uses SnapKit syntax without an explicit --use-packages SnapKit. |
LumiKit auto-enables darkMode and emits theme file plus LMK wiring |
widget + bundleID |
Derives App Group identifier group.<bundleID> into both the entitlements file and the shared AppGroup.swift. Must match between app and widget targets or containerURL(forSecurityApplicationGroupIdentifier:) returns nil at runtime. |
widget extension emits target files, App Group, and entitlements |
deepLinks + name |
Derives lowercase-name URL scheme (<name> lowercased) into Info.plist CFBundleURLSchemes. |
deepLinks emit URL scheme and SceneDelegate handlers |
coreData/swiftData + devTooling |
Makefile test: target gets -parallel-testing-enabled NO (singleton-prone persistence layers race under Swift Testing's parallel scheduler; matches Petfolio's documented PetRepository.shared race). |
disableTestParallelism adds -parallel-testing-enabled NO to test target only |
| Every recommended option ON together | Picks recommended tech for either/or choices (swiftData over coreData, xcodeProj over xcodeGen/spm, proprietary license per app default; legacy rSwift/fastlane excluded). Verifies generator interactions: SwiftData wins over Core Data when both could apply, AppDelegate imports the union of every feature's libraries without one path clobbering another, SceneDelegate carries CloudKit-sharing + deep links + spotlight + deferred-launch hooks side-by-side. |
App with every recommended option enabled stays self-consistent |
When you add a new option to Feature.swift / AppConfig.resolvedFeatures:
- Add one focused integration test in the appropriate file (per-feature suite).
- If the new option's behavior changes when combined with an existing option, add one combination test under "Combinations with distinct output" and update the table above.
- If the new option is on the recommended-everything-on path, include it in
App with every recommended option enabled stays self-consistentand update its assertions. - Update this matrix in the same commit.
Substring-only assertions (output.contains("foo")) are not enough for structurally-meaningful output (YAML indentation, init chains, import lines, package products). Add a structural assertion alongside the substring one when the output's well-formedness matters; parse the YAML, regex over indentation, check the exact line sequence.
| Library | Version | Purpose |
|---|---|---|
| ArgumentParser | 1.7.0+ | Command-line argument parsing |
| CEditLine (system) | macOS built-in | Terminal line editing with arrow key support (via libedit) |
- Set up GitHub Actions CI (test on push/PR)
- Add DocC API reference documentation
- Create CONTRIBUTING.md
-
monolith update: update generated files in existing projects - Plugin system for custom generators
Monolith is released under the Apache License 2.0. See LICENSE for details.
See CHANGELOG.md for a detailed history of changes.