macOS app for managing shell configuration through a native GUI.
xcodegen generate # Regenerate .xcodeproj from project.yml
xcodebuild -project ShellCraft.xcodeproj -scheme ShellCraft -configuration Debug \
-derivedDataPath ~/Library/Developer/Xcode/DerivedData/ShellCraft-btghkkwhgqrpkvcxxrffbfehbzie build
open ~/Library/Developer/Xcode/DerivedData/ShellCraft-btghkkwhgqrpkvcxxrffbfehbzie/Build/Products/Debug/ShellCraft.appAlways launch the app after a successful build.
Automated via release.sh. The script handles version bump, build, code signing, notarization, packaging, git tag, and GitHub release creation.
./release.sh # Interactive: prompts for version bump type
./release.sh --bump patch # Non-interactive: auto-selects bump type (patch/minor/major)
./release.sh --dry-run # Preview version changes without executing
./release.sh --skip-notarize # Skip notarization (for testing builds)- Developer ID signing — handled automatically via Xcode managed signing. If export fails, install a Developer ID Application certificate from developer.apple.com
- Notarytool keychain profile — run:
xcrun notarytool store-credentials "ShellCraft" --apple-id YOUR_APPLE_ID --team-id N9DRSTM2U6 - GitHub CLI —
brew install ghand authenticate withgh auth login
- Preflight checks (tools, certificates, clean git, main branch)
- Prompts for version bump (patch/minor/major/custom) and increments build number
- Updates
project.yml, regenerates.xcodeproj(with icon fix) - Archives and exports with Developer ID signing
- Submits to Apple notary service and staples the ticket
- Packages as
Releases/ShellCraft-{version}.zip - Commits version bump, tags
v{version}, pushes - Creates GitHub release with auto-generated notes
Releases/ShellCraft-{version}.zip— notarized release artifact (gitignored)- Build artifacts in
build/are cleaned up automatically
- XcodeGen:
project.ymlis the source of truth. Never edit.xcodeprojmanually — runxcodegen generateafter adding/removing files. - Deployment target: macOS 26.0 (Tahoe)
- Swift 6 with strict concurrency (
SWIFT_STRICT_CONCURRENCY: complete) - No SPM dependencies — pure SwiftUI + AppKit
XcodeGen doesn't natively understand .icon bundles (Icon Composer format). Two issues arise:
- Folder expansion: XcodeGen recurses into
ShellCraft.icon/and addsicon.json/Assets/as individual files instead of treating it as an opaque folder reference. - Wrong file type: XcodeGen's
type: folderproduceslastKnownFileType = folder, but Xcode needsfolder.iconcomposer.iconto recognize it as an app icon.
Fix in project.yml: Exclude from normal sources, add back as folder reference, and set the icon name:
sources:
- path: ShellCraft
excludes:
- "ShellCraft.icon"
- path: ShellCraft/ShellCraft.icon
type: folder
buildPhase: resources
settings:
base:
ASSETCATALOG_COMPILER_APPICON_NAME: ShellCraftPost-generate fix (required after every xcodegen generate):
sed -i '' 's|lastKnownFileType = folder; name = ShellCraft.icon; path = ShellCraft/ShellCraft.icon; sourceTree = SOURCE_ROOT;|lastKnownFileType = folder.iconcomposer.icon; path = ShellCraft.icon; sourceTree = "<group>";|' ShellCraft.xcodeproj/project.pbxprojWithout this sed fix, the icon file type is wrong and the app icon won't appear.
- MVVM with
@MainActor @ObservableViewModels AppState— environment object for sidebar selection and unsaved-changes tracking per section- Round-trip safe writes — raw
.zshrclines are kept in memory; only targeted lines are modified viaShellConfigWriter.Modification ProcessServicewraps shell commands via/bin/zsh -cFileIOServicehandles all file reads/writes with atomic writes and automatic backup
ShellCraft/
├── App/ # ShellCraftApp, AppState
├── Models/ # Data models (ShellAlias, OhMyZshPlugin, etc.)
├── Services/ # Business logic (ShellConfigParser, OhMyZshService, etc.)
├── ViewModels/ # @MainActor @Observable VMs — one per sidebar section
├── Views/
│ ├── Sidebar/ # SidebarView, SidebarSection enum
│ ├── Shell/ # Aliases, Functions
│ ├── OhMyZsh/ # Oh My Zsh themes/plugins/settings
│ ├── Path/ # PATH manager
│ ├── Environment/ # Env vars
│ ├── Claude/ # Claude Code settings (tabbed)
│ ├── Git/ # Git config
│ ├── SSH/ # SSH config
│ ├── Secrets/ # Keychain secrets
│ ├── Tools/ # Custom tools
│ ├── Homebrew/ # Homebrew packages
│ └── Shared/ # Reusable: SaveBar, ImportExportToolbar, SearchableList, etc.
├── Extensions/ # String+Shell, URL+Home
├── Utilities/ # ShellLineParser, FileWatcher, PathValidator
└── Resources/ # Assets.xcassets
- Add case to
SidebarSectionenum (SidebarSection.swift) — setdisplayName,icon,group - Add routing in
ContentView.detailView(for:) - Create Model, Service, ViewModel, and View files following existing patterns
- Run
xcodegen generateto pick up new files
- SaveBar pattern: All editable sections show a
SaveBarat the bottom whenhasUnsavedChangesis true. Changes are only written on explicit Save; Discard reverts to the loaded state. - Import/Export: Each section implements
exportData(),previewImport(_:),applyImport(_:). Export usesImportExportService.export()withNSSavePanel. Import shows anImportConfirmationSheetpreview before applying. - Dirty tracking: ViewModels compare current state against a saved snapshot (e.g.,
savedSnapshot,originalTheme,originalEnabledPlugins). ShellConfigWriter.Modification: All.zshrcwrites go through.updateLine,.insertAfter,.deleteLine, or.appendLine— applied in reverse index order for safe multi-edit.- Tabbed sections (Claude Code, Oh My Zsh): Use
TabViewwith.tabViewStyle(.grouped)and a tab enum withrawValue/iconproperties.
.expandingTildeInPath— expands~to home directory.shellEscaped— escapes special shell characters.singleQuoted/.doubleQuoted— wraps for shell safety.abbreviatingWithTildeInPath— replaces home dir with~.trimmed— trims whitespace/newlines