Skip to content

Repository files navigation

Monolith

Monolith

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.

Who scaffolded with Monolith

  • 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).

Table of Contents

  1. Who scaffolded with Monolith
  2. Requirements
  3. Installation
  4. Quick Start
  5. Usage
  6. Shared Flags
  7. Package Wiring
  8. Presets
  9. App Features (25)
  10. Package Features
  11. CLI Features
  12. License Types
  13. Architecture
  14. Build & Test
  15. Integration Test Coverage
  16. Dependencies
  17. TODO
  18. License
  19. Changelog

Requirements

  • Swift 6.2+
  • macOS 14+

Installation

git clone https://github.com/Luminoid/Monolith.git
cd Monolith
swift build -c release
cp .build/release/monolith /usr/local/bin/

Quick Start

# 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.json

Usage

Every 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.

Create an iOS App

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
OptionDefaultDescription
--name(required)App name (letter start, alphanumeric/hyphens/underscores, max 50 chars)
--bundle-idcom.example.<name>Bundle identifier in reverse-DNS format
--deployment-target18.0Minimum iOS version (major.minor, >= 18.0)
--platformsiPhoneComma-separated: iPhone, iPad, macCatalyst
--project-systemxcodeprojxcodeproj (default) or xcodegen
--primary-color#007AFFHex 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
--licenseproprietaryLicense 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

Create a Swift Package

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
OptionDefaultDescription
--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: "6.0.0"' for registry packages (the --use-packages shorthand is on new app only).
--platformsiOS 18.0Comma-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)
--licensemitLicense 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-interactive
Generated 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

Create a Swift CLI

monolith new cli \
--name mytool \
--features argumentParser,devTooling,claudeMD \
--git \
--no-interactive
OptionDefaultDescription
--name(required)CLI name
--features(none)Comma-separated feature flags (see CLI Features)
--licenseapache2License 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

Other Commands

# 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 version

add 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.


Shared Flags

These flags are available on all new commands (new app, new package, new cli):

FlagDefaultDescription
--preset(none)minimal, standard, or full; pre-selects features
--forcefalseOverwrite existing project directory without prompting
--openfalseOpen project in Xcode after generation
--resolvefalseRun 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
--outputcurrent directoryOutput directory for generated project
--dry-runfalsePreview generated files without writing
--no-interactivefalseSkip 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.


Package Wiring

Three flags cover the spectrum from registered well-known packagesarbitrary SPM repos with versionslocal-path development. They work on both new app and new package with identical syntax.

--use-packages (built-in registry)

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.

--external-packages (arbitrary SPM)

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 (`Name=path`, no `://`, no requirement)
monolith new app --name MyApp \
--external-packages 'LumiKit=../LumiKit' \
--target-deps LumiKitUI

Externals override built-ins: --external-packages 'LumiKit=../LumiKit' replaces Monolith's default GitHub URL with the local path. Path-form entries emit .package(name:path:); absolute paths are normalized to project-root-relative so the manifest stays portable.

Multi-product packages on new package: the must-be-consumed check matches an external's Name against the names in --target-deps / --package-deps, and those name products, not packages. So wire one entry per product you consume, using the :packageName tail to point them at the same SPM package. Duplicate declarations de-dupe into a single .package(...) line:

monolith new package --name MyLib \
--targets MyLibCore,MyLibUI \
--target-deps "MyLibCore:LumiKitCore;MyLibUI:MyLibCore,LumiKitUI" \
--external-packages 'LumiKitCore=../LumiKit:LumiKit;LumiKitUI=../LumiKit:LumiKit'

--target-deps

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.


Presets

PresetFeatures
minimalNo features
standarddevTooling, gitHooks, claudeMD
fullAll features (excludes legacy rSwift, fastlane)

App Features (25)

Data

FeatureFlagDescription
SwiftDataswiftDataSample @Model, ModelContainer setup, test helpers
Core DatacoreDataNSManagedObject scaffold + persistent container with fatalError on load failure
CloudKitcloudKitNSPersistentCloudKitContainer wiring + registerForRemoteNotifications()
CloudKit SharingcloudKitSharingCKShare acceptance hooks in SceneDelegate

UI / third-party (code-shaping)

FeatureFlagDescription
LumiKitlumiKitLumiKit dependency with 22-color theme generation from primary color
LottielottieLottie animation dependency, optional LumiKitLottie integration
Dark ModedarkModeStandalone AppTheme with adaptive UIColor patterns (auto-derived from LumiKit)
CombinecombinePublisher/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.

System

FeatureFlagDescription
NotificationsnotificationsUNUserNotificationCenter wiring + permission request
Deep LinksdeepLinksURL scheme handler with route dispatch
SpotlightspotlightCSSearchable item handler + continueUserActivity
Deferred LaunchdeferredLaunchWorkPost-activation work scheduler (off the launch critical path)
WidgetwidgetWidgetKit extension target + App Group entitlements; widget bundle always includes its own PrivacyInfo.xcprivacy
LocalizationlocalizationString Catalog + L10n helper + make audit-strings audit script (catches the silent-fail \(...) interpolation bug)

App Store hygiene

FeatureFlagDescription
Privacy ManifestprivacyManifestPrivacyInfo.xcprivacy on app target (widget extension always gets its own regardless of this flag)
App Icon ValidationappIconValidationBuild-phase script flagging icons with alpha channel before submission

Tooling

FeatureFlagDescription
Dev ToolingdevToolingSwiftLint, SwiftFormat, Makefile, Brewfile
Git HooksgitHooksPre-commit hook (lint + format check on staged files)
Core Data Audit HookcoreDataAuditHookPre-commit reminder when .xcdatamodel changes (auto-enabled with coreData/swiftData + cloudKit + gitHooks)
CLAUDE.mdclaudeMDProject-specific Claude Code guide
License + ChangeloglicenseChangelogLicense file (configurable type) and Keep a Changelog template

Legacy (XcodeGen only)

FeatureFlagDescription
R.swiftrSwiftR.swift code generation + Mintfile (inactive development; Xcode 15+ has native type-safe resources)
FastlanefastlaneGemfile, Appfile, Fastfile (prefer Makefile or Xcode Cloud)

Auto-derived

FeatureFlagDescription
TabsautoTab bar controller; auto-enabled when --tabs is provided
Mac CatalystautoWindow config, menu bar; auto-enabled when --platforms includes macCatalyst

Migrated to --use-packages (v0.3.0+)

Old flagReplacement
--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.


Package Features

FeatureFlagDescription
Strict ConcurrencystrictConcurrencyNo-op at swift-tools-version 6.2 (strict concurrency is the language default). Flag accepted for backwards-compat; generates no swiftSettings entry.
Default IsolationdefaultIsolationdefaultIsolation: MainActor on selected targets
Dev ToolingdevToolingSwiftLint, SwiftFormat, Makefile, Brewfile
Git HooksgitHooksPre-commit hook (lint + format check on staged files)
CLAUDE.mdclaudeMDProject-specific Claude Code guide
License + ChangeloglicenseChangelogLicense file (configurable type) and Keep a Changelog template

CLI Features

FeatureFlagDescription
ArgumentParserargumentParserSwift ArgumentParser dependency
Strict ConcurrencystrictConcurrencyNo-op at swift-tools-version 6.2 (strict concurrency is the language default). Flag accepted for backwards-compat; generates no swiftSettings entry.
Dev ToolingdevToolingSwiftLint, SwiftFormat, Makefile, Brewfile
Git HooksgitHooksPre-commit hook (lint + format check on staged files)
CLAUDE.mdclaudeMDProject-specific Claude Code guide
License + ChangeloglicenseChangelogLicense file (configurable type) and Keep a Changelog template

License Types

The --license flag controls which license is generated when licenseChangelog is enabled. Each project type has a different default:

Type--license valueDefault forDescription
MITmitPackagePermissive, minimal restrictions. Most common for Swift packages
Apache 2.0apache2CLIPermissive with patent grant. Standard for developer tooling
ProprietaryproprietaryAppAll 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 mit

Architecture

All 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/ # 28 generators (incl. ColorCodeGenerator, EntitlementsGenerator)
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/ # 828 tests, 73 suites; mirrors source structure

83 source files, 65 test files, 828 tests (Swift Testing), all passing.

Key Patterns

  • Pure function generators: each generator is (Config) -> String with no side effects
  • Feature flags drive generation: resolvedFeatures auto-derives tabs, macCatalyst, darkMode, coreDataAuditHook
  • NewCommandRunner: shared post-config orchestration (dry-run → overwrite-check → signal-install → generate → git init → resolve → open). The three new commands diverge only in config-building.
  • KnownPackages registry: 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 22 LMKTheme colors
  • Shell-out centralized: all Process() calls route through ShellRunner. Surfaces error.localizedDescription and stderr on failure
  • SignalHandler: SIGINT mid-generation removes the partial output directory; the wizard's raw-mode 0x03 path raise(SIGINT)s into the same handler
  • FileWriter path-traversal guard: rejects absolute paths and .. segments with a typed FileWriterError
  • Synchronous ParsableCommand: no async; all readline, FileManager, string ops

Build settings emitted into generated projects

  • iOS app Makefile: -parallel-testing-enabled NO is auto-added to the test: target when the app has Core Data or SwiftData (matches Petfolio's documented PetRepository.shared race with Swift Testing's in-process scheduler)
  • iOS app PrivacyInfo.xcprivacy: always emitted for the widget bundle when widget is enabled (every shipped bundle needs its own per App Store), separately gated on the app-level privacyManifest feature for the app bundle
  • XcodeGen YAML: GENERATE_INFOPLIST_FILE, SWIFT_APPROACHABLE_CONCURRENCY, SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY, MARKETING_VERSION, CURRENT_PROJECT_VERSION; SwiftLint as postCompileScripts, SwiftFormat as preBuildScripts with ARM64 Homebrew PATH detection

Build & Test

swift build # Build
swift test# Run all 828 tests (73 suites)
swift run monolith version # Smoke test
make check # SwiftLint + SwiftFormat lint

Integration Test Coverage

Integration 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.

FilePurpose
IntegrationTests.swiftBaseline smoke tests (one per project type), negative tests (feature deliberately OFF), output-dir flag, ecosystem color sanity.
AppFeatureIntegrationTests.swiftOne test per AppFeature in isolation + the recommended-everything-on combo + isolated combination tests.
PackageCLIIntegrationTests.swiftPer-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.

App features → test that covers it

OptionTest
swiftDataApp with all features generates expected files (also exercised in App with every recommended option enabled stays self-consistent)
coreDataCore Data without CloudKit emits NSPersistentContainer stack and non-CloudKit model
cloudKitCloudKit auto-derives Core Data and registers for remote notifications
cloudKitSharingCloudKit 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 SnapKitSnapKit is wired into project.yml dependencies
lottieLottie emits helper and wires SPM dependency
--use-packages LookinServerLookin 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
combineApp with all features generates expected files baseline
notificationsnotifications wires UNUserNotificationCenterDelegate and import
deepLinksdeepLinks emit URL scheme and SceneDelegate handlers
spotlightspotlight emits NSUserActivity handler in SceneDelegate
deferredLaunchWorkdeferredLaunchWork emits helper in SceneDelegate
widgetwidget extension emits target files, App Group, and entitlements
privacyManifestprivacyManifest writes PrivacyInfo file even without widget
appIconValidationappIconValidation writes executable build-phase script
localizationApp 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)
devToolingCLI project generates all expected files + baseline App with all features
gitHooksPre-commit hook has executable permissions + baseline
claudeMDCLI project generates all expected files + Package/CLI all-feature tests
licenseChangelogeach LicenseType generates a matching LICENSE file (covers all 3 license bodies)
rSwiftrSwift emits Mintfile and surfaces deprecation warning
fastlanefastlane emits Gemfile, Appfile, Fastfile and surfaces deprecation warning

Project systems

OptionTest
xcodeProjApp project generates core files baseline; content check in generated project.yml is valid for xcodeProj app
xcodeGenApp with all features generates expected files baseline
spm (app)SPM app project writes Package_swift with iOS platform

Platforms

OptionTest
iPhoneevery app test
iPadApp with every recommended option enabled stays self-consistent
macCatalystbaseline App with all features + Lookin + tabs combined with macCatalyst + everything-on combo; generated-output formatting checked by generated macCatalyst app conforms to its own swiftformat config

Package features

OptionTest
strictConcurrencyPackage with every PackageFeature generates expected files + CLI with every CLIFeature generates expected files
defaultIsolation + mainActorTargetsPackage with every PackageFeature generates expected files (only BigLibUI is in mainActorTargets; verifies per-target opt-in)
devTooling / gitHooks / claudeMD / licenseChangelogPackage 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

CLI features

OptionTest
argumentParser ONgenerated CLI main has ArgumentParser structure + CLI with every CLIFeature generates expected files
argumentParser OFFCLI without ArgumentParser omits dependency from Package_swift
strictConcurrency / devTooling / gitHooks / claudeMD / licenseChangelogCLI with every CLIFeature generates expected files

License types

OptionTest
mit / apache2 / proprietaryeach LicenseType generates a matching LICENSE file

Negative tests (asserting a feature is correctly absent when not requested)

BehaviorTest
Hook script present without MakefileGit hooks without devTooling generates hook but no Makefile
Makefile present without hook scriptDevTooling without gitHooks generates no hook script
Pre-commit script is 0o755 executablePre-commit hook has executable permissions
Bare package skips tooling and docsPackage with no features omits tooling and docs
CLI without ArgumentParser skips depCLI without ArgumentParser omits dependency from Package_swift
Widget alone (without privacyManifest) emits widget PrivacyInfo but no app PrivacyInfowidget extension emits target files, App Group, and entitlements
Persistence apps emit -parallel-testing-enabled NO; non-persistence apps don'tdisableTestParallelism adds -parallel-testing-enabled NO to test target only + disableTestParallelism off by default
FileWriter rejects .. and absolute pathswriteFile rejects relative paths containing .. + writeFile rejects absolute paths in the relative arg

Combinations with distinct output (each gets a dedicated test)

The per-feature tests can't catch behaviors that emerge from interactions. These combinations produce output that neither feature alone would emit:

CombinationDistinct behaviorTest
widget + privacyManifestEmits twoPrivacyInfo.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 + macCatalystAppDelegate's buildMenu(with:) block gains per-tab ⌘1, ⌘2, …⌘NUIKeyCommand entries inside a UIMenu(title: "Tabs"…). Neither feature alone emits these.tabs combined with macCatalyst emit per-tab UIKeyCommand entries
coreData + cloudKit + gitHooksAuto-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 aloneAuto-derives cloudKitcoreData; emits CKSharingSupported = true in Info.plist + userDidAcceptCloudKitShareWith in SceneDelegate.CloudKit Sharing implies CloudKit and emits CKSharingSupported plus accept handler
lumiKit aloneAuto-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 + bundleIDDerives 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 + nameDerives lowercase-name URL scheme (<name> lowercased) into Info.plist CFBundleURLSchemes.deepLinks emit URL scheme and SceneDelegate handlers
coreData/swiftData + devToolingMakefiletest: 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 togetherPicks 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

Adding new tests

When you add a new option to Feature.swift / AppConfig.resolvedFeatures:

  1. Add one focused integration test in the appropriate file (per-feature suite).
  2. 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.
  3. If the new option is on the recommended-everything-on path, include it in App with every recommended option enabled stays self-consistent and update its assertions.
  4. 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.


Dependencies

LibraryVersionPurpose
ArgumentParser1.7.0+Command-line argument parsing
CEditLine (system)macOS built-inTerminal line editing with arrow key support (via libedit)

TODO

Infrastructure

  • Set up GitHub Actions CI (test on push/PR)
  • Add DocC API reference documentation
  • Create CONTRIBUTING.md

Features

  • monolith update: update generated files in existing projects
  • Plugin system for custom generators

License

Monolith is released under the Apache License 2.0. See LICENSE for details.


Changelog

See CHANGELOG.md for a detailed history of changes.

About

Swift CLI that scaffolds iOS apps, Swift Packages, and Swift CLIs. 25 optional app features, interactive wizard, 22-color theme pipeline, Swift 6.2 strict concurrency, privacy manifests, design-system tokens.

Topics

Resources

Contributing

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages