├── .gitignore ├── Resources ├── dmg-bg@2x.png ├── teaBASE.icns ├── Media.xcassets │ ├── Contents.json │ ├── AppIcons │ │ ├── Contents.json │ │ ├── Editors │ │ │ ├── Contents.json │ │ │ ├── Code.imageset │ │ │ │ ├── Code.png │ │ │ │ └── Contents.json │ │ │ ├── Cot.imageset │ │ │ │ ├── Cot.png │ │ │ │ └── Contents.json │ │ │ ├── Zed.imageset │ │ │ │ ├── Zed.png │ │ │ │ └── Contents.json │ │ │ ├── Cursor.imageset │ │ │ │ ├── Cursor.png │ │ │ │ └── Contents.json │ │ │ └── Sublime Text.imageset │ │ │ │ ├── Sublime Text.png │ │ │ │ └── Contents.json │ │ └── Terminals │ │ │ ├── Contents.json │ │ │ ├── Warp.imageset │ │ │ ├── Warp.png │ │ │ └── Contents.json │ │ │ ├── Hyper.imageset │ │ │ ├── Hyper.png │ │ │ └── Contents.json │ │ │ ├── Tabby.imageset │ │ │ ├── Tabby.png │ │ │ └── Contents.json │ │ │ ├── iTerm2.imageset │ │ │ ├── iTerm2.png │ │ │ └── Contents.json │ │ │ └── Terminal.imageset │ │ │ ├── Terminal.png │ │ │ └── Contents.json │ ├── `cd to.app` Screenshot.imageset │ │ ├── cd-to-screenshot.png │ │ └── Contents.json │ ├── Icon.imageset │ │ ├── Contents.json │ │ └── SystemSettingsIcon.svg │ └── Wordmark.imageset │ │ ├── Contents.json │ │ └── tea-base-black.svg ├── dotfile-sync-exclude.txt └── dotfile-sync-launchd.plist ├── Scripts ├── install-docker.sh ├── docker-version.sh ├── usr-local-install.sh ├── self-update.sh ├── download-count.sh ├── build-rc-dmg.sh ├── github-integration.sh ├── dotfile-sync.sh ├── publish-release.sh └── make-clean-install-pack.sh ├── Sources ├── ClickableTextField.h ├── ClickableTextField.m ├── teaBASE+CleanInstall.m ├── teaBASE+Helpers.m ├── teaBASE+DotfileSync.m ├── teaBASE+SelfUpdate.m ├── teaBASE.h ├── teaBASE+GPG.m ├── misc.m ├── teaBASE+git.m ├── teaBASE.m ├── teaBASE+DevTools.m └── teaBASE+SSH.m ├── teaBASE.xcodeproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── project.pbxproj ├── Sundries ├── teaBASE.entitlements └── Info.plist ├── .github └── workflows │ └── cd.yml ├── Docs └── clean-install-guide.md ├── README.md └── LICENSE.txt /.gitignore: -------------------------------------------------------------------------------- 1 | xcuserdata 2 | /Build 3 | .DS_Store 4 | /*.dmg 5 | -------------------------------------------------------------------------------- /Resources/dmg-bg@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teaxyz/teaBASE/HEAD/Resources/dmg-bg@2x.png -------------------------------------------------------------------------------- /Resources/teaBASE.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teaxyz/teaBASE/HEAD/Resources/teaBASE.icns -------------------------------------------------------------------------------- /Resources/Media.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Resources/Media.xcassets/AppIcons/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Resources/Media.xcassets/AppIcons/Editors/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Resources/Media.xcassets/AppIcons/Terminals/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Resources/Media.xcassets/AppIcons/Editors/Code.imageset/Code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teaxyz/teaBASE/HEAD/Resources/Media.xcassets/AppIcons/Editors/Code.imageset/Code.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/AppIcons/Editors/Cot.imageset/Cot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teaxyz/teaBASE/HEAD/Resources/Media.xcassets/AppIcons/Editors/Cot.imageset/Cot.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/AppIcons/Editors/Zed.imageset/Zed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teaxyz/teaBASE/HEAD/Resources/Media.xcassets/AppIcons/Editors/Zed.imageset/Zed.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/AppIcons/Terminals/Warp.imageset/Warp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teaxyz/teaBASE/HEAD/Resources/Media.xcassets/AppIcons/Terminals/Warp.imageset/Warp.png -------------------------------------------------------------------------------- /Scripts/install-docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if ! test -d /Applications/Docker.app; then 4 | brew install --cask Docker 5 | else 6 | brew uninstall --cask Docker 7 | fi 8 | -------------------------------------------------------------------------------- /Sources/ClickableTextField.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface ClickableTextField : NSTextField 4 | 5 | @property (nonatomic, copy) void (^onClick)(void); 6 | 7 | @end 8 | -------------------------------------------------------------------------------- /Resources/Media.xcassets/AppIcons/Editors/Cursor.imageset/Cursor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teaxyz/teaBASE/HEAD/Resources/Media.xcassets/AppIcons/Editors/Cursor.imageset/Cursor.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/AppIcons/Terminals/Hyper.imageset/Hyper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teaxyz/teaBASE/HEAD/Resources/Media.xcassets/AppIcons/Terminals/Hyper.imageset/Hyper.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/AppIcons/Terminals/Tabby.imageset/Tabby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teaxyz/teaBASE/HEAD/Resources/Media.xcassets/AppIcons/Terminals/Tabby.imageset/Tabby.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/AppIcons/Terminals/iTerm2.imageset/iTerm2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teaxyz/teaBASE/HEAD/Resources/Media.xcassets/AppIcons/Terminals/iTerm2.imageset/iTerm2.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/AppIcons/Terminals/Terminal.imageset/Terminal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teaxyz/teaBASE/HEAD/Resources/Media.xcassets/AppIcons/Terminals/Terminal.imageset/Terminal.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/`cd to.app` Screenshot.imageset/cd-to-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teaxyz/teaBASE/HEAD/Resources/Media.xcassets/`cd to.app` Screenshot.imageset/cd-to-screenshot.png -------------------------------------------------------------------------------- /Resources/Media.xcassets/AppIcons/Editors/Sublime Text.imageset/Sublime Text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teaxyz/teaBASE/HEAD/Resources/Media.xcassets/AppIcons/Editors/Sublime Text.imageset/Sublime Text.png -------------------------------------------------------------------------------- /Scripts/docker-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export PATH=/usr/bin:/bin 4 | 5 | defaults read "$(mdfind "kMDItemCFBundleIdentifier == 'com.docker.docker'" | head -n 1)/Contents/Info.plist" CFBundleShortVersionString 6 | -------------------------------------------------------------------------------- /teaBASE.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Resources/dotfile-sync-exclude.txt: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | 4 | /Applications 5 | /.cache 6 | /Desktop 7 | /Documents 8 | /Downloads 9 | /Library 10 | /Movies 11 | /Music 12 | /Photos 13 | /Pictures 14 | /Sites 15 | 16 | # speed up glob adds for eg config.yml 17 | /.pkgx 18 | -------------------------------------------------------------------------------- /Sundries/teaBASE.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-unsigned-executable-memory 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Scripts/usr-local-install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ^^ using bash fails for the admin escalation API we use, we dunno why 3 | 4 | if ! test -d /usr/local/bin; then 5 | /bin/mkdir -p /usr/local/bin 6 | /bin/chmod 755 /usr/local/bin 7 | fi 8 | 9 | /bin/cp "$1" /usr/local/bin 10 | /bin/chmod 755 /usr/local/bin/"$(/usr/bin/basename "$1")" 11 | -------------------------------------------------------------------------------- /Sundries/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleIconFile 6 | teaBASE.icns 7 | NSPrefPaneIconFile 8 | teaBASE.icns 9 | NSPrefPaneIconLabel 10 | teaBASE β 11 | 12 | 13 | -------------------------------------------------------------------------------- /Resources/Media.xcassets/AppIcons/Editors/Code.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "Code.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Resources/Media.xcassets/AppIcons/Editors/Cot.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "Cot.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Resources/Media.xcassets/AppIcons/Editors/Zed.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "Zed.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Resources/Media.xcassets/Icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "SystemSettingsIcon.svg", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Resources/Media.xcassets/AppIcons/Editors/Cursor.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "Cursor.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Resources/Media.xcassets/AppIcons/Terminals/Hyper.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "Hyper.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Resources/Media.xcassets/AppIcons/Terminals/Tabby.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "Tabby.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Resources/Media.xcassets/AppIcons/Terminals/Warp.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "Warp.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Resources/Media.xcassets/AppIcons/Terminals/iTerm2.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "iTerm2.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Resources/Media.xcassets/AppIcons/Terminals/Terminal.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "Terminal.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Resources/Media.xcassets/AppIcons/Editors/Sublime Text.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "Sublime Text.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Resources/Media.xcassets/`cd to.app` Screenshot.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "cd-to-screenshot.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Scripts/self-update.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DMG_PATH="$1" 4 | OUTPUT_DIR="$2" 5 | TARGET_FILE="teaBASE.prefPane" 6 | 7 | # Create a temporary mount point 8 | TMP_MOUNT=$(mktemp -d) 9 | 10 | # Mount the DMG silently 11 | hdiutil attach "$DMG_PATH" -mountpoint "$TMP_MOUNT" -nobrowse -quiet 12 | 13 | #TODO use `ditto` 14 | rsync -a --delete "$TMP_MOUNT/teaBASE.prefPane/" "$OUTPUT_DIR/" 15 | 16 | # Unmount the DMG 17 | hdiutil detach "$TMP_MOUNT" -quiet 18 | 19 | # Clean up the temporary mount point 20 | rmdir "$TMP_MOUNT" 21 | -------------------------------------------------------------------------------- /Resources/Media.xcassets/Wordmark.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "tea-base-black.svg", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "template-rendering-intent" : "template" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Scripts/download-count.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S pkgx +gh +jq bash -eo pipefail 2 | 3 | # Repository to analyze 4 | REPO="teaxyz/teaBASE" 5 | 6 | # Fetch release data using the GitHub CLI 7 | echo "Fetching release data for $REPO..." 8 | releases=$(gh api -H "Accept: application/vnd.github+json" /repos/$REPO/releases) 9 | 10 | # Extract download counts from each asset and sum them up 11 | total_downloads=$(echo "$releases" | jq '[.[] | .assets[].download_count] | add') 12 | 13 | echo "Total downloads for all releases: $total_downloads" 14 | -------------------------------------------------------------------------------- /Sources/ClickableTextField.m: -------------------------------------------------------------------------------- 1 | #import "ClickableTextField.h" 2 | 3 | @implementation ClickableTextField 4 | 5 | - (void)mouseDown:(NSEvent *)event { 6 | if (self.onClick) { 7 | self.onClick(); 8 | } else if (self.target && self.action) { 9 | [self.target performSelector:self.action withObject:self]; 10 | } 11 | } 12 | 13 | // update the cursor to be a pointer 14 | - (void)resetCursorRects { 15 | [super resetCursorRects]; 16 | [self addCursorRect:self.bounds cursor:[NSCursor pointingHandCursor]]; 17 | } 18 | 19 | @end 20 | -------------------------------------------------------------------------------- /Sources/teaBASE+CleanInstall.m: -------------------------------------------------------------------------------- 1 | #import "teaBASE.h" 2 | 3 | @implementation teaBASE (CleanInstall) 4 | 5 | - (IBAction)generateCleanInstallPack:(id)sender { 6 | NSString *script_path = [[[NSBundle bundleForClass:[self class]] bundlePath] stringByAppendingPathComponent:@"Contents/Scripts/make-clean-install-pack.sh"]; 7 | 8 | run_in_terminal(script_path, [NSBundle bundleForClass:self.class]); 9 | } 10 | 11 | - (IBAction)openCleanInstallGuide:(id)sender { 12 | id url = @"https://github.com/teaxyz/teaBASE/blob/main/Docs/clean-install-guide.md"; 13 | [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:url]]; 14 | } 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /Resources/dotfile-sync-launchd.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Label 6 | xyz.tea.BASE.dotfile-sync 7 | ProgramArguments 8 | 9 | $PREFPANE/Contents/Scripts/dotfile-sync.sh 10 | 11 | StartInterval 12 | 600 13 | RunAtLoad 14 | 15 | StandardOutPath 16 | $HOME/Library/Logs/teaBASE/dotfile-sync.stdout.txt 17 | StandardErrorPath 18 | $HOME/Library/Logs/teaBASE/dotfile-sync.stderr.txt 19 | 20 | 21 | -------------------------------------------------------------------------------- /Scripts/build-rc-dmg.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S pkgx +create-dmg bash -eo pipefail 2 | 3 | cd "$(dirname "$0")/.." 4 | 5 | if ! test "$1"; then 6 | echo "usage $0 " >&2 7 | exit 1 8 | fi 9 | 10 | v=$1-rc 11 | 12 | tmp_xcconfig="$(mktemp)" 13 | echo "MARKETING_VERSION = $v" > "$tmp_xcconfig" 14 | 15 | xcodebuild \ 16 | -scheme teaBASE \ 17 | -configuration Release \ 18 | -xcconfig "$tmp_xcconfig" \ 19 | -derivedDataPath ./Build \ 20 | build 21 | 22 | codesign \ 23 | --entitlements ./Sundries/teaBASE.entitlements \ 24 | --deep --force \ 25 | --options runtime \ 26 | --sign "Developer ID Application: Tea Inc. (7WV56FL599)" \ 27 | build/Build/Products/Release/teaBASE.prefPane 28 | 29 | rm -f teaBASE-$v.dmg 30 | 31 | #NOTE UDZO is half the size of the supposedly “better” ULMO 32 | 33 | create-dmg \ 34 | --volname "teaBASE v$1" \ 35 | --window-size 435 435 \ 36 | --window-pos 538 273 \ 37 | --filesystem APFS \ 38 | --format ULFO \ 39 | --background ./Resources/dmg-bg@2x.png \ 40 | --icon teaBASE.prefPane 217.5 223.5 \ 41 | --hide-extension teaBASE.prefPane \ 42 | --icon-size 100 \ 43 | teaBASE-$v.dmg \ 44 | build/Build/Products/Release/teaBASE.prefPane 45 | 46 | codesign \ 47 | --force \ 48 | --sign "Developer ID Application: Tea Inc. (7WV56FL599)" \ 49 | ./teaBASE-$v.dmg 50 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: cd 2 | 3 | on: 4 | release: 5 | types: [published] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | update-download-redirect: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | with: 14 | ref: gh-pages 15 | 16 | - name: generate download.html 17 | id: meta 18 | env: 19 | GH_TOKEN: ${{ github.token }} 20 | run: | 21 | url=$(gh release view --json assets | jq -r '.assets[]? | select(.name | endswith(".dmg")) | .url') 22 | v=$(gh release view --json tagName --jq .tagName) 23 | 24 | echo "::set-output name=v::$v" 25 | 26 | cat <<-EOS > download.html 27 | 28 | 29 | 30 | 31 | 32 | 33 | Downloading teaBASE-$v.dmg… 34 | 35 | 36 |

Downloading $url

37 | 38 | 39 | EOS 40 | 41 | - uses: fregante/setup-git-user@v2 42 | 43 | - run: | 44 | git add download.html 45 | git commit --message 'Update download.html for ${{ steps.meta.outputs.v }}' 46 | git push origin gh-pages 47 | -------------------------------------------------------------------------------- /Scripts/github-integration.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S pkgx +gh^2 +gum +git^2 bash -eo pipefail 2 | 3 | set -eo pipefail 4 | 5 | if gh auth status 2>/dev/null; then 6 | # check for required scopes 7 | scopes="$(gh api user -H "Authorization: token $(gh auth token)" -i | grep -i 'X-OAuth-Scopes:')" 8 | fi 9 | 10 | if [[ $scopes == *"admin:public_key"* && $scopes == *"write:gpg_key"* ]]; then 11 | true 12 | else 13 | gum format \ 14 | "# \`gh\` auth status is missing required scopes" \ 15 | "firstly, we need to add the \`write:gpg_key\` and " \ 16 | "\`admin:public_key\` scopes to your \`gh\` authentication" \ 17 | "" \ 18 | "> alternatively upload your keys manually:" \ 19 | "> https://github.com/settings/keys" 20 | 21 | gh auth login -h github.com -p https -s write:gpg_key -s admin:public_key -w 22 | fi 23 | 24 | if test $(find ~/.ssh -name id_\*.pub | wc -l) -gt 1; then 25 | gum format \ 26 | "# multiple ssh public keys found" \ 27 | "choose which to upload" \ 28 | "> use the arrow keys to move the cursor, space to toggle and press return when done" 29 | echo #spacer 30 | 31 | files="$(gum choose --no-limit --selected=id_ed25519.pub,id_rsa.pub $(cd ~/.ssh && ls id_*.pub))" 32 | 33 | echo #spacer 34 | 35 | for x in $files; do 36 | gum format "uploading \`$x\`…" 37 | gh ssh-key add ~/.ssh/"$x" --title "$(hostname -s) (added by teaBASE)" 38 | done 39 | else 40 | x="$(ls "$HOME"/.ssh/id_*.pub)" 41 | gum format "# uploading \`$x\`…" 42 | gh ssh-key add "$x" --title "$(hostname -s) (added by teaBASE)" 43 | fi 44 | 45 | if GPG="$(bpb print)"; then 46 | gum format "# uploading your gpg public key…" 47 | 48 | echo "$GPG" | gh gpg-key add --title "$(hostname -s) (added by teaBASE)" 49 | # ^^ this errors out if the key already exists which sucks 50 | # ^^ TODO report bug 51 | fi 52 | -------------------------------------------------------------------------------- /Docs/clean-install-guide.md: -------------------------------------------------------------------------------- 1 | # Clean Install Guide 2 | 3 | ## Why Clean Install? 4 | 5 | As developers, we install a lot of software. Every item could potentially 6 | contain malware. The only way to be sure your system isn’t compromised is 7 | a fresh installation. 8 | 9 | It is also good to practice your restoration flow for disaster scenarios, like 10 | losing your computer or hardware failure. 11 | 12 | ## How to Clean Install 13 | 14 | macOS makes it easy to do a clean install. In System Settings go to “General”, 15 | “Transfer or Reset” and click “Erase All Content and Settings”. 16 | 17 | ## Using teaBASE’s “Clean Install Pack” 18 | 19 | Before clean installing generate your clean install pack with teaBASE. 20 | 21 | > [!INFO] 22 | > Add all files that are not otherwise backed up to cloud services, eg. the 23 | > working sources for your projects. 24 | > 25 | > Take advantage of things like iCloud Drive to have other documents restored 26 | > automatically. 27 | 28 | > [!IMPORTANT] 29 | > Transfer your pack to external storage before clean installing! A USB key or 30 | > another computer are good. 31 | 32 | Once macOS is clean installed, transfer the pack back and open it. The bundled 33 | `restore` script will reinstall your packages, apps and dotfile configuration. 34 | 35 | ## Restoring Other Settings 36 | 37 | Nowadays the majority of data and settings are stored in the cloud. 38 | However, some apps will certainly lose settings as part of a clean install. 39 | The important thing is that your data and dotfiles are restored. 40 | 41 | > [!NOTE] 42 | > You can take advantage of this to explore your apps with a clean slate. 43 | > Maybe you don’t want them configured the way you used to? 44 | 45 | > It would be potentially nice to restore GUI app settings too. If you would 46 | > like this feature open a discussion about it and let’s plan it out. 47 | -------------------------------------------------------------------------------- /Resources/Media.xcassets/Icon.imageset/SystemSettingsIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Sources/teaBASE+Helpers.m: -------------------------------------------------------------------------------- 1 | #import "teaBASE.h" 2 | 3 | @implementation teaBASE (Helpers) 4 | 5 | - (void)installSubexecutable:(NSString *)name { 6 | NSString *src = [[[NSBundle bundleForClass:[self class]] bundlePath] stringByAppendingPathComponent:[NSString stringWithFormat:@"Contents/MacOS/%@", name]]; 7 | NSString *script = [[[NSBundle bundleForClass:[self class]] bundlePath] stringByAppendingPathComponent:@"Contents/Scripts/usr-local-install.sh"]; 8 | 9 | char *arguments[] = {(char *)src.fileSystemRepresentation, NULL}; 10 | 11 | // we cannot use bash 12 | sudo_run_cmd((char *)script.fileSystemRepresentation, arguments, [NSString stringWithFormat:@"`%@` install failed", name]); 13 | } 14 | 15 | - (BOOL)xcodeCLTInstalled { 16 | NSTask *task = [[NSTask alloc] init]; 17 | [task setLaunchPath:@"/usr/bin/xcode-select"]; 18 | [task setArguments:@[@"-p"]]; 19 | 20 | NSPipe *nullPipe = [NSPipe pipe]; 21 | [task setStandardOutput:nullPipe]; 22 | [task setStandardError:nullPipe]; 23 | 24 | NSError *error = nil; 25 | [task launchAndReturnError:&error]; 26 | 27 | if (error) { 28 | NSLog(@"teaBASE: xcodeCLTInstalled [error]: %@", error); 29 | return NO; 30 | } 31 | 32 | [task waitUntilExit]; 33 | 34 | return task.terminationStatus == 0; 35 | } 36 | 37 | - (BOOL)xcodeInstalled { 38 | NSTask *task = [[NSTask alloc] init]; 39 | [task setLaunchPath:@"/usr/bin/mdfind"]; 40 | [task setArguments:@[@"kMDItemCFBundleIdentifier == 'com.apple.dt.Xcode'"]]; 41 | 42 | NSPipe *pipe = [NSPipe pipe]; 43 | [task setStandardOutput:pipe]; 44 | [task setStandardError:pipe]; 45 | 46 | NSError *error = nil; 47 | [task launchAndReturnError:&error]; 48 | 49 | if (error) { 50 | NSLog(@"teaBASE: xcodeInstalled [error]: %@", error); 51 | return NO; 52 | } 53 | 54 | NSFileHandle *fileHandle = [pipe fileHandleForReading]; 55 | NSData *data = [fileHandle readDataToEndOfFile]; 56 | NSString *output = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; 57 | 58 | [task waitUntilExit]; 59 | 60 | return output.length > 0; 61 | } 62 | 63 | - (BOOL)homebrewInstalled { 64 | return [NSFileManager.defaultManager isReadableFileAtPath:brewPath()]; 65 | } 66 | 67 | - (BOOL)pkgxInstalled { 68 | NSArray *locations = @[ 69 | @"/usr/local/bin/pkgx", // system-wide 70 | [NSString stringWithFormat:@"%@/.local/bin/pkgx", NSHomeDirectory()] // user-specific 71 | ]; 72 | 73 | NSFileManager *fm = NSFileManager.defaultManager; 74 | for (NSString *path in locations) { 75 | if ([fm isExecutableFileAtPath:path]) { 76 | return YES; 77 | } 78 | } 79 | return NO; 80 | } 81 | 82 | - (IBAction)modalCancel:(NSButton *)sender { 83 | [NSApp endSheet:[sender window] returnCode:NSModalResponseCancel]; 84 | } 85 | 86 | - (IBAction)modalOK:(NSButton *)sender { 87 | [NSApp endSheet:[sender window] returnCode:NSModalResponseOK]; 88 | } 89 | 90 | @end 91 | -------------------------------------------------------------------------------- /Sources/teaBASE+DotfileSync.m: -------------------------------------------------------------------------------- 1 | #import "teaBASE.h" 2 | 3 | @implementation teaBASE (dotfileSync) 4 | 5 | - (BOOL)dotfileSyncEnabled { 6 | return run(@"/bin/launchctl", @[@"list", @"xyz.tea.BASE.dotfile-sync"], nil); 7 | } 8 | 9 | - (BOOL)dotfileDirThere { 10 | BOOL isdir = NO; 11 | id path = [NSHomeDirectory() stringByAppendingPathComponent:@"Library/Application Support/teaBASE/dotfiles.git"]; 12 | if (![NSFileManager.defaultManager fileExistsAtPath:path isDirectory:&isdir]) return NO; 13 | if (!isdir) return NO; 14 | return YES; 15 | } 16 | 17 | - (IBAction)onDotfileSyncToggled:(NSSwitch *)sender { 18 | 19 | id dst = [NSHomeDirectory() stringByAppendingPathComponent:@"Library/LaunchAgents/xyz.tea.BASE.dotfile-sync.plist"]; 20 | 21 | if (sender.state == NSControlStateValueOn) { 22 | NSBundle *bundle = [NSBundle bundleForClass:[self class]]; 23 | id src = [bundle pathForResource:@"dotfile-sync-launchd" ofType:@"plist"]; 24 | NSString *contents = [NSString stringWithContentsOfFile:src encoding:NSUTF8StringEncoding error:nil]; 25 | id prefpane_path = [bundle bundlePath]; 26 | contents = [contents stringByReplacingOccurrencesOfString:@"$PREFPANE" withString:prefpane_path]; 27 | contents = [contents stringByReplacingOccurrencesOfString:@"$HOME" withString:NSHomeDirectory()]; 28 | [NSFileManager.defaultManager createDirectoryAtPath:[dst stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:nil]; 29 | [contents writeToFile:dst atomically:NO encoding:NSUTF8StringEncoding error:nil]; 30 | run(@"/bin/launchctl", @[@"load", dst], nil); 31 | 32 | //TODO need to wait for the above launchctl job to finish lol 33 | 34 | NSLog(@"teaBASE: %@", self.dotfileDirThere ? @"YES" :@"NO"); 35 | 36 | if (!self.dotfileDirThere) { 37 | NSString *script_path = [[[NSBundle bundleForClass:[self class]] bundlePath] stringByAppendingPathComponent:@"Contents/Scripts/dotfile-sync.sh"]; 38 | run_in_terminal(script_path, [NSBundle bundleForClass:self.class]); 39 | } 40 | 41 | } else { 42 | run(@"/bin/launchctl", @[@"unload", dst], nil); 43 | [[NSFileManager defaultManager] removeItemAtPath:dst error:nil]; 44 | } 45 | 46 | //FIXME need to know when the below script finishes before loading us into launchctl 47 | 48 | BOOL worked = [self dotfileSyncEnabled]; 49 | 50 | [self.dotfileSyncEditWhitelistButton setEnabled:worked]; 51 | [self.dotfileSyncViewRepoButton setEnabled:worked]; 52 | [self.dotfileSyncSwitch setState:worked ? NSControlStateValueOn : NSControlStateValueOff]; 53 | } 54 | 55 | - (IBAction)viewDotfilesRepo:(id)sender { 56 | //TODO use the origin remote URL to figure this out instead 57 | id pkgx = [[[NSBundle bundleForClass:[self class]] bundlePath] stringByAppendingPathComponent:@"Contents/MacOS/pkgx"]; 58 | run(pkgx, @[@"gh", @"repo", @"view", @"--web", @"dotfiles"], nil); 59 | } 60 | 61 | - (IBAction)editWhitelist:(id)sender { 62 | id url = @"https://github.com/teaxyz/teaBASE/blob/main/Scripts/dotfile-sync.sh"; 63 | [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:url]]; 64 | } 65 | 66 | @end 67 | 68 | -------------------------------------------------------------------------------- /Sources/teaBASE+SelfUpdate.m: -------------------------------------------------------------------------------- 1 | #import "teaBASE.h" 2 | 3 | @implementation teaBASE (SelfUpdate) 4 | 5 | - (void)checkForUpdates { 6 | id current_version = [[NSBundle bundleForClass:[self class]] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; 7 | current_version = [@"v" stringByAppendingString:current_version ?: @"0.0.0"]; 8 | 9 | id url = @"https://api.github.com/repos/teaxyz/teaBASE/releases/latest"; 10 | url = [NSURL URLWithString:url]; 11 | 12 | NSMutableURLRequest *rq = [NSMutableURLRequest requestWithURL:url]; 13 | [rq setValue:@"application/vnd.github+json" forHTTPHeaderField:@"Accept"]; 14 | 15 | NSURLSession *session = [NSURLSession sharedSession]; 16 | NSURLSessionDataTask *task = [session dataTaskWithRequest:rq completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { 17 | if (error) return NSLog(@"teaBASE: fetch error: %@", error.localizedDescription); 18 | if (!data) return NSLog(@"teaBASE: no data from: %@", url); 19 | 20 | NSError *jsonError; 21 | NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError]; 22 | if (jsonError) return NSLog(@"Error parsing JSON: %@", jsonError.localizedDescription); 23 | 24 | NSString *latest_version = json[@"tag_name"]; 25 | 26 | if (!semver_is_greater(latest_version, current_version)) return; 27 | 28 | id fmt = [NSString stringWithFormat:@"-%@.dmg", [latest_version substringFromIndex:1]]; 29 | for (NSDictionary *asset in json[@"assets"]) { 30 | if ([asset[@"name"] hasSuffix:fmt]) { 31 | return dispatch_async(dispatch_get_global_queue(0, 0), ^{ 32 | download(asset[@"browser_download_url"], self); 33 | }); 34 | } 35 | } 36 | }]; 37 | [task resume]; 38 | } 39 | 40 | void download(id url, teaBASE *self) { 41 | #if DEBUG 42 | NSLog(@"teaBASE: would update to: %@", url); 43 | #else 44 | NSLog(@"teaBASE: updating to: %@", url); 45 | 46 | id bundle_path = [[NSBundle bundleForClass:[self class]] bundlePath]; 47 | id script_path = [bundle_path stringByAppendingPathComponent:@"Contents/Scripts/self-update.sh"]; 48 | 49 | run(script_path, @[url, bundle_path], nil); 50 | #endif 51 | } 52 | 53 | // naive compare that ignores pre-release ids etc. 54 | BOOL semver_is_greater(NSString *version1, NSString *version2) { 55 | // Remove leading 'v' if present 56 | if ([version1 hasPrefix:@"v"]) { 57 | version1 = [version1 substringFromIndex:1]; 58 | } 59 | if ([version2 hasPrefix:@"v"]) { 60 | version2 = [version2 substringFromIndex:1]; 61 | } 62 | 63 | NSArray *components1 = [version1 componentsSeparatedByString:@"."]; 64 | NSArray *components2 = [version2 componentsSeparatedByString:@"."]; 65 | 66 | NSInteger maxLength = MAX(components1.count, components2.count); 67 | 68 | for (NSInteger i = 0; i < maxLength; i++) { 69 | NSInteger value1 = i < components1.count ? [components1[i] integerValue] : 0; 70 | NSInteger value2 = i < components2.count ? [components2[i] integerValue] : 0; 71 | 72 | if (value1 > value2) { 73 | return YES; 74 | } else if (value1 < value2) { 75 | return NO; 76 | } 77 | } 78 | 79 | return NO; 80 | } 81 | 82 | @end 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # teaBASE 2 | 3 | The developer cockpit. 4 | 5 | [![Download teaBASE.dmg](https://custom-icon-badges.demolab.com/badge/-Download-blue?style=for-the-badge&logo=download&logoColor=white "Download DMG")](https://teaxyz.github.io/teaBASE/download.html) 6 | 7 | > teaBASE is a macOS ≥13.5 Preference Pane. 8 | 9 | > [!WARNING] 10 | > teaBASE is in *beta*, almost certainly there are edge cases we are not yet 11 | > catering to. Please report any issues you encounter. 12 | 13 | 14 | ## Secure Development 15 | 16 | Set up SSH as securely as possible and configure `git` to prove your identity 17 | by signing your commits. 18 | 19 | 20 | 21 | Our `gpg` signing is best-in-class secure with way less bloat, hassle and 22 | overhead than GNU’s GPG suite. 23 | 24 | > [!TIP] 25 | > We store your GPG private key in the macOS keychain, signed such that it 26 | > is impossible for any other app to get it. We never expose it, it is fetched 27 | > for as small a time as possible into memory, used to sign your commits and 28 | > then discarded. 29 | 30 | > [!NOTE] 31 | > We use a custom fork of [withoutboats]’s [bpb]. 32 | 33 | [bpb]: https://github.com/pkgxdev/bpb 34 | [withoutboats]: https://github.com/withoutboats 35 | 36 | 37 | ## Git Add-Ons 38 | 39 | A [fork-scaled] “package manager” for the vibrant Git ecosystem. 40 | 41 | 42 | 43 | [fork scaled]: https://github.com/pkgxdev/git-gud 44 | 45 | 46 | ## Dotfile Sync 47 | 48 | 49 | 50 | teaBASE’s dotfile sync keeps your dotfiles versioned, backed up to a private 51 | GitHub repo and synchronized to any number of computers automatically. 52 | You can easily override, restore or otherwise fiddle with how it works because 53 | it’s all just `git` under the hood. 54 | 55 | > [!NOTE] 56 | > Dotfiles are `.` prefixed files in your home directory and are how open 57 | > source stores configuration. 58 | 59 | > [!CAUTION] 60 | > [Read the script] before enabling this. \ 61 | > We have carefully made every effort to ensure data loss is impossible but: 62 | > *this is new software!* 63 | 64 | [Read the script]: https://github.com/teaxyz/teaBASE/blob/main/Scripts/dotfile-sync.sh 65 | 66 | 67 | ## Install & Update Common Developer Tools 68 | 69 | 70 | 71 | 72 | 73 | We make it easy to install popular terminals and editors and set them as 74 | defaults. We also optionally can configure macOS to open all common programmer 75 | file formats with your chosen editor. 76 | 77 | ## Clean Install Pack 78 | 79 | Create an encrypted `.dmg` with your dotfiles, working source trees and 80 | your GPG private key ready for re-import. The pack can also reinstall your 81 | apps and tools. 82 | 83 | 84 | 85 |   86 | 87 | 88 | # Contributing 89 | 90 | > [!NOTE] 91 | > Building teaBASE requires Xcode >=16, which requires macOS >=14.5. 92 | > You can check device compatibility [here]. 93 | 94 | Prefpanes are fiddly. 95 | 96 | 1. Build the prefpane with Xcode. 97 | 2. Select to show the build folder from the *Product* menu 98 | 3. Quit “System Settings.app” (if open) † 99 | 4. Open the `.prefPane` product from inside the `Debug` subfolder 100 | 101 | > † System Settings.app doesn’t seem to otherwise reload the `.prefPane` 102 | > bundle. 103 | 104 | Debugging is hard. In theory you can connect the debugger. In practice logging 105 | with a `teaBASE:` prefix and filtering in “Console.app” or showing 106 | `NSAlert`s is the path of least resistance. 107 | 108 | [here]: https://support.apple.com/en-us/105113 109 | 110 | 111 | ## Contributing FAQ 112 | 113 | ### Why is this a Preference Pane Rather than a `.app`? 114 | 115 | More tools like this *should* be Preference Panes in our opinion. You don’t 116 | need Menu Bar apps or clutter in your `/Applications` for rarely used 117 | configuration tools. If you disagree we’d like to hear your take though. 118 | 119 | 120 | ### Why is this Written in Objective-C rather than Swift? 121 | 122 | Preference Panes are old-school and the continued integration of them into 123 | macOS is not well documented nor well supported. We didn’t want to risk 124 | potential deployment hassles by choosing Swift here even though we would 125 | prefer Swift. 126 | -------------------------------------------------------------------------------- /Scripts/dotfile-sync.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # * https://dotfiles.github.io/ 3 | # * https://news.ycombinator.com/item?id=11070797 4 | 5 | set -eo pipefail 6 | 7 | export GIT_DIR="${XDG_DATA_HOME:-$HOME/Library/Application Support}"/teaBASE/dotfiles.git 8 | export GIT_WORK_TREE="$HOME" 9 | 10 | BUNDLE="$(cd "$(dirname "$0")"/.. && pwd)" 11 | 12 | main() { 13 | if test -d "$GIT_DIR"; then 14 | pull 15 | push 16 | elif [ -t 0 ]; then 17 | # ^^ launchd cannot do this stuff; we need an interactive session 18 | 19 | if ! gh auth status >/dev/null 2>&1; then 20 | gh auth login 21 | fi 22 | 23 | if ! gh repo view dotfiles --json id >/dev/null 2>&1; then 24 | gh repo create dotfiles --private 25 | clone 26 | git commit --allow-empty --message 'r0' 27 | push 28 | elif cold_start_choice; then 29 | gum format "# unimplemented" "we’ll get to it soon. soz" 30 | #cold_start 31 | #clone 32 | elif test $? -eq 1; then 33 | clone 34 | cd . 35 | git checkout --force HEAD # replaces tracked files. ignores untracked files 36 | push 37 | else 38 | exit 130 # CTRL-C on cold_start_choice 39 | fi 40 | fi 41 | } 42 | 43 | clone() { 44 | USER="$(gh api user --jq '.login')" 45 | gh repo clone "git@github.com:$USER/dotfiles.git" "$GIT_DIR" -- --bare 46 | configure 47 | cp -f "$BUNDLE/Resources/dotfile-sync-exclude.txt" "$GIT_DIR/info/exclude" 48 | rm "$GIT_DIR"/hooks/*.sample # gardening 49 | } 50 | 51 | configure() { 52 | git config user.name "teaBASE" 53 | git config user.email "hello@tea.xyz" 54 | git config commit.gpgSign false 55 | } 56 | 57 | cold_start_choice() { 58 | gum format \ 59 | "# GitHub dotfiles repo found" \ 60 | "we can either replace the files here or perform an interactive merge." \ 61 | "" \ 62 | "* interactive merge allows you to choose how to combine both sets of files before committing, merging and pushing back to GitHub." \ 63 | "* *replace* overwrites local files from your remote GitHub repo. No other files will be effected." \ 64 | "" \ 65 | "> ⌃C to abort" 66 | 67 | gum confirm --affirmative=Merge --negative=Replace 68 | } 69 | 70 | cold_start() { 71 | # TODO what if the repo has no commits? 72 | 73 | unset GIT_DIR # or breaks our temporary clone 74 | 75 | cd "$(mktemp -d)" 76 | 77 | env -u GIT_WORK_TREE gh repo clone dotfiles . 78 | configure 79 | 80 | git checkout -b cold-start 81 | git add . 82 | 83 | if ! git diff-index --quiet HEAD --; then 84 | git commit --message "in situ ($(hostname))" 85 | git checkout main --force 86 | if ! git merge --no-ff cold-start --message "r$(git rev-list --count HEAD)"; then 87 | resolve 88 | fi 89 | git push origin main 90 | fi 91 | 92 | export GIT_DIR="${XDG_DATA_HOME:-$HOME/Library/Application Support}"/teaBASE/dotfiles.git 93 | } 94 | 95 | pull() { 96 | if ! git pull origin main --no-ff; then 97 | resolve 98 | fi 99 | } 100 | 101 | resolve() { 102 | #TODO if editor is a cli then we need terminal to be launched 103 | # ^^ ideally pick a merge client of some sort 104 | 105 | if test "$EDITOR" = "code"; then 106 | EDITOR="code --wait" 107 | fi 108 | # get the user to handle the merge 109 | git diff --name-only --relative -z --diff-filter=U | xargs -0 ${EDITOR} 110 | git merge --continue 111 | } 112 | 113 | push() { 114 | cd "$HOME" 115 | set -e 116 | 117 | cnf="${XDG_CONFIG_HOME:-.config}" 118 | 119 | git add .aws/config 120 | git add .bash_login 121 | git add .bashrc 122 | git add .bash_profile 123 | git add .config/btop/btop.conf 124 | git add .config/fish/config.fish 125 | git add ".*/config.xml" "$cnf/**/config.xml" 126 | git add ".*/config.yml" "$cnf/**/config.yml" ".*/config.yaml" "$cnf/**/config.yaml" 127 | git add ".*/config.json" "$cnf/**/config.json" 128 | git add ".*/settings.json" "$cnf/**/settings.json" 129 | git add .gitconfig "$cnf"/git/config 130 | git add .profile 131 | git add .vimrc 132 | git add .zprofile 133 | git add .zshenv 134 | git add .zshrc 135 | 136 | ## we don’t do .*/**/config as it tends to pick up files in cached cloned repos (eg. cargo) 137 | for x in .*/config "$cnf/**/config"; do 138 | if test -f "$x"; then 139 | git add "$x" 140 | fi 141 | done 142 | 143 | set +e 144 | cd "$OLDPWD" 145 | 146 | if ! git diff-index --quiet HEAD --; then 147 | git commit --message "r$(git rev-list --count HEAD)" 148 | git push origin main 149 | fi 150 | } 151 | 152 | prep() { 153 | export PATH="$BUNDLE/MacOS:${PATH:+:$PATH}" 154 | 155 | set -a 156 | eval "$(pkgx +gh^2 +gum +git^2)" 157 | set +a 158 | 159 | if test "$VERBOSE"; then 160 | set -x 161 | fi 162 | } 163 | 164 | prep 165 | main 166 | -------------------------------------------------------------------------------- /Scripts/publish-release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S pkgx +gh +gum +create-dmg +npx +rustup +xz bash -eo pipefail 2 | 3 | cd "$(dirname "$0")"/.. 4 | 5 | if ! test "$APPLE_PASSWORD"; then 6 | echo "\$APPLE_PASSWORD must be set to an Apple App Specific Password" 7 | exit 1 8 | fi 9 | if ! test "$APPLE_USERNAME"; then 10 | echo "\$APPLE_USERNAME must be set to the Apple ID for the \$APPLE_PASSWORD" 11 | exit 2 12 | fi 13 | 14 | if ! git diff-index --quiet HEAD --; then 15 | echo "error: dirty working tree" >&2 16 | exit 1 17 | fi 18 | 19 | if [ "$(git rev-parse --abbrev-ref HEAD)" != main ]; then 20 | echo "error: requires main branch" >&2 21 | exit 1 22 | fi 23 | 24 | if test "$VERBOSE"; then 25 | set -x 26 | fi 27 | 28 | # ensure we have the latest version tags 29 | git fetch origin -pft 30 | 31 | # ensure github tags the right release 32 | git push origin main 33 | 34 | versions="$(git tag | grep '^v[0-9]\+\.[0-9]\+\.[0-9]\+')" 35 | v_latest="$(npx --yes -- semver --include-prerelease $versions | tail -n1)" 36 | 37 | case $1 in 38 | clobber) 39 | v_new=$v_latest 40 | ;; 41 | major|minor|patch|prerelease) 42 | v_new=$(npx -- semver bump $v_latest --increment $1) 43 | ;; 44 | "") 45 | echo "usage $0 " >&2 46 | exit 1;; 47 | *) 48 | if test "$(npx --yes -- semver """$1""")" != "$1"; then 49 | echo "$1 doesn't look like valid semver." 50 | exit 1 51 | fi 52 | v_new=$1 53 | ;; 54 | esac 55 | 56 | if [ $v_new = $v_latest ] && [ "$1" != clobber ]; then 57 | echo "$v_new already exists!" >&2 58 | exit 1 59 | fi 60 | 61 | if [ "$1" == clobber ]; then 62 | true 63 | elif ! gh release view v$v_new >/dev/null 2>&1; then 64 | gum confirm "prepare draft release for $v_new?" || exit 1 65 | 66 | gh release create \ 67 | v$v_new \ 68 | --draft=true \ 69 | --generate-notes \ 70 | --notes-start-tag=v$v_latest \ 71 | --title=v$v_new 72 | else 73 | gum format "> existing $v_new release found, using that" 74 | echo #spacer 75 | fi 76 | 77 | tmp_xcconfig="$(mktemp)" 78 | echo "MARKETING_VERSION = $v_new" > "$tmp_xcconfig" 79 | 80 | xcodebuild \ 81 | -scheme teaBASE \ 82 | -configuration Release \ 83 | -xcconfig "$tmp_xcconfig" \ 84 | -derivedDataPath ./Build \ 85 | -destination "generic/platform=macOS" \ 86 | ARCHS="x86_64 arm64" \ 87 | EXCLUDED_ARCHS="" \ 88 | build 89 | 90 | BPB_DIR="$PWD/Build/Build/Intermediates.noindex/teaBASE.build/Release/teaBASE.build/DerivedSources/bpb" 91 | pushd "$BPB_DIR" 92 | rustup target add x86_64-apple-darwin 93 | ~/.cargo/bin/cargo build --release --target x86_64-apple-darwin 94 | popd 95 | 96 | lipo -create \ 97 | -output ./Build/Build/Products/Release/teaBASE.prefPane/Contents/MacOS/bpb \ 98 | "$BPB_DIR"/target/release/bpb \ 99 | "$BPB_DIR"/target/x86_64-apple-darwin/release/bpb 100 | 101 | curl https://pkgx.sh/Darwin/x86_64 -o ./Build/pkgx_intel 102 | 103 | lipo -create \ 104 | -output build/Build/Products/Release/teaBASE.prefPane/Contents/MacOS/pkgx \ 105 | ./Build/pkgx_intel \ 106 | ./Build/Build/Products/Release/teaBASE.prefPane/Contents/MacOS/pkgx 107 | 108 | codesign \ 109 | --entitlements ./Build/Build/Intermediates.noindex/teaBASE.build/Release/teaBASE.build/DerivedSources/cdto/Sources/cd_to.entitlements \ 110 | --deep --force \ 111 | --options runtime \ 112 | --sign "Developer ID Application: Tea Inc. (7WV56FL599)" \ 113 | ./Build/Build/Products/Release/teaBASE.prefPane/Contents/Resources/cd\ to.app/ 114 | 115 | codesign \ 116 | --entitlements ./Sundries/teaBASE.entitlements \ 117 | --deep --force \ 118 | --options runtime \ 119 | --sign "Developer ID Application: Tea Inc. (7WV56FL599)" \ 120 | ./Build/Build/Products/Release/teaBASE.prefPane 121 | 122 | rm -f teaBASE-$v_new.dmg 123 | 124 | create-dmg \ 125 | --volname "teaBASE v$v_new" \ 126 | --window-size 435 435 \ 127 | --window-pos 538 273 \ 128 | --filesystem APFS \ 129 | --format ULFO \ 130 | --background ./Resources/dmg-bg@2x.png \ 131 | --icon teaBASE.prefPane 217.5 223.5 \ 132 | --hide-extension teaBASE.prefPane \ 133 | --icon-size 100 \ 134 | teaBASE-$v_new.dmg \ 135 | ./Build/Build/Products/Release/teaBASE.prefPane 136 | 137 | codesign \ 138 | --force \ 139 | --sign "Developer ID Application: Tea Inc. (7WV56FL599)" \ 140 | ./teaBASE-$v_new.dmg 141 | 142 | xcrun notarytool submit \ 143 | --apple-id $APPLE_USERNAME \ 144 | --team-id 7WV56FL599 \ 145 | --password $APPLE_PASSWORD \ 146 | --wait \ 147 | ./teaBASE-$v_new.dmg 148 | 149 | xcrun stapler staple ./teaBASE-$v_new.dmg 150 | 151 | gh release upload --clobber v$v_new teaBASE-$v_new.dmg 152 | 153 | gh release view v$v_new 154 | 155 | if [ "$1" != clobber ]; then 156 | gum confirm "draft prepared, release $v_new?" || exit 1 157 | 158 | gh release edit \ 159 | v$v_new \ 160 | --verify-tag \ 161 | --latest \ 162 | --draft=false \ 163 | --discussion-category=Announcements 164 | fi 165 | 166 | gh release view v$v_new --web 167 | -------------------------------------------------------------------------------- /Resources/Media.xcassets/Wordmark.imageset/tea-base-black.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Sources/teaBASE.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface teaBASE : NSPreferencePane { 4 | NSArray *gitGudInstalledListing; 5 | NSArray *gitGudListing; 6 | } 7 | 8 | @property (weak) IBOutlet NSSwitch *sshSwitch; 9 | @property (weak) IBOutlet NSSwitch *sshPassPhraseSwitch; 10 | @property (weak) IBOutlet NSSwitch *gpgSignSwitch; 11 | 12 | @property (weak) IBOutlet NSSwitch *xcodeCLTSwitch; 13 | @property (weak) IBOutlet NSSwitch *homebrewSwitch; 14 | @property (weak) IBOutlet NSSwitch *pkgxSwitch; 15 | @property (weak) IBOutlet NSSwitch *dockerSwitch; 16 | 17 | @property (weak) IBOutlet NSLevelIndicator *ratingIndicator; 18 | @property (weak) IBOutlet NSTableView *gitExtensionsTable; 19 | @property (weak) IBOutlet NSTextField *sshPassphraseTextField; 20 | @property (weak) IBOutlet NSTextField *sshRemovePassphraseTextField; 21 | @property (weak) IBOutlet NSSwitch *sshPassphraseICloudIntegrationSwitch; 22 | @property (weak) IBOutlet NSButton *sshApplyPassphraseButton; 23 | @property (weak) IBOutlet NSButton *sshRemovePassphraseButton; 24 | 25 | @property (weak) IBOutlet NSImageView *greenCheckGPGBackup; 26 | @property (weak) IBOutlet NSImageView *greenCheckGitHubIntegration; 27 | 28 | @property (weak) IBOutlet NSTextField *gitVersion; 29 | @property (weak) IBOutlet NSTextField *brewVersion; 30 | @property (weak) IBOutlet NSTextField *pkgxVersion; 31 | @property (weak) IBOutlet NSTextField *xcodeCLTVersion; 32 | @property (weak) IBOutlet NSTextField *dockerVersion; 33 | 34 | @property (weak) IBOutlet NSTextField *setupGPGWindowUsername; 35 | @property (weak) IBOutlet NSTextField *setupGPGWindowEmail; 36 | 37 | @property (weak) IBOutlet NSWindow *sshPassphraseWindow; 38 | @property (weak) IBOutlet NSWindow *sshRemovePassphraseWindow; 39 | @property (weak) IBOutlet NSWindow *gpgPassphraseWindow; 40 | @property (weak) IBOutlet NSWindow *brewInstallWindow; 41 | 42 | @property (weak) IBOutlet NSProgressIndicator *brewInstallWindowSpinner; 43 | @property (weak) IBOutlet NSButton *setupBrewShellEnvCheckbox; 44 | 45 | @property (weak) IBOutlet NSUserDefaultsController *defaultsController; 46 | 47 | @property (weak) IBOutlet NSComboButton *installGitButton; 48 | 49 | @property (weak) IBOutlet NSWindow *gitGudWindow; 50 | @property (weak) IBOutlet NSProgressIndicator *gitGudWindowSpinner; 51 | @property (weak) IBOutlet NSTableView *gitGudTableView; 52 | @property (weak) IBOutlet NSButton *gitGudInstallButton; 53 | @property (weak) IBOutlet NSButton *gitGudUninstallButton; 54 | @property (weak) IBOutlet NSButton *gitGudBVetButton; 55 | 56 | @property (weak) IBOutlet NSTextView *brewManualInstallInstructions; 57 | 58 | @property (weak) IBOutlet NSSwitch *dotfileSyncSwitch; 59 | @property (weak) IBOutlet NSButton *dotfileSyncEditWhitelistButton; 60 | @property (weak) IBOutlet NSButton *dotfileSyncViewRepoButton; 61 | 62 | @property (weak) IBOutlet NSTextField *selfVersionLabel; 63 | 64 | @property (weak) IBOutlet NSTextField *gitIdentityLabel; 65 | @property (weak) IBOutlet NSTextField *gitIdentityUsernameLabel; 66 | @property (weak) IBOutlet NSTextField *gitIdentityEmailLabel; 67 | @property (weak) IBOutlet NSWindow *gitIdentityWindow; 68 | 69 | @property (weak) IBOutlet NSButton *warpInstallButton; 70 | @property (weak) IBOutlet NSButton *hyperInstallButton; 71 | @property (weak) IBOutlet NSButton *iterm2InstallButton; 72 | @property (weak) IBOutlet NSPopUpButton *defaultTerminalChooser; 73 | 74 | @property (weak) IBOutlet NSButton *vscodeInstallButton; 75 | @property (weak) IBOutlet NSButton *cursorInstallButton; 76 | @property (weak) IBOutlet NSButton *cotEditorInstallButton; 77 | @property (weak) IBOutlet NSButton *zedInstallButton; 78 | @property (weak) IBOutlet NSTextField *defaultEditorLabel; 79 | 80 | @property (weak) IBOutlet NSWindow *defaultEditorWindow; 81 | @property (weak) IBOutlet NSPopUpButton *defaultEditorChooser; 82 | @property (weak) IBOutlet NSButton *addAdditionalProgrammerTextFormatsCheckbox; 83 | 84 | - (void)calculateSecurityRating; 85 | - (void)updateVersions; 86 | 87 | @end 88 | 89 | @interface teaBASE (SSH) 90 | - (void)updateSSHStates; 91 | @end 92 | 93 | @interface teaBASE (GPG) 94 | - (BOOL)gpgSignEnabled; 95 | @end 96 | 97 | @interface teaBASE (Helpers) 98 | - (void)installSubexecutable:(NSString *)name; 99 | - (BOOL)xcodeCLTInstalled; 100 | - (BOOL)xcodeInstalled; 101 | - (BOOL)homebrewInstalled; 102 | - (BOOL)pkgxInstalled; 103 | @end 104 | 105 | @interface teaBASE (git) 106 | - (void)updateGitGudListing; 107 | - (void)updateGitIdentity; 108 | @end 109 | 110 | @interface teaBASE (dotfileSync) 111 | - (BOOL)dotfileSyncEnabled; 112 | @end 113 | 114 | @interface teaBASE (SelfUpdate) 115 | - (void)checkForUpdates; 116 | @end 117 | 118 | @interface teaBASE (DevTools) 119 | - (void)updateInstallationStatuses; 120 | @end 121 | 122 | 123 | BOOL run(NSString *cmd, NSArray *args, NSPipe *pipe); 124 | BOOL file_contains(NSString *path, NSString *token); 125 | BOOL sudo_run_cmd(char *cmd, char *arguments[], NSString *errorTitle); 126 | BOOL run_in_terminal(NSString *cmd, NSBundle *bundle); 127 | NSString *output(NSString *cmd, NSArray *args); 128 | NSString *which(NSString *cmd); 129 | NSString *brewPath(void); 130 | -------------------------------------------------------------------------------- /Sources/teaBASE+GPG.m: -------------------------------------------------------------------------------- 1 | #import "teaBASE.h" 2 | 3 | @implementation teaBASE (GPG) 4 | 5 | - (BOOL)gpgSignEnabled { 6 | id pkgx = [[[NSBundle bundleForClass:[self class]] bundlePath] stringByAppendingPathComponent:@"Contents/MacOS/pkgx"]; 7 | return [output(pkgx, @[@"git", @"config", @"--global", @"commit.gpgsign"]) isEqualToString:@"true"]; 8 | } 9 | 10 | - (NSString *)bpbConfigPath { 11 | //FIXME if XDG_* vars set uses that which requires us to run a script in a login shell to extract 12 | id configfile = [NSHomeDirectory() stringByAppendingPathComponent:@".config/pkgx/bpb.toml"]; 13 | if (![NSFileManager.defaultManager isReadableFileAtPath:configfile]) { 14 | configfile = [NSHomeDirectory() stringByAppendingPathComponent:@".local/share/pkgx/bpb.toml"]; 15 | } 16 | return configfile; 17 | } 18 | 19 | - (IBAction)signCommits:(NSSwitch *)sender { 20 | NSString *git = which(@"git"); 21 | NSArray *config = @[@"config", @"--global"]; 22 | 23 | if ([git isEqualToString:@"/usr/bin/git"] && ![self xcodeCLTInstalled]) { 24 | git = [[[NSBundle bundleForClass:[self class]] bundlePath] stringByAppendingPathComponent:@"Contents/MacOS/pkgx"]; 25 | config = @[@"git", @"config", @"--global"]; 26 | } 27 | 28 | if (sender.state == NSControlStateValueOn) { 29 | 30 | 31 | id configfile = [self bpbConfigPath]; 32 | 33 | if ([NSFileManager.defaultManager isReadableFileAtPath:configfile]) { 34 | run(git, [config arrayByAddingObjectsFromArray:@[@"commit.gpgsign", @"true"]], nil); 35 | run(git, [config arrayByAddingObjectsFromArray:@[@"gpg.program", @"bpb"]], nil); 36 | 37 | if (![NSFileManager.defaultManager isExecutableFileAtPath:@"/usr/local/bin/bpb"]) { 38 | [self installSubexecutable:@"bpb"]; 39 | } 40 | 41 | [self calculateSecurityRating]; 42 | } 43 | else [self.mainView.window beginSheet:self.gpgPassphraseWindow completionHandler:^(NSModalResponse returnCode) { 44 | if (returnCode != NSModalResponseOK) { 45 | [self.gpgSignSwitch setState:NSControlStateValueOff]; 46 | } else { 47 | id username = self.setupGPGWindowUsername.stringValue; 48 | id email = self.setupGPGWindowEmail.stringValue; 49 | [self installBPB:username email:email]; 50 | 51 | //TODO need to have a git installed first 52 | run(git, [config arrayByAddingObjectsFromArray:@[@"commit.gpgsign", @"true"]], nil); 53 | run(git, [config arrayByAddingObjectsFromArray:@[@"gpg.program", @"bpb"]], nil); 54 | [self calculateSecurityRating]; 55 | } 56 | }]; 57 | } else { 58 | run(git, [config arrayByAddingObjectsFromArray:@[@"commit.gpgsign", @"false"]], nil); 59 | [self calculateSecurityRating]; 60 | } 61 | } 62 | 63 | - (void)installBPB:(id)username email:(id)email { 64 | if (![NSFileManager.defaultManager isExecutableFileAtPath:@"/usr/local/bin/bpb"]) { 65 | [self installSubexecutable:@"bpb"]; 66 | } 67 | 68 | if (![NSFileManager.defaultManager isReadableFileAtPath:[self bpbConfigPath]]) { 69 | id initstr = [NSString stringWithFormat:@"%@ <%@>", username, email]; 70 | run(@"/usr/local/bin/bpb", @[@"init", initstr], nil); 71 | } 72 | } 73 | 74 | - (IBAction)printGPGEmergencyKit:(id)sender { 75 | NSString *pubkey = output(@"/usr/local/bin/bpb", @[@"print"]); 76 | NSString *privkey = output(@"/usr/bin/security", @[@"find-generic-password", @"-s", @"xyz.tea.BASE.bpb", @"-w"]); 77 | 78 | if (!pubkey || !privkey) { 79 | NSAlert *alert = [NSAlert new]; 80 | alert.informativeText = @"An error occurred trying to obtain your GPG keypair"; 81 | [alert runModal]; 82 | return; 83 | } 84 | 85 | id content = @"Public Key:\n\n"; 86 | content = [content stringByAppendingString:pubkey]; 87 | content = [content stringByAppendingString:@"\n\nPrivate Key:\n\n"]; 88 | content = [content stringByAppendingString:privkey]; 89 | 90 | // Create an NSTextView and set the document content 91 | NSTextView *textView = [[NSTextView alloc] initWithFrame:NSMakeRect(0, 0, 612, 612)]; // Typical page size 92 | [textView setString:content]; 93 | 94 | // Set font to Menlo (monospace) and make it smaller in order to fit the page 95 | NSFont *monoFont = [NSFont fontWithName:@"Menlo" size:9.6]; 96 | [textView setFont:monoFont]; 97 | [[textView textStorage] setFont:monoFont]; // Ensure the entire text storage uses the font 98 | 99 | // Configure the print operation for the text view 100 | NSPrintOperation *printOperation = [NSPrintOperation printOperationWithView:textView]; 101 | [printOperation setShowsPrintPanel:YES]; 102 | [printOperation setShowsProgressPanel:YES]; 103 | [printOperation setJobTitle:@"GPG_Emergency_Kit.pdf"]; 104 | 105 | // Run the print operation (this will display the print dialog) 106 | [printOperation runOperationModalForWindow:[sender window] delegate:self didRunSelector:@selector(didPrintGPGEmergencyKit:success:) contextInfo:nil]; 107 | } 108 | 109 | - (void)didPrintGPGEmergencyKit:(NSPrintOperation *)op success:(BOOL)success { 110 | if (success) { 111 | self.greenCheckGPGBackup.hidden = NO; 112 | [self.defaultsController.defaults setValue:@YES forKey:@"xyz.tea.BASE.printed-GPG-emergency-kit"]; 113 | [self calculateSecurityRating]; 114 | } 115 | } 116 | 117 | @end 118 | -------------------------------------------------------------------------------- /Scripts/make-clean-install-pack.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S pkgx +gum bash>=4 -eo pipefail 2 | 3 | PREFPANE="$(cd "$(dirname "$0")"/../.. && pwd)" 4 | 5 | d="$(mktemp -dt teaBASE)" 6 | 7 | gum format \ 8 | "# teaBASE clean install pack" \ 9 | "clean installing your machine regularly is good developer hygiene." 10 | echo #spacer 11 | 12 | cd "$d" 13 | 14 | if command -v brew >/dev/null 2>&1; then 15 | gum format "## Running \`brew bundle dump\`" 16 | brew bundle dump 17 | fi 18 | 19 | echo #spacer 20 | gum format "## dotfiles" "adding whitelisted files" 21 | 22 | cd "$HOME" 23 | 24 | cnf="${XDG_CONFIG_HOME:-.config}" 25 | cnf="${cnf/#$HOME/}" 26 | cnf="${cnf#/}" 27 | 28 | datahome="${XDG_DATA_HOME:-$HOME/.local/share}" 29 | datahome="${datahome/#$HOME/}" 30 | datahome="${datahome#/}" 31 | 32 | dotfiles=() 33 | 34 | # note, not space safe 35 | #TODO some of these eg. .config/git/config are XDG aware 36 | for x in .aws/* \ 37 | .bash_login \ 38 | .bash_history \ 39 | .bashrc \ 40 | .bash_profile \ 41 | .*/config $cnf/**/config \ 42 | $cnf/btop/btop.conf \ 43 | $cnf/fish/config.fish \ 44 | $cnf/pkgx/bpb.toml $datahome/pkgx/bpb.toml \ 45 | .*/config.xml $cnf/**/config.xml \ 46 | .*/config.yml $cnf/**/config.yml .*/config.yaml $cnf/**/config.yaml \ 47 | .*/config.json $cnf/**/config.json \ 48 | $datahome/share/fish/fish_history \ 49 | .duckdb_history \ 50 | .gitconfig $cnf/git/* \ 51 | .irb_history \ 52 | .lesshst \ 53 | .netrc \ 54 | .node_repl_history \ 55 | .profile \ 56 | .python_history \ 57 | .*/settings.json $cnf/**/settings.json \ 58 | .sh_history \ 59 | .ssh/* \ 60 | .sqlite_history \ 61 | .vimrc \ 62 | .zprofile \ 63 | .zsh_history \ 64 | .zshenv \ 65 | .zshrc 66 | do 67 | if test -f "$x"; then 68 | dotfiles+=("$x") 69 | gum format "\`~/$x\`" 70 | fi 71 | done 72 | 73 | if test -d .zsh_sessions; then 74 | dotfiles+=(.zsh_sessions) 75 | gum format "\`~/.zsh_sessions\`" 76 | fi 77 | 78 | tar cf "$d/dotfiles.tar" "${dotfiles[@]}" 79 | 80 | add_file() { 81 | STEM="$1" 82 | 83 | gitdirs=() 84 | mapfile -d '' gitdirs < <(find "$STEM" -name .git -type d -print0) 85 | 86 | if [ "${#gitdirs[@]}" -eq 0 ]; then 87 | tar rf "$d/dotfiles.tar" "$STEM" 88 | else 89 | exclude_file="$(mktemp -t teaBASE)" 90 | 91 | srcdirs=() 92 | for gitdir in "${gitdirs[@]}"; do 93 | srcdir="$(dirname "$gitdir")" 94 | srcdirs+=("$srcdir") 95 | 96 | # get a list of all files except those that are ignored 97 | # rationale: `node_modules` etc. are gigabytes of caching 98 | mapfile -d '' tracked_files < <(git -C "$srcdir" ls-files --ignored --others --cached --directory --exclude-standard -z) 99 | 100 | for file in "${tracked_files[@]}"; do 101 | echo "$srcdir/$file" >> "$exclude_file" 102 | done 103 | done 104 | 105 | tar rf "$d/dotfiles.tar" --exclude-from="$exclude_file" "$STEM" 106 | fi 107 | } 108 | 109 | gum format \ 110 | "# add additional files" \ 111 | "for example, you may like to add your \`~/srcs\` directory." \ 112 | "> we exclude files according to any discovered \`.gitignore\` files." \ 113 | "" "or dotfiles we didn’t add above" \ 114 | "> add dotfiles to our whitelist: https://github.com/teaxyz/teaBASE/issues/new" 115 | 116 | while gum confirm "add additional files to pack?" 117 | do 118 | file="$(gum file "$HOME" --all --file --directory)" 119 | 120 | STEM="${file#$HOME/}" 121 | 122 | if test "$STEM" = "$file"; then 123 | gum format "error: \`$file\` is not in \`$HOME\`" >&2 124 | elif test -f "$file"; then 125 | tar rf "$d/dotfiles.tar" "$STEM" 126 | gum format "\`~/$STEM\`" 127 | else 128 | export d 129 | export -f add_file 130 | gum spin --show-output --title "adding \`~/$STEM\`" -- bash -c "add_file \"$STEM\"" 131 | fi 132 | done 133 | 134 | cd "$d" 135 | 136 | if test -x /usr/local/bin/bpb && gum confirm "include GPG private key?"; then 137 | gum format "you will be prompted for your login password *twice*" 138 | 139 | BPB="$(security find-generic-password -s xyz.tea.BASE.bpb -w)" 140 | fi 141 | 142 | #TODO pkg brew into pkgx 143 | cat <restore.command 144 | #!/bin/bash 145 | 146 | cd "\$(dirname "\$0")" 147 | 148 | PATH="\$PWD/teaBASE.prefPane/Contents/MacOS:\$PATH" 149 | 150 | set -a 151 | eval "\$(pkgx +gum +mas)" 152 | set +a 153 | 154 | gum spin --title 'installing teaBASE' -- ditto teaBASE.prefPane ~/Library/PreferencePanes/teaBASE.prefPane 155 | 156 | if gum confirm "extract dotfiles to \\\`\$HOME\\\`?"; then 157 | gum spin -- tar xf dotfiles.tar --cd "\$HOME" 158 | fi 159 | 160 | if test -f Brewfile && gum confirm 'install Homebrew; restore \`Brewfile\`?'; then 161 | /bin/bash -c "\$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" 162 | 163 | PATH="/opt/homebrew/bin:\$PATH" brew bundle install 164 | fi 165 | 166 | if test "$BPB"; then 167 | gum format "# restoring GPG private key" 168 | bpb import "$BPB" 169 | fi 170 | 171 | EoSH 172 | 173 | 174 | unset BPB 175 | 176 | chmod +x restore.command 177 | 178 | for x in "$PREFPANE" ~/Library/PreferencePanes/teaBASE.prefPane /Library/PreferencePanes/teaBASE.prefPane 179 | do 180 | if test "$(basename "$x")" = teaBASE.prefPane; then 181 | gum spin --title "copying teaBASE" -- ditto "$x" ./teaBASE.prefPane 182 | break 183 | fi 184 | done 185 | 186 | if ! test -d teaBASE.prefPane; then 187 | gum format \ 188 | "# error: couldn’t bundle teaBASE" \ 189 | "We will finish the bundle, but you’ll have to apply its contents yourself." 190 | fi 191 | 192 | gum format \ 193 | "# creating DMG" \ 194 | "enter an encryption password for your pack when prompted" 195 | 196 | hdiutil create \ 197 | -volname "teaBASE Clean Install Pack" \ 198 | -encryption AES-256 \ 199 | -stdinpass \ 200 | -format UDZO \ 201 | -srcfolder "$d" \ 202 | ~/Downloads/Clean\ Install\ Pack.dmg 203 | 204 | rm -rf "$d" 205 | 206 | gum format \ 207 | "# the pack is in ~/Downloads" \ 208 | "**for the love of all that is good!** *verify* you can open the DMG with your password before clean installing!" 209 | -------------------------------------------------------------------------------- /Sources/misc.m: -------------------------------------------------------------------------------- 1 | @import Foundation; 2 | @import AppKit; 3 | 4 | NSString *brewPath(void) { 5 | #if __arm64__ 6 | return @"/opt/homebrew/bin/brew"; 7 | #else 8 | return @"/usr/local/bin/brew"; 9 | #endif 10 | } 11 | 12 | BOOL run(NSString *cmd, NSArray *args, NSPipe *pipe) { 13 | if (![NSFileManager.defaultManager isExecutableFileAtPath:cmd]) { 14 | // throws an exception if cannot execute which makes the prefpane go POOF 15 | return -1; 16 | } 17 | 18 | id brew = [brewPath() stringByDeletingLastPathComponent]; 19 | id PATH = [NSString stringWithFormat:@"%@:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin", brew]; 20 | 21 | NSTask *task = [NSTask new]; 22 | [task setLaunchPath:cmd]; 23 | [task setArguments:args]; 24 | // need to add other PATHs to PATH since GUI apps don’t operate with the user’s shell rc added 25 | [task setEnvironment:@{ 26 | @"PATH": PATH, 27 | @"HOME": NSHomeDirectory() 28 | }]; 29 | if (pipe) [task setStandardError:pipe]; 30 | id error; 31 | @try { 32 | [task launchAndReturnError:&error]; // configures task to not throw and thus potentially break us 33 | if (!error) { 34 | [task waitUntilExit]; 35 | return task.terminationStatus == 0; 36 | } else { 37 | NSLog(@"teaBASE: %@", error); 38 | return -1; 39 | } 40 | } @catch (id e) { 41 | NSLog(@"teaBASE: %@", e); 42 | return -2; 43 | } 44 | } 45 | 46 | NSString *which(NSString *cmd) { 47 | id brew = [brewPath() stringByDeletingLastPathComponent]; 48 | NSArray *paths = @[brew, @"/usr/local/bin", @"/usr/bin", @"/bin", @"/usr/sbin", @"/sbin"]; 49 | 50 | for (NSString *dir in paths) { 51 | NSString *path = [dir stringByAppendingPathComponent:cmd]; 52 | if ([[NSFileManager defaultManager] isExecutableFileAtPath:path]) { 53 | return path; 54 | } 55 | } 56 | 57 | return cmd; //ohwell 58 | } 59 | 60 | NSString *output(NSString *cmd, NSArray *args) { 61 | if (![NSFileManager.defaultManager isExecutableFileAtPath:cmd]) { 62 | // throws an exception if cannot execute which makes the prefpane go POOF 63 | return nil; 64 | } 65 | 66 | NSTask *task = [NSTask new]; 67 | [task setLaunchPath:cmd]; 68 | [task setArguments:args]; 69 | NSPipe *pipe = [NSPipe pipe]; 70 | [task setStandardOutput:pipe]; 71 | id error; 72 | [task launchAndReturnError:&error]; // configures task to not throw and thus potentially break us 73 | if (error) return nil; 74 | [task waitUntilExit]; 75 | if (task.terminationStatus == 0) { 76 | NSData *data = [[pipe fileHandleForReading] readDataToEndOfFile]; 77 | id str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; 78 | return [str stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; 79 | } else { 80 | return nil; 81 | } 82 | } 83 | 84 | BOOL file_contains(NSString *path, NSString *token) { 85 | NSFileHandle *fileHandle = [NSFileHandle fileHandleForReadingAtPath:path]; 86 | 87 | if (fileHandle == nil) { 88 | return NO; // TODO error condition 89 | } 90 | 91 | // Read the first 1024 bytes (or less if the file is shorter) 92 | NSData *fileData = [fileHandle readDataOfLength:1024]; 93 | [fileHandle closeFile]; 94 | 95 | if (fileData == nil || [fileData length] == 0) { 96 | return NO; // TODO error condition 97 | } 98 | 99 | // Convert the data to a string 100 | NSString *fileContents = [[NSString alloc] initWithData:fileData encoding:NSUTF8StringEncoding]; 101 | 102 | if (fileContents == nil) { 103 | return NO; // TODO error condition 104 | } 105 | 106 | // Normalize whitespace in both strings before comparison 107 | NSString *normalizedContents = [fileContents stringByReplacingOccurrencesOfString:@"\\s+" withString:@" " options:NSRegularExpressionSearch range:NSMakeRange(0, fileContents.length)]; 108 | NSString *normalizedToken = [token stringByReplacingOccurrencesOfString:@"\\s+" withString:@" " options:NSRegularExpressionSearch range:NSMakeRange(0, token.length)]; 109 | 110 | NSRange range = [normalizedContents rangeOfString:normalizedToken]; 111 | return (range.location != NSNotFound); 112 | } 113 | 114 | BOOL sudo_run_cmd(char *cmd, char *arguments[], NSString *errorTitle) { 115 | 116 | #define DIE(xx) { dispatch_async(dispatch_get_main_queue(), ^{ \ 117 | NSAlert *alert = [NSAlert new]; \ 118 | alert.messageText = errorTitle; \ 119 | alert.informativeText = @"dunno why ∵ we cannot get stderr (stage " xx ")"; \ 120 | [alert runModal]; \ 121 | AuthorizationFree(authorization, kAuthorizationFlagDefaults); \ 122 | }); return NO; } 123 | 124 | AuthorizationRef authorization; 125 | OSStatus status = AuthorizationCreate(NULL, NULL, kAuthorizationFlagDefaults, &authorization); 126 | if (status != errAuthorizationSuccess) DIE("0"); 127 | 128 | AuthorizationItem items = {kAuthorizationRightExecute, 0, NULL, 0}; 129 | AuthorizationRights rights = {1, &items}; 130 | status = AuthorizationCopyRights(authorization, &rights, NULL, kAuthorizationFlagDefaults | kAuthorizationFlagInteractionAllowed | kAuthorizationFlagExtendRights | kAuthorizationFlagPreAuthorize, NULL); 131 | 132 | if (status != errAuthorizationSuccess) DIE(); 133 | if (status != errAuthorizationSuccess) DIE("1"); 134 | 135 | #pragma clang diagnostic push 136 | #pragma clang diagnostic ignored "-Wdeprecated-declarations" 137 | status = AuthorizationExecuteWithPrivileges(authorization, cmd, kAuthorizationFlagDefaults, arguments, NULL); 138 | #pragma clang diagnostic pop 139 | 140 | if (status != errAuthorizationSuccess) DIE("2"); 141 | 142 | int wait_status; 143 | pid_t pid = wait(&wait_status); 144 | if (pid == -1 || !WIFEXITED(wait_status) || WEXITSTATUS(wait_status) != 0) { 145 | DIE(); 146 | } 147 | 148 | AuthorizationFree(authorization, kAuthorizationFlagDefaults); 149 | 150 | return YES; 151 | 152 | #undef DIE 153 | } 154 | 155 | BOOL run_in_terminal(NSString *input, NSBundle *bundle) { 156 | id brew = [brewPath() stringByDeletingLastPathComponent]; 157 | id bndl = [bundle.bundlePath stringByAppendingPathComponent:@"Contents/MacOS"]; 158 | id path = [NSString stringWithFormat:@"%@:%@:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin", bndl, brew]; 159 | 160 | char template[] = "/tmp/teaBASE_XXXXXX.command"; 161 | int fd = mkstemps(template, 8); 162 | write(fd, "#!/bin/sh\n", 10); 163 | 164 | NSData *data = [[NSString stringWithFormat:@"export PATH='%@'\n", path] dataUsingEncoding:NSUTF8StringEncoding]; 165 | write(fd, data.bytes, data.length); 166 | 167 | data = [input dataUsingEncoding:NSUTF8StringEncoding]; 168 | write(fd, data.bytes, data.length); 169 | 170 | close(fd); 171 | 172 | path = [NSString stringWithUTF8String:template]; 173 | 174 | [NSFileManager.defaultManager setAttributes:@{NSFilePosixPermissions:@(0755)} ofItemAtPath:path error:nil]; 175 | 176 | if ([NSWorkspace.sharedWorkspace openURL:[NSURL fileURLWithPath:path]]) { 177 | return YES; 178 | } else { 179 | NSLog(@"teaBASE: execution failed: %@", input); 180 | return NO; 181 | } 182 | } 183 | 184 | 185 | @interface VerticallyAlignedTextFieldCell: NSTextFieldCell 186 | @end 187 | 188 | @implementation VerticallyAlignedTextFieldCell 189 | 190 | - (NSRect)titleRectForBounds:(NSRect)theRect { 191 | NSRect titleFrame = [super titleRectForBounds:theRect]; 192 | NSSize titleSize = [[self attributedStringValue] size]; 193 | titleFrame.origin.y = theRect.origin.y - .5 + (theRect.size.height - titleSize.height) / 2.0; 194 | return titleFrame; 195 | } 196 | 197 | - (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView { 198 | NSRect titleRect = [self titleRectForBounds:cellFrame]; 199 | [[self attributedStringValue] drawInRect:titleRect]; 200 | } 201 | 202 | @end 203 | -------------------------------------------------------------------------------- /Sources/teaBASE+git.m: -------------------------------------------------------------------------------- 1 | #import "teaBASE.h" 2 | 3 | @implementation teaBASE (git) 4 | 5 | - (IBAction)installGit:(NSButton *)sender { 6 | if (sender.selectedTag != 2) { 7 | run(@"/usr/bin/xcode-select", @[@"--install"], nil); 8 | // for weird reasons the install window does not come to the front on Sonoma 9 | run(@"/usr/bin/open", @[@"/System/Library/CoreServices/Install Command Line Developer Tools.app"], nil); 10 | } else { 11 | run(brewPath(), @[@"install", @"git"], nil); 12 | } 13 | } 14 | 15 | - (IBAction)editGitIdentity:(NSButton *)sender { 16 | [sender.window beginSheet:self.gitIdentityWindow completionHandler:^(NSModalResponse returnCode) { 17 | if (returnCode != NSModalResponseOK) return; 18 | 19 | id pkgx = [[[NSBundle bundleForClass:[self class]] bundlePath] stringByAppendingPathComponent:@"Contents/MacOS/pkgx"]; 20 | run(pkgx, @[@"git", @"config", @"--global", @"user.name", self.gitIdentityUsernameLabel.stringValue], nil); 21 | run(pkgx, @[@"git", @"config", @"--global", @"user.email", self.gitIdentityEmailLabel.stringValue], nil); 22 | 23 | self.gitIdentityLabel.stringValue = [NSString stringWithFormat:@"%@ <%@>", self.gitIdentityUsernameLabel.stringValue, self.gitIdentityEmailLabel.stringValue]; 24 | }]; 25 | } 26 | 27 | - (void)updateGitGudListing { 28 | NSString *pkgx = [[[NSBundle bundleForClass:[self class]] bundlePath] stringByAppendingPathComponent:@"Contents/MacOS/pkgx"]; 29 | 30 | //FIXME deno will cache this permanently, we need to version it or pkg this properly 31 | id url = @"https://raw.githubusercontent.com/pkgxdev/git-gud/refs/heads/main/src/app.ts"; 32 | 33 | dispatch_async(dispatch_get_global_queue(0, 0), ^{ 34 | NSString *json = output(pkgx, @[ 35 | @"+git", 36 | @"deno~2.0", @"run", @"--unstable-kv", @"-A", url, @"lsij" 37 | ]); 38 | 39 | if (!json) return; 40 | 41 | NSData *jsonData = [json dataUsingEncoding:NSUTF8StringEncoding]; 42 | 43 | NSError *error; 44 | id jsonObject = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error]; 45 | 46 | dispatch_async(dispatch_get_main_queue(), ^{ 47 | if (error) { 48 | [[NSAlert alertWithError:error] runModal]; 49 | } else { 50 | self->gitGudInstalledListing = jsonObject; 51 | [self.gitExtensionsTable reloadData]; 52 | } 53 | }); 54 | }); 55 | } 56 | 57 | - (IBAction)manageGitGud:(id)sender { 58 | if (!gitGudListing) { 59 | [self reloadGitGudListing:sender]; 60 | } 61 | 62 | [self.mainView.window beginSheet:self.gitGudWindow completionHandler:^(NSModalResponse returnCode) { 63 | [self updateGitGudListing]; 64 | }]; 65 | } 66 | 67 | - (void)reloadGitGudListing:(id)sender{ 68 | //TODO need to update this sometimes 69 | 70 | [self.gitGudWindowSpinner startAnimation:sender]; 71 | 72 | NSString *pkgx = [[[NSBundle bundleForClass:[self class]] bundlePath] stringByAppendingPathComponent:@"Contents/MacOS/pkgx"]; 73 | id url = @"https://raw.githubusercontent.com/pkgxdev/git-gud/refs/heads/main/src/app.ts"; 74 | 75 | dispatch_async(dispatch_get_global_queue(0, 0), ^{ 76 | @try { 77 | NSString *json = output(pkgx, @[ 78 | @"+git", 79 | @"deno~2.0", @"run", @"--unstable-kv", @"-Ar", url, @"lsj" 80 | ]); 81 | 82 | NSError *error; 83 | NSData *jsonData = [json dataUsingEncoding:NSUTF8StringEncoding]; 84 | id jsonObject = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error]; 85 | 86 | dispatch_async(dispatch_get_main_queue(), ^{ 87 | if (error) { 88 | [[NSAlert alertWithError:error] runModal]; 89 | } else { 90 | self->gitGudListing = jsonObject; 91 | [self.gitGudTableView reloadData]; 92 | [self updateGitGudSelection]; 93 | } 94 | }); 95 | } @catch (id e) { 96 | //noop 97 | } @finally { 98 | dispatch_async(dispatch_get_main_queue(), ^{ 99 | [self.gitGudWindowSpinner stopAnimation:sender]; 100 | }); 101 | } 102 | }); 103 | } 104 | 105 | - (IBAction)vetGitGudPackage:(id)sender { 106 | NSInteger row = self.gitGudTableView.selectedRow; 107 | if (row < 0 || row >= gitGudListing.count) return; 108 | NSString *name = [gitGudListing[row] objectForKey:@"name"]; 109 | if (!name) return; 110 | 111 | NSString *pkgx = [[[NSBundle bundleForClass:[self class]] bundlePath] stringByAppendingPathComponent:@"Contents/MacOS/pkgx"]; 112 | id url = @"https://raw.githubusercontent.com/pkgxdev/git-gud/refs/heads/main/src/app.ts"; 113 | 114 | run(pkgx, @[ 115 | @"+git", 116 | @"deno~2.0", @"run", @"--unstable-kv", @"-A", url, @"vet", name 117 | ], nil); 118 | } 119 | 120 | - (IBAction)installGitGudPackage:(id)sender { 121 | [self.gitGudWindowSpinner startAnimation:sender]; 122 | 123 | NSInteger row = self.gitGudTableView.selectedRow; 124 | if (row < 0 || row >= gitGudListing.count) return; 125 | NSString *name = [gitGudListing[row] objectForKey:@"name"]; 126 | if (!name) return; 127 | 128 | NSString *pkgx = [[[NSBundle bundleForClass:[self class]] bundlePath] stringByAppendingPathComponent:@"Contents/MacOS/pkgx"]; 129 | id url = @"https://raw.githubusercontent.com/pkgxdev/git-gud/refs/heads/main/src/app.ts"; 130 | 131 | run(pkgx, @[ 132 | @"+git", 133 | @"deno~2.0", @"run", @"--unstable-kv", @"-A", url, @"install", name 134 | ], nil); 135 | 136 | [self reloadGitGudListing:sender]; 137 | } 138 | 139 | - (IBAction)uninstallGitGudPackage:(id)sender { 140 | [self.gitGudWindowSpinner startAnimation:sender]; 141 | 142 | NSInteger row = self.gitGudTableView.selectedRow; 143 | if (row < 0 || row >= gitGudListing.count) return; 144 | NSString *name = [gitGudListing[row] objectForKey:@"name"]; 145 | if (!name) return; 146 | 147 | NSString *pkgx = [[[NSBundle bundleForClass:[self class]] bundlePath] stringByAppendingPathComponent:@"Contents/MacOS/pkgx"]; 148 | id url = @"https://raw.githubusercontent.com/pkgxdev/git-gud/refs/heads/main/src/app.ts"; 149 | 150 | run(pkgx, @[ 151 | @"+git", 152 | @"deno~2.0", @"run", @"--unstable-kv", @"-A", url, @"uninstall", name 153 | ], nil); 154 | 155 | [self reloadGitGudListing:sender]; 156 | } 157 | 158 | - (void)updateGitGudSelection { 159 | NSInteger row = [self.gitGudTableView selectedRow]; 160 | if (row < 0 || row >= gitGudListing.count) { 161 | [self.gitGudInstallButton setEnabled:NO]; 162 | [self.gitGudUninstallButton setEnabled:NO]; 163 | [self.gitGudBVetButton setEnabled:NO]; 164 | } else { 165 | BOOL installed = [gitGudListing[row] boolForKey:@"installed"]; 166 | [self.gitGudInstallButton setEnabled:!installed]; 167 | [self.gitGudUninstallButton setEnabled:installed]; 168 | [self.gitGudBVetButton setEnabled:YES]; 169 | } 170 | } 171 | 172 | - (void)updateGitIdentity { 173 | id pkgx = [[[NSBundle bundleForClass:[self class]] bundlePath] stringByAppendingPathComponent:@"Contents/MacOS/pkgx"]; 174 | id user = output(pkgx, @[@"git", @"config", @"--global", @"user.name"]); 175 | id mail = output(pkgx, @[@"git", @"config", @"--global", @"user.email"]); 176 | if (user && mail) { 177 | self.gitIdentityLabel.stringValue = [NSString stringWithFormat:@"%@ <%@>", user, mail]; 178 | self.gitIdentityUsernameLabel.stringValue = user; 179 | self.gitIdentityEmailLabel.stringValue = mail; 180 | } 181 | } 182 | 183 | @end 184 | 185 | 186 | @implementation teaBASE (NSTableViewDataSource) 187 | 188 | - (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView { 189 | if (tableView == self.gitExtensionsTable) { 190 | return gitGudInstalledListing.count; 191 | } else { 192 | return gitGudListing.count; 193 | } 194 | } 195 | 196 | - (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row { 197 | if (tableView == self.gitExtensionsTable) { 198 | if ([tableColumn.identifier isEqualToString:@"name"]) { 199 | return [gitGudInstalledListing[row] objectForKey:@"name"]; 200 | } else if ([tableColumn.identifier isEqualToString:@"type"]) { 201 | return [gitGudInstalledListing[row] objectForKey:@"description"]; 202 | } 203 | } else { 204 | if ([tableColumn.identifier isEqualToString:@"name"]) { 205 | return [gitGudListing[row] objectForKey:@"name"]; 206 | } else if ([tableColumn.identifier isEqualToString:@"description"]) { 207 | return [gitGudListing[row] objectForKey:@"description"]; 208 | } else if ([tableColumn.identifier isEqualToString:@"installed"]) { 209 | return [[gitGudListing[row] objectForKey:@"installed"] boolValue] ? @"✓" : @""; 210 | } 211 | } 212 | return @""; 213 | } 214 | 215 | - (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row { 216 | return 20; 217 | } 218 | 219 | - (void)tableViewSelectionDidChange:(NSNotification *)notification { 220 | if (notification.object == self.gitGudTableView) { 221 | [self updateGitGudSelection]; 222 | } 223 | } 224 | 225 | @end 226 | -------------------------------------------------------------------------------- /Sources/teaBASE.m: -------------------------------------------------------------------------------- 1 | #import "teaBASE.h" 2 | 3 | @implementation teaBASE 4 | 5 | - (void)mainViewDidLoad { 6 | for (NSTableColumn *col in self.gitExtensionsTable.tableColumns) { 7 | col.headerCell.attributedStringValue = [[NSAttributedString alloc] initWithString:col.title attributes:@{NSFontAttributeName: [NSFont systemFontOfSize:10]}]; 8 | } 9 | } 10 | 11 | - (void)willSelect { 12 | // Initially disable all interactive elements 13 | [self.gpgSignSwitch setEnabled:NO]; 14 | [self.homebrewSwitch setEnabled:NO]; 15 | [self.pkgxSwitch setEnabled:NO]; 16 | [self.xcodeCLTSwitch setEnabled:NO]; 17 | [self.dockerSwitch setEnabled:NO]; 18 | [self.dotfileSyncSwitch setEnabled:NO]; 19 | [self.dotfileSyncEditWhitelistButton setEnabled:NO]; 20 | [self.dotfileSyncViewRepoButton setEnabled:NO]; 21 | 22 | dispatch_group_t group = dispatch_group_create(); 23 | dispatch_queue_t backgroundQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 24 | 25 | // Create containers for async results 26 | __block BOOL hasSignedCommits = NO; 27 | __block BOOL homebrewInstalled = NO; 28 | __block BOOL pkgxInstalled = NO; 29 | __block BOOL xcodeCLTInstalled = NO; 30 | __block BOOL dotfileSyncEnabled = NO; 31 | 32 | // Perform heavy operations in background 33 | dispatch_group_async(group, backgroundQueue, ^{ 34 | [self updateSSHStates]; 35 | }); 36 | 37 | dispatch_group_async(group, backgroundQueue, ^{ 38 | hasSignedCommits = [self gpgSignEnabled]; 39 | dispatch_async(dispatch_get_main_queue(), ^{ 40 | [self.gpgSignSwitch setState:hasSignedCommits ? NSControlStateValueOn : NSControlStateValueOff]; 41 | [self.gpgSignSwitch setEnabled:YES]; 42 | }); 43 | }); 44 | 45 | dispatch_group_async(group, backgroundQueue, ^{ 46 | [self updateVersions]; 47 | dispatch_async(dispatch_get_main_queue(), ^{ 48 | [self.dockerSwitch setState:self.dockerVersion.stringValue.length > 0 ? NSControlStateValueOn : NSControlStateValueOff]; 49 | [self.dockerSwitch setEnabled:YES]; 50 | }); 51 | }); 52 | 53 | dispatch_group_async(group, backgroundQueue, ^{ 54 | homebrewInstalled = [self homebrewInstalled]; 55 | pkgxInstalled = [self pkgxInstalled]; 56 | xcodeCLTInstalled = [self xcodeCLTInstalled]; 57 | dotfileSyncEnabled = self.dotfileSyncEnabled; 58 | 59 | dispatch_async(dispatch_get_main_queue(), ^{ 60 | [self.homebrewSwitch setState:homebrewInstalled ? NSControlStateValueOn : NSControlStateValueOff]; 61 | [self.pkgxSwitch setState:pkgxInstalled ? NSControlStateValueOn : NSControlStateValueOff]; 62 | [self.xcodeCLTSwitch setState:xcodeCLTInstalled ? NSControlStateValueOn : NSControlStateValueOff]; 63 | [self.dotfileSyncSwitch setState:dotfileSyncEnabled ? NSControlStateValueOn : NSControlStateValueOff]; 64 | 65 | [self.homebrewSwitch setEnabled:YES]; 66 | [self.pkgxSwitch setEnabled:YES]; 67 | [self.xcodeCLTSwitch setEnabled:YES]; 68 | [self.dotfileSyncSwitch setEnabled:YES]; 69 | 70 | BOOL dotfileSyncActive = self.dotfileSyncSwitch.state == NSControlStateValueOn; 71 | [self.dotfileSyncEditWhitelistButton setEnabled:dotfileSyncActive]; 72 | [self.dotfileSyncViewRepoButton setEnabled:dotfileSyncActive]; 73 | }); 74 | }); 75 | 76 | // Once all background tasks complete, update remaining UI elements 77 | dispatch_group_notify(group, dispatch_get_main_queue(), ^{ 78 | if ([self.defaultsController.defaults boolForKey:@"xyz.tea.BASE.integrated-GitHub"]) { 79 | self.greenCheckGitHubIntegration.hidden = NO; 80 | } 81 | if ([self.defaultsController.defaults boolForKey:@"xyz.tea.BASE.printed-GPG-emergency-kit"]) { 82 | self.greenCheckGPGBackup.hidden = NO; 83 | } 84 | 85 | [self calculateSecurityRating]; 86 | [self updateGitGudListing]; 87 | [self updateGitIdentity]; 88 | 89 | id v = [[NSBundle bundleForClass:[self class]] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; 90 | if (v) { 91 | self.selfVersionLabel.stringValue = [NSString stringWithFormat:@"v%@", v]; 92 | } 93 | }); 94 | 95 | [self updateInstallationStatuses]; 96 | } 97 | 98 | - (void)didSelect { 99 | [self checkForUpdates]; 100 | } 101 | 102 | - (void)calculateSecurityRating { 103 | BOOL hasSignedCommits = self.gpgSignSwitch.state == NSControlStateValueOn; 104 | BOOL hasSSHPassPhrase = self.sshPassPhraseSwitch.state == NSControlStateValueOn; 105 | BOOL hasSSH = self.sshSwitch.state == NSControlStateValueOn; 106 | BOOL hasGitHubIntegration = self.greenCheckGitHubIntegration.hidden == NO; 107 | BOOL hasGPGBackup = self.greenCheckGPGBackup.hidden == NO; 108 | 109 | float rating = hasSignedCommits + hasSSHPassPhrase + hasSSH + hasGitHubIntegration + hasGPGBackup; 110 | [self.ratingIndicator setIntValue:rating]; 111 | 112 | if (self.ratingIndicator.intValue >= self.ratingIndicator.maxValue) { 113 | [self.ratingIndicator setFillColor:[NSColor systemGreenColor]]; 114 | } else if (self.ratingIndicator.intValue <= 1) { 115 | [self.ratingIndicator setFillColor:[NSColor systemRedColor]]; 116 | } 117 | } 118 | 119 | - (void)updateVersions { 120 | NSString *brew_out = output(brewPath(), @[@"--version"]); 121 | NSString *pkgx_out = output(@"/usr/local/bin/pkgx", @[@"--version"]); 122 | NSString *xcode_clt_out = output(@"/usr/sbin/pkgutil", @[@"--pkg-info=com.apple.pkg.CLTools_Executables"]); 123 | 124 | id path = [[[NSBundle bundleForClass:[self class]] bundlePath] stringByAppendingPathComponent:@"Contents/Scripts/docker-version.sh"]; 125 | 126 | NSString *docker_version = output(path, @[]); 127 | 128 | BOOL has_clt = [self xcodeCLTInstalled]; 129 | BOOL has_xcode = [self xcodeInstalled]; 130 | // emulate login shell to ensure PATH contains everything the user has configured 131 | // ∵ GUI apps do not have full user PATH set otherwise 132 | NSString *git_out = nil; 133 | if (has_clt || has_xcode) { 134 | // only check if clt or xcode otherwise this may trigger the XcodeCLT installation GUI flow 135 | git_out = output(@"/bin/sh", @[@"-l", @"-c", @"git --version"]); 136 | } 137 | 138 | brew_out = [[brew_out componentsSeparatedByString:@" "] lastObject]; // Homebrew 1.2.3 139 | pkgx_out = [[pkgx_out componentsSeparatedByString:@" "] lastObject]; // pkgx 1.2.3 140 | git_out = [[git_out componentsSeparatedByString:@" "] objectAtIndex:2]; // git version 1.2.3 141 | xcode_clt_out = [[[[xcode_clt_out componentsSeparatedByString:@"\n"] objectAtIndex:1] componentsSeparatedByString:@" "] objectAtIndex:1]; // Version: 1.2.3 142 | 143 | self.brewVersion.stringValue = brew_out ? [NSString stringWithFormat:@"v%@", brew_out] : @""; 144 | self.pkgxVersion.stringValue = pkgx_out ? [NSString stringWithFormat:@"v%@", pkgx_out] : @""; 145 | self.gitVersion.stringValue = git_out ? [NSString stringWithFormat:@"v%@", git_out] : @""; 146 | self.xcodeCLTVersion.stringValue = xcode_clt_out ? [NSString stringWithFormat:@"v%@", xcode_clt_out] : @""; 147 | self.dockerVersion.stringValue = docker_version ? [@"v" stringByAppendingString:docker_version] : @""; 148 | 149 | [self.installGitButton setHidden:git_out != nil]; 150 | } 151 | 152 | - (IBAction)openGitHub:(id)sender { 153 | NSURL *url = [NSURL URLWithString:@"https://github.com/teaxyz/teaBASE"]; 154 | [[NSWorkspace sharedWorkspace] openURL:url]; 155 | } 156 | 157 | - (IBAction)onShareClicked:(id)sender { 158 | const int starCount = self.ratingIndicator.intValue; 159 | 160 | // Construct the stars string 161 | NSMutableString *stars = [NSMutableString string]; 162 | for (int i = 0; i < 5; i++) { 163 | if (i < starCount) { 164 | [stars appendString:@"★"]; 165 | } else { 166 | [stars appendString:@"☆"]; 167 | } 168 | } 169 | 170 | // URL encode the message 171 | NSString *message = [NSString stringWithFormat:@"I got %@ developer security with @teaprotocol’s teaBASE", stars]; 172 | NSString *encodedMessage = [message stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]; 173 | 174 | // Construct the full URL 175 | NSString *urlString = [NSString stringWithFormat:@"https://x.com/intent/tweet?text=%@", encodedMessage]; 176 | NSURL *url = [NSURL URLWithString:urlString]; 177 | 178 | // Open the URL 179 | [NSWorkspace.sharedWorkspace openURL:url]; 180 | } 181 | 182 | @end 183 | 184 | 185 | @implementation teaBASE (Integration) 186 | 187 | - (IBAction)integrateWithGitHub:(id)sender { 188 | //TODO not great since we don’t know when we’re finished 189 | //NOTE `gh` fails when uploading an existing GPG key (though is fine for existing ssh keys) 190 | //TODO ^^ report as bug? 191 | //TODO pipe output and handle it so exit code is good 192 | 193 | NSString *script_path = [[[NSBundle bundleForClass:[self class]] bundlePath] stringByAppendingPathComponent:@"Contents/Scripts/github-integration.sh"]; 194 | 195 | run_in_terminal(script_path, [NSBundle bundleForClass:self.class]); 196 | 197 | self.greenCheckGitHubIntegration.hidden = NO; 198 | [self.defaultsController.defaults setValue:@YES forKey:@"xyz.tea.BASE.integrated-GitHub"]; 199 | [self calculateSecurityRating]; 200 | } 201 | 202 | @end 203 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2022–23 pkgx inc. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Sources/teaBASE+DevTools.m: -------------------------------------------------------------------------------- 1 | #import "teaBASE.h" 2 | 3 | @implementation teaBASE (DevTools) 4 | 5 | - (IBAction)installBrew:(NSSwitch *)sender { 6 | if (![NSFileManager.defaultManager isExecutableFileAtPath:@"/Library/Developer/CommandLineTools/usr/bin/git"]) { 7 | NSAlert *alert = [NSAlert new]; 8 | alert.messageText = @"Prerequisite Unsatisfied"; 9 | alert.informativeText = @"Homebrew requires the Xcode Command Line Tools (CLT) to be installed first"; 10 | [alert runModal]; 11 | 12 | [sender setState:NSControlStateValueOff]; 13 | return; 14 | } 15 | 16 | if (sender.state == NSControlStateValueOn) { 17 | [self.brewManualInstallInstructions setEditable:YES]; 18 | [self.brewManualInstallInstructions checkTextInDocument:sender]; 19 | [self.brewManualInstallInstructions setEditable:NO]; 20 | 21 | [self.mainView.window beginSheet:self.brewInstallWindow completionHandler:^(NSModalResponse returnCode) { 22 | if (returnCode != NSModalResponseOK) { 23 | [self.homebrewSwitch setState:NSControlStateValueOff]; 24 | } else { 25 | [self updateVersions]; 26 | } 27 | [self.brewInstallWindowSpinner stopAnimation:sender]; 28 | }]; 29 | } else { 30 | #if __arm64 31 | // Get the contents of the directory 32 | NSError *error = nil; 33 | NSArray *contents = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:@"/opt/homebrew" error:&error]; 34 | 35 | if (error) { 36 | [[NSAlert alertWithError:error] runModal]; 37 | return; 38 | } 39 | 40 | // Iterate over each item in the directory 41 | for (NSString *item in contents) { 42 | NSString *itemPath = [@"/opt/homebrew" stringByAppendingPathComponent:item]; 43 | 44 | BOOL success = [[NSFileManager defaultManager] removeItemAtPath:itemPath error:&error]; 45 | if (!success) { 46 | [[NSAlert alertWithError:error] runModal]; 47 | return; 48 | } 49 | } 50 | 51 | [self updateVersions]; 52 | #else 53 | NSAlert *alert = [NSAlert new]; 54 | alert.informativeText = @"Please manually run the Homebrew uninstall script"; 55 | [alert runModal]; 56 | [sender setState:NSControlStateValueOn]; 57 | #endif 58 | } 59 | } 60 | 61 | static BOOL installer(NSURL *url) { 62 | NSURL *newurl = [[url URLByDeletingPathExtension] URLByAppendingPathExtension:@".pkg"]; 63 | [NSFileManager.defaultManager moveItemAtURL:url toURL:newurl error:nil]; 64 | 65 | char *arguments[] = {"-pkg", (char*)newurl.fileSystemRepresentation, "-target", "/", NULL}; 66 | 67 | return sudo_run_cmd("/usr/sbin/installer", arguments, @"Homebrew install failed"); 68 | } 69 | 70 | static NSString* fetchLatestBrewVersion(void) { 71 | NSURL *url = [NSURL URLWithString:@"https://api.github.com/repos/Homebrew/brew/releases/latest"]; 72 | NSData *data = [NSData dataWithContentsOfURL:url]; 73 | if (!data) return nil; 74 | 75 | NSError *error = nil; 76 | NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; 77 | if (error || !json[@"tag_name"]) return nil; 78 | 79 | NSString *version = json[@"tag_name"]; 80 | if ([version hasPrefix:@"v"]) { 81 | version = [version substringFromIndex:1]; 82 | } 83 | return version; 84 | } 85 | 86 | - (IBAction)installBrewStep2:(NSButton *)sender { 87 | [sender setEnabled:NO]; 88 | [self.brewInstallWindowSpinner startAnimation:sender]; 89 | 90 | NSString *version = fetchLatestBrewVersion(); 91 | if (!version) { 92 | NSAlert *alert = [NSAlert new]; 93 | alert.messageText = @"Failed to fetch latest Homebrew version"; 94 | alert.informativeText = @"Please try again later or install manually."; 95 | [alert runModal]; 96 | [NSApp endSheet:self.brewInstallWindow returnCode:NSModalResponseAbort]; 97 | [sender setEnabled:YES]; 98 | return; 99 | } 100 | 101 | NSString *urlstr = [NSString stringWithFormat:@"https://github.com/Homebrew/brew/releases/download/%@/Homebrew-%@.pkg", version, version]; 102 | NSURL *url = [NSURL URLWithString:urlstr]; 103 | 104 | [[[NSURLSession sharedSession] downloadTaskWithURL:url completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) { 105 | if (error) { 106 | dispatch_async(dispatch_get_main_queue(), ^{ 107 | [[NSAlert alertWithError:error] runModal]; 108 | [NSApp endSheet:self.brewInstallWindow returnCode:NSModalResponseAbort]; 109 | [sender setEnabled:YES]; 110 | }); 111 | } else if (installer(location)) { 112 | // ^^ runs the installer on the NSURLSession queue as the download 113 | // is deleted when it exits. afaict this is fine. 114 | dispatch_async(dispatch_get_main_queue(), ^{ 115 | 116 | if (self.setupBrewShellEnvCheckbox.state == NSControlStateValueOn) { 117 | NSString *zprofilePath = [NSHomeDirectory() stringByAppendingPathComponent:@".zprofile"]; 118 | NSString *cmdline = [NSString stringWithFormat:@"eval \"$(%@ shellenv)\"", brewPath()]; 119 | 120 | BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:zprofilePath]; 121 | 122 | // Check if the file exists, if not create it 123 | if (!exists) { 124 | [[NSFileManager defaultManager] createFileAtPath:zprofilePath contents:nil attributes:nil]; 125 | } 126 | if (!file_contains(zprofilePath, cmdline)) { 127 | // Open the file for appending 128 | NSFileHandle *fileHandle = [NSFileHandle fileHandleForWritingAtPath:zprofilePath]; 129 | if (fileHandle) { 130 | [fileHandle seekToEndOfFile]; 131 | if (exists) { 132 | [fileHandle writeData:[@"\n" dataUsingEncoding:NSUTF8StringEncoding]]; 133 | } 134 | [fileHandle writeData:[cmdline dataUsingEncoding:NSUTF8StringEncoding]]; 135 | [fileHandle writeData:[@"\n" dataUsingEncoding:NSUTF8StringEncoding]]; 136 | [fileHandle closeFile]; 137 | } else { 138 | //TODO 139 | } 140 | } 141 | } 142 | 143 | [NSApp endSheet:self.brewInstallWindow returnCode:NSModalResponseOK]; 144 | [sender setEnabled:YES]; 145 | }); 146 | } else { 147 | dispatch_async(dispatch_get_main_queue(), ^{ 148 | NSAlert *alert = [NSAlert new]; 149 | alert.messageText = @"Installation Error"; 150 | alert.informativeText = @"Unknown error occurred. Please install Homebrew manually."; 151 | [alert runModal]; 152 | [NSApp endSheet:self.brewInstallWindow returnCode:NSModalResponseAbort]; 153 | [sender setEnabled:YES]; 154 | }); 155 | } 156 | }] resume]; 157 | } 158 | 159 | - (IBAction)installPkgx:(NSSwitch *)sender { 160 | if (sender.state == NSControlStateValueOn) { 161 | [self installSubexecutable:@"pkgx"]; 162 | [self updateVersions]; 163 | } else { 164 | char *args[] = {"/usr/local/bin/pkgx", NULL}; 165 | sudo_run_cmd("/bin/rm", args, @"Couldn’t delete /usr/local/bin/pkgx"); 166 | } 167 | } 168 | 169 | - (IBAction)installDocker:(NSSwitch *)sender { 170 | // using a terminal as the install steps requires `sudo` 171 | id path = [[[NSBundle bundleForClass:[self class]] bundlePath] stringByAppendingPathComponent:@"Contents/Scripts/install-docker.sh"]; 172 | run_in_terminal(path, [NSBundle bundleForClass:self.class]); 173 | } 174 | 175 | - (IBAction)openDockerHome:(id)sender { 176 | NSURL *url = [NSURL URLWithString:@"https://docker.com"]; 177 | [[NSWorkspace sharedWorkspace] openURL:url]; 178 | } 179 | 180 | - (IBAction)openPkgxHome:(id)sender { 181 | NSURL *url = [NSURL URLWithString:@"https://pkgx.sh"]; 182 | [[NSWorkspace sharedWorkspace] openURL:url]; 183 | } 184 | 185 | - (IBAction)openHomebrewHome:(id)sender { 186 | NSURL *url = [NSURL URLWithString:@"https://brew.sh"]; 187 | [[NSWorkspace sharedWorkspace] openURL:url]; 188 | } 189 | 190 | - (IBAction)openXcodeCLTHome:(id)sender { 191 | NSURL *url = [NSURL URLWithString:@"https://developer.apple.com/xcode/resources/"]; 192 | [[NSWorkspace sharedWorkspace] openURL:url]; 193 | } 194 | 195 | - (IBAction)gitAddOnsHelpButton:(id)sender { 196 | NSURL *url = [NSURL URLWithString:@"https://github.com/pkgxdev/git-gud"]; 197 | [[NSWorkspace sharedWorkspace] openURL:url]; 198 | } 199 | 200 | - (IBAction)installCaskItem:(NSButton *)sender { 201 | if (sender.imagePosition == NSNoImage) { 202 | [[NSWorkspace sharedWorkspace] openURLs:@[] withAppBundleIdentifier:sender.toolTip options:NSWorkspaceLaunchDefault 203 | additionalEventParamDescriptor:nil launchIdentifiers:nil]; 204 | } else { 205 | if (![NSFileManager.defaultManager isExecutableFileAtPath:brewPath()]) { 206 | NSAlert *alert = [NSAlert new]; 207 | alert.messageText = @"Prerequisite Unsatisfied"; 208 | alert.informativeText = @"Homebrew must be installed first."; 209 | [alert runModal]; 210 | return; 211 | } 212 | 213 | id cmd = [NSString stringWithFormat:@"brew install --cask %@", sender.identifier]; 214 | run_in_terminal(cmd, [NSBundle bundleForClass:self.class]); 215 | [sender setTitle:@"Installing…"]; 216 | [sender setImage:[NSImage imageWithSystemSymbolName:@"circle.lefthalf.striped.horizontal.inverse" accessibilityDescription:nil]]; 217 | } 218 | } 219 | 220 | NSString* getBundleIDForUTI(NSString* uti) { 221 | CFStringRef cfUTI = (__bridge CFStringRef)uti; 222 | LSRolesMask role = kLSRolesAll; 223 | 224 | CFStringRef bundleID = LSCopyDefaultRoleHandlerForContentType(cfUTI, role); 225 | 226 | if (bundleID != NULL) { 227 | NSString* result = (__bridge_transfer NSString*)bundleID; 228 | return result; 229 | } 230 | 231 | return nil; 232 | } 233 | 234 | - (void)updateInstallationStatuses { 235 | [self.defaultTerminalChooser removeAllItems]; 236 | [self.defaultTerminalChooser addItemWithTitle:@"Terminal.app"]; 237 | [self.defaultTerminalChooser itemAtIndex:0].identifier = @"com.apple.terminal"; 238 | 239 | [self.defaultEditorChooser removeAllItems]; 240 | [self.defaultEditorChooser addItemWithTitle:@"TextEdit.app"]; 241 | [self.defaultEditorChooser itemAtIndex:0].identifier = @"com.apple.TextEdit"; 242 | 243 | #define update_button(btn, bundleID, chooser, title) { \ 244 | BOOL is_installed = [[NSWorkspace sharedWorkspace] URLForApplicationWithBundleIdentifier:bundleID] != nil; \ 245 | [btn setTitle:is_installed ? @"Open" : @"Install"]; \ 246 | [btn setImage:[NSImage imageWithSystemSymbolName:is_installed ? @"checkmark.circle" : @"arrow.down.circle" accessibilityDescription:nil]]; \ 247 | [btn setImagePosition:is_installed ? NSNoImage : NSImageLeft]; \ 248 | if (is_installed) [chooser addItemWithTitle:title]; \ 249 | if ([defaultBundleID isEqualToString:bundleID]) [chooser selectItemWithTitle:title]; \ 250 | [chooser itemWithTitle:title].identifier = bundleID; \ 251 | } 252 | 253 | #define update(btn, bundleID) \ 254 | update_button(btn, bundleID, self.defaultTerminalChooser, btn.identifier) 255 | 256 | NSString* defaultBundleID = getBundleIDForUTI(@"public.unix-executable"); 257 | update(self.warpInstallButton, @"dev.warp.Warp-Stable"); 258 | update(self.hyperInstallButton, @"co.zeit.Hyper"); 259 | update(self.iterm2InstallButton, @"com.googlecode.iterm2"); 260 | 261 | #undef update 262 | #define update(btn, bundleID, title) \ 263 | update_button(btn, bundleID, self.defaultEditorChooser, title); \ 264 | if ([bundleID isEqualToString:defaultBundleID]) { \ 265 | self.defaultEditorLabel.stringValue = title; \ 266 | } 267 | 268 | defaultBundleID = getBundleIDForUTI(@"public.text"); 269 | update(self.vscodeInstallButton, @"com.microsoft.VSCode", @"Visual Studio Code"); 270 | update(self.cotEditorInstallButton, @"com.coteditor.CotEditor", @"Cot"); 271 | update(self.zedInstallButton, @"dev.zed.Zed", @"Zed"); 272 | update(self.cursorInstallButton, @"com.todesktop.230313mzl4w4u92", @"Cursor"); 273 | #undef update 274 | } 275 | 276 | - (IBAction)onDefaultTerminalChanged:(NSPopUpButton *)sender { 277 | CFStringRef bundleID = (__bridge CFStringRef)sender.selectedItem.identifier; 278 | 279 | LSSetDefaultRoleHandlerForContentType((__bridge CFStringRef)@"public.unix-executable", kLSRolesShell, bundleID); 280 | LSSetDefaultRoleHandlerForContentType((__bridge CFStringRef)@"com.apple.terminal.shell-script", kLSRolesShell, bundleID); 281 | 282 | [self updateInstallationStatuses]; 283 | } 284 | 285 | - (IBAction)openCdToLocationInFinder:(id)sender { 286 | id path = [[[NSBundle bundleForClass:[self class]] bundlePath] stringByAppendingPathComponent:@"Contents/Resources/cd to.app"]; 287 | [[NSWorkspace sharedWorkspace] activateFileViewerSelectingURLs:@[[NSURL fileURLWithPath:path]]]; 288 | } 289 | 290 | - (IBAction)showDefaultEditorWindow:(id)sender { 291 | [[sender window] beginSheet:self.defaultEditorWindow completionHandler:^(NSModalResponse returnCode) { 292 | 293 | CFStringRef bundleID = (__bridge CFStringRef)self.defaultEditorChooser.selectedItem.identifier; 294 | 295 | self.defaultEditorLabel.stringValue = self.defaultEditorChooser.selectedItem.title; 296 | 297 | LSSetDefaultRoleHandlerForContentType((__bridge CFStringRef)@"public.text", kLSRolesEditor | kLSRolesViewer, bundleID); 298 | LSSetDefaultRoleHandlerForContentType((__bridge CFStringRef)@"public.plain-text", kLSRolesEditor | kLSRolesViewer, bundleID); 299 | 300 | if (self.addAdditionalProgrammerTextFormatsCheckbox.state == NSControlStateValueOn) { 301 | id utis = @[ 302 | @"public.plain-text", 303 | @"public.source-code", 304 | @"public.swift-source", 305 | @"public.geojson", 306 | @"public.protobuf-source", 307 | @"com.apple.property-list", 308 | @"com.apple.xml-property-list", 309 | @"com.apple.ascii-property-list", 310 | @"public.c-header", 311 | @"public.c-plus-plus-header", 312 | @"public.c-source", 313 | @"public.c-source.preprocessed", 314 | @"public.opencl-source", 315 | @"public.module-map", 316 | @"public.objective-c-source", 317 | @"public.objective-c-source.preprocessed", 318 | @"public.objective-c-plus-plus-source", 319 | @"public.objective-c-plus-plus-source.preprocessed", 320 | @"public.c-plus-plus-source", 321 | @"public.c-plus-plus-source.preprocessed", 322 | @"public.assembly-source", 323 | @"public.nasm-assembly-source", 324 | @"public.yacc-source", 325 | @"public.lex-source", 326 | @"public.mig-source", 327 | @"public.ruby-script", 328 | @"public.python-script", 329 | @"public.php-script", 330 | @"public.perl-script", 331 | @"public.make-source", 332 | @"public.bash-script", 333 | @"public.shell-script", 334 | @"public.csh-script", 335 | @"public.ksh-script", 336 | @"public.tcsh-script", 337 | @"public.zsh-script", 338 | @"public.xml", 339 | @"net.daringfireball.markdown", 340 | @"public.json", 341 | @"public.json.jsonc", 342 | @"public.yaml", 343 | @"public.css", 344 | @"com.microsoft.typescript", 345 | @"org.python.restructuredtext", 346 | @"org.lua.lua-source", 347 | @"com.netscape.javascript-source", 348 | @"org.rust-lang.rust-script", 349 | @"org.rust-lang.rust", 350 | @"public.html", //NOTE doesn't seem to stick 351 | @"org.golang.go-script", 352 | @"public.comma-separated-values-text", 353 | @"org.iso.sql", 354 | @"com.sun.java-source", 355 | @"com.microsoft.c-sharp", 356 | @"org.tug.tex", 357 | @"public.toml", 358 | @"com.microsoft.ini", 359 | @"public.patch-file", 360 | @"dev.dart.dart-script", 361 | @"public.mpeg-2-transport-stream", // steals .ts extensions 362 | ]; 363 | 364 | for (id uti in utis) { 365 | LSSetDefaultRoleHandlerForContentType((__bridge CFStringRef)uti, kLSRolesEditor | kLSRolesViewer, bundleID); 366 | } 367 | } 368 | }]; 369 | } 370 | 371 | @end 372 | -------------------------------------------------------------------------------- /Sources/teaBASE+SSH.m: -------------------------------------------------------------------------------- 1 | #import "teaBASE.h" 2 | 3 | @implementation teaBASE (SSH) 4 | 5 | - (void)updateSSHStates { 6 | BOOL hasSSH = [self checkForSSH]; 7 | BOOL hasSSHPassPhrase = hasSSH && [self checkForSSHPassPhrase]; // check both ∵ our pp-check is false-positive if no key file 8 | BOOL hasICloudIntegration = hasSSH && hasSSHPassPhrase && [self checkForSSHPassphraseICloudKeychainIntegration]; 9 | 10 | [self.sshSwitch setState:hasSSH ? NSControlStateValueOn : NSControlStateValueOff]; 11 | [self.sshPassPhraseSwitch setState:hasSSHPassPhrase ? NSControlStateValueOn : NSControlStateValueOff]; 12 | [self.sshPassphraseICloudIntegrationSwitch setState:hasICloudIntegration ? NSControlStateValueOn : NSControlStateValueOff]; 13 | 14 | self.sshPassPhraseSwitch.enabled = hasSSH; 15 | self.sshPassphraseICloudIntegrationSwitch.enabled = hasSSH; 16 | } 17 | 18 | - (NSString *)sshPrivateKeyFile { 19 | NSURL *home = [NSFileManager.defaultManager homeDirectoryForCurrentUser]; 20 | 21 | //TODO filenames can be arbitrary and configurable which makes life complex 22 | // e.g we could just try and figure out what is used for github, but that's not our *whole* story is it? 23 | 24 | //NOTE order is same as ssh sources read order 25 | for (NSString *file in @[@"id_rsa", @"id_dsa", @"id_ecdsa", @"id_ed25519"]) { 26 | NSString *path = [[home.path stringByAppendingPathComponent:@".ssh"] stringByAppendingPathComponent:file]; 27 | if (![NSFileManager.defaultManager isReadableFileAtPath:path]) continue; 28 | if (![NSFileManager.defaultManager isReadableFileAtPath:[NSString stringWithFormat:@"%@.pub", path]]) continue; 29 | return path; 30 | } 31 | 32 | return nil; 33 | } 34 | 35 | - (BOOL)checkForSSH { 36 | return [self sshPrivateKeyFile] != nil; 37 | } 38 | 39 | - (BOOL)checkForSSHPassPhrase { 40 | id path = [self sshPrivateKeyFile]; 41 | 42 | // attempts to decrypt the key, if there’s a passphrase this will fail 43 | return run(@"/usr/bin/ssh-keygen", @[@"-y", @"-f", path, @"-P", @""], nil) == NO; 44 | } 45 | 46 | - (BOOL)checkForSSHPassphraseICloudKeychainIntegration { 47 | NSURL *home = [NSFileManager.defaultManager homeDirectoryForCurrentUser]; 48 | id path = [home.path stringByAppendingPathComponent:@".ssh/config"]; 49 | return file_contains(path, @"UseKeychain yes"); 50 | } 51 | 52 | - (IBAction)createSSHPrivateKey:(NSSwitch *)sender { 53 | id path = [self sshPrivateKeyFile] ?: [NSHomeDirectory() stringByAppendingPathComponent:@".ssh/id_ed25519"]; 54 | 55 | if (sender.state == NSControlStateValueOn) { 56 | NSArray *arguments = @[@"-t", @"ed25519", @"-C", @"Generated by teaBASE", @"-f", path, @"-N", @""]; 57 | 58 | if (run(@"/usr/bin/ssh-keygen", arguments, nil)) { 59 | [self calculateSecurityRating]; 60 | [self updateSSHStates]; 61 | } else { 62 | NSAlert *alert = [NSAlert new]; 63 | alert.messageText = @"ssh-keygen failed"; 64 | [alert runModal]; 65 | [self.sshSwitch setState:NSControlStateValueOff]; 66 | } 67 | } else { 68 | NSAlert *alert = [NSAlert new]; 69 | alert.alertStyle = NSAlertStyleCritical; 70 | alert.messageText = @"Data Loss Warning"; 71 | alert.informativeText = @"Deleting your SSH key pair cannot be undone by teaBASE."; 72 | 73 | NSButton *deleteButton = [alert addButtonWithTitle:@"Delete Keys"]; 74 | deleteButton.hasDestructiveAction = YES; 75 | 76 | [alert addButtonWithTitle:@"Cancel"]; 77 | 78 | [alert beginSheetModalForWindow:sender.window completionHandler:^(NSModalResponse returnCode) { 79 | if (returnCode == NSAlertFirstButtonReturn) { 80 | id err; 81 | if (![NSFileManager.defaultManager removeItemAtPath:path error:&err]) { 82 | [[NSAlert alertWithError:err] runModal]; 83 | return; 84 | } 85 | id pubpath = [NSString stringWithFormat:@"%@.pub", path]; 86 | if (![NSFileManager.defaultManager removeItemAtPath:pubpath error:&err]) { 87 | [[NSAlert alertWithError:err] runModal]; 88 | } 89 | } 90 | [self updateSSHStates]; 91 | }]; 92 | } 93 | } 94 | 95 | - (IBAction)createSSHPassPhrase:(NSSwitch *)sender { 96 | if (sender.state == NSControlStateValueOn) { 97 | [self.mainView.window beginSheet:self.sshPassphraseWindow completionHandler:^(NSModalResponse returnCode) { 98 | 99 | //TODO should not remove passphrase window until complete in case of failure 100 | 101 | if (returnCode == NSModalResponseCancel) { 102 | [self.sshPassPhraseSwitch setState:NSControlStateValueOff]; 103 | return; 104 | } 105 | 106 | id path = [self sshPrivateKeyFile]; 107 | 108 | id passphrase = self.sshPassphraseTextField.stringValue; 109 | 110 | NSPipe *pipe = [NSPipe pipe]; 111 | 112 | if (!run(@"/usr/bin/ssh-keygen", @[@"-p", @"-N", passphrase, @"-f", path], pipe)) { 113 | id stderr = [NSString stringWithUTF8String:[pipe.fileHandleForReading readDataToEndOfFile].bytes]; 114 | 115 | NSAlert *alert = [NSAlert new]; 116 | alert.messageText = @"ssh-keygen failed"; 117 | alert.informativeText = stderr; 118 | [alert runModal]; 119 | return; 120 | } 121 | 122 | [self.sshPassphraseTextField setStringValue:@""]; // get it out of memory ASAP 123 | [self.sshPassPhraseSwitch setEnabled:NO]; 124 | [self updateSSHStates]; 125 | [self calculateSecurityRating]; 126 | }]; 127 | } else { 128 | [self.mainView.window beginSheet:self.sshRemovePassphraseWindow completionHandler:^(NSModalResponse returnCode) { 129 | if (returnCode == NSModalResponseCancel) { 130 | [sender setState:NSControlStateValueOn]; 131 | return; 132 | } 133 | 134 | id path = [self sshPrivateKeyFile]; 135 | id passphrase = self.sshRemovePassphraseTextField.stringValue; 136 | 137 | NSPipe *pipe = [NSPipe pipe]; 138 | 139 | if (!run(@"/usr/bin/ssh-keygen", @[@"-p", @"-P", passphrase, @"-N", @"", @"-f", path], pipe)) { 140 | id stderr = [NSString stringWithUTF8String:[pipe.fileHandleForReading readDataToEndOfFile].bytes]; 141 | 142 | NSAlert *alert = [NSAlert new]; 143 | alert.messageText = @"Failed to remove passphrase. Incorrect passphrase?"; 144 | alert.informativeText = stderr; 145 | [alert runModal]; 146 | [sender setState:NSControlStateValueOn]; 147 | [self.sshRemovePassphraseTextField setStringValue:@""]; // Clear passphrase from memory 148 | return; 149 | } 150 | 151 | [self.sshRemovePassphraseTextField setStringValue:@""]; // Clear passphrase from memory 152 | [self updateSSHStates]; 153 | [self calculateSecurityRating]; 154 | }]; 155 | } 156 | } 157 | 158 | - (IBAction)createSSHPassPhraseStep2:(id)sender { 159 | NSAlert *alert = [NSAlert new]; 160 | alert.messageText = @"Your passphrase won’t be stored"; 161 | alert.informativeText = @"Please print the Emergency Kit and save it securely, or confirm you have another way to restore your credentials.\n\nDon’t worry—losing your SSH credentials is (usually—but tediously) recoverable."; 162 | [alert addButtonWithTitle:@"Print Kit"]; 163 | [alert addButtonWithTitle:@"Proceed Without Kit"]; 164 | [alert addButtonWithTitle:@"Cancel"]; 165 | 166 | [alert beginSheetModalForWindow:self.sshPassphraseWindow completionHandler:^(NSModalResponse returnCode) { 167 | if (returnCode == NSAlertFirstButtonReturn) { 168 | [self printSSHEmergencyKit:self.sshPassphraseTextField.stringValue sender:sender]; 169 | } else if (returnCode == NSAlertSecondButtonReturn) { 170 | [NSApp endSheet:self.sshPassphraseWindow returnCode:NSModalResponseOK]; 171 | } 172 | }]; 173 | } 174 | 175 | - (IBAction)removeSSHPassPhraseStep2:(id)sender { 176 | // We just need to go back to the main window's sheet 177 | [NSApp endSheet:self.sshRemovePassphraseWindow returnCode:NSModalResponseOK]; 178 | } 179 | 180 | - (void)printSSHEmergencyKit:(NSString *)passphrase sender:(id)sender { 181 | NSString *privkey_path = [self sshPrivateKeyFile]; 182 | NSString *pubkey_path = [NSString stringWithFormat:@"%@.pub", privkey_path]; 183 | NSString *filename = [privkey_path lastPathComponent]; 184 | NSError *error = nil; 185 | NSString *content = [NSString stringWithContentsOfFile:pubkey_path encoding:NSUTF8StringEncoding error:&error]; 186 | 187 | if (error) { 188 | [[NSAlert alertWithError:error] runModal]; 189 | return; 190 | } 191 | 192 | // Break the public key content into 70-char lines (aligned with the private key visually) to make it fit the page 193 | NSMutableString *formattedContent = [NSMutableString string]; 194 | NSUInteger lineLength = 70; 195 | NSUInteger currentIndex = 0; 196 | 197 | while (currentIndex < content.length) { 198 | NSUInteger remainingLength = content.length - currentIndex; 199 | NSUInteger substringLength = MIN(lineLength, remainingLength); 200 | NSRange range = NSMakeRange(currentIndex, substringLength); 201 | 202 | [formattedContent appendString:[content substringWithRange:range]]; 203 | [formattedContent appendString:@"\n"]; 204 | 205 | currentIndex += substringLength; 206 | } 207 | 208 | content = [NSString stringWithFormat:@"Recreate the following at: `~/.ssh/%@.pub`:\n\n%@", filename, formattedContent]; 209 | 210 | NSString *privkey_content = [NSString stringWithContentsOfFile:privkey_path encoding:NSUTF8StringEncoding error:&error]; 211 | 212 | if (error) { 213 | [[NSAlert alertWithError:error] runModal]; 214 | return; 215 | } 216 | 217 | content = [content stringByAppendingString:@"\n\n"]; 218 | content = [content stringByAppendingString:@"Recreate the following at: `~/.ssh/"]; 219 | content = [content stringByAppendingString:filename]; 220 | content = [content stringByAppendingString:@"\n\n"]; 221 | content = [content stringByAppendingString:privkey_content]; 222 | content = [content stringByAppendingString:@"\n\n"]; 223 | 224 | content = [content stringByAppendingString:@"Passphrase:\n\n"]; 225 | content = [content stringByAppendingString:passphrase]; 226 | 227 | // Create an NSTextView and set the document content 228 | NSTextView *textView = [[NSTextView alloc] initWithFrame:NSMakeRect(0, 0, 612, 612)]; // Typical page size 229 | [textView setString:content]; 230 | 231 | // Set font to Menlo (monospace) and make it smaller in order to fit the page 232 | NSFont *monoFont = [NSFont fontWithName:@"Menlo" size:10.4]; 233 | [textView setFont:monoFont]; 234 | [[textView textStorage] setFont:monoFont]; // Ensure the entire text storage uses the font 235 | 236 | // Configure the print operation for the text view 237 | NSPrintOperation *printOperation = [NSPrintOperation printOperationWithView:textView]; 238 | [printOperation setShowsPrintPanel:YES]; 239 | [printOperation setShowsProgressPanel:YES]; 240 | [printOperation setJobTitle:@"SSH_Emergency_Kit.pdf"]; 241 | 242 | // Run the print operation (this will display the print dialog) 243 | [printOperation runOperationModalForWindow:[sender window] delegate:self didRunSelector:@selector(sshPrintOperationDidRun:success:) contextInfo:nil]; 244 | } 245 | 246 | - (void)sshPrintOperationDidRun:(NSPrintOperation *)op success:(BOOL)success { 247 | if (success) { 248 | [NSApp endSheet:self.sshPassphraseWindow returnCode:NSModalResponseOK]; 249 | } 250 | } 251 | 252 | - (IBAction)configureSSHPassphraseICloudKeychainIntegration:(NSSwitch *)sender { 253 | NSURL *home = [NSFileManager.defaultManager homeDirectoryForCurrentUser]; 254 | NSString *sshDir = [home.path stringByAppendingPathComponent:@".ssh"]; 255 | NSString *ssh_config = [sshDir stringByAppendingPathComponent:@"config"]; 256 | NSString *content = @"Host *\n UseKeychain yes"; 257 | 258 | // Create .ssh directory if it doesn't exist and set permissions 259 | if (![[NSFileManager defaultManager] fileExistsAtPath:sshDir]) { 260 | NSError *dirError = nil; 261 | [[NSFileManager defaultManager] createDirectoryAtPath:sshDir withIntermediateDirectories:YES attributes:@{NSFilePosixPermissions: @0700} error:&dirError]; 262 | if (dirError) { 263 | [[NSAlert alertWithError:dirError] runModal]; 264 | [sender setState:NSControlStateValueOff]; 265 | return; 266 | } 267 | } 268 | 269 | if (sender.state == NSControlStateValueOn) { 270 | NSError *error = nil; 271 | NSString *existingContent = @""; 272 | BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:ssh_config]; 273 | 274 | if (exists) { 275 | existingContent = [NSString stringWithContentsOfFile:ssh_config encoding:NSUTF8StringEncoding error:&error]; 276 | if (error) { 277 | [[NSAlert alertWithError:error] runModal]; 278 | [sender setState:NSControlStateValueOff]; 279 | return; 280 | } 281 | existingContent = [existingContent stringByAppendingString:@"\n"]; 282 | } 283 | 284 | NSString *newContent = [existingContent stringByAppendingString:[NSString stringWithFormat:@"%@\n", content]]; 285 | NSError *writeError = nil; 286 | BOOL success = [newContent writeToFile:ssh_config atomically:YES encoding:NSUTF8StringEncoding error:&writeError]; 287 | 288 | if (!success || writeError) { 289 | [[NSAlert alertWithError:writeError] runModal]; 290 | [sender setState:NSControlStateValueOff]; 291 | return; 292 | } 293 | 294 | // Check and set proper file permissions (600) if needed 295 | NSError *attributesError = nil; 296 | NSDictionary *currentAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:ssh_config error:&attributesError]; 297 | if (attributesError) { 298 | [[NSAlert alertWithError:attributesError] runModal]; 299 | } else { 300 | NSNumber *currentPermissions = [currentAttributes objectForKey:NSFilePosixPermissions]; 301 | if (![currentPermissions isEqualToNumber:@0600]) { 302 | NSError *chmodError = nil; 303 | NSDictionary *attributes = @{NSFilePosixPermissions: @0600}; 304 | [[NSFileManager defaultManager] setAttributes:attributes ofItemAtPath:ssh_config error:&chmodError]; 305 | if (chmodError) { 306 | [[NSAlert alertWithError:chmodError] runModal]; 307 | // Don't revert the switch state since the file was written successfully 308 | } 309 | } 310 | } 311 | } else { 312 | NSError *error = nil; 313 | NSString *fileContent = [NSString stringWithContentsOfFile:ssh_config encoding:NSUTF8StringEncoding error:&error]; 314 | 315 | if (error) { 316 | [[NSAlert alertWithError:error] runModal]; 317 | [sender setState:NSControlStateValueOn]; 318 | return; 319 | } 320 | 321 | NSMutableArray *lines = [[fileContent componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]] mutableCopy]; 322 | NSMutableArray *newLines = [NSMutableArray array]; 323 | BOOL skipNextLine = NO; 324 | 325 | for (NSString *line in lines) { 326 | if (skipNextLine) { 327 | skipNextLine = NO; 328 | continue; 329 | } 330 | if ([line isEqualToString:@"Host *"]) { 331 | NSUInteger index = [lines indexOfObject:line]; 332 | if (index + 1 < lines.count && [lines[index + 1] containsString:@"UseKeychain yes"]) { 333 | skipNextLine = YES; 334 | continue; 335 | } 336 | } 337 | [newLines addObject:line]; 338 | } 339 | 340 | NSString *updatedContent = [[newLines componentsJoinedByString:@"\n"] stringByAppendingString:@"\n"]; 341 | NSError *writeError = nil; 342 | BOOL success = [updatedContent writeToFile:ssh_config atomically:YES encoding:NSUTF8StringEncoding error:&writeError]; 343 | 344 | if (!success || writeError) { 345 | [[NSAlert alertWithError:writeError] runModal]; 346 | [sender setState:NSControlStateValueOn]; 347 | return; 348 | } 349 | } 350 | } 351 | 352 | @end 353 | 354 | 355 | @implementation teaBASE (NSTextFieldDelegate) 356 | 357 | - (void)controlTextDidChange:(NSNotification *)obj { 358 | if ([obj.object isEqual:self.sshRemovePassphraseTextField]) { 359 | self.sshRemovePassphraseButton.enabled = self.sshRemovePassphraseTextField.stringValue.length > 0; 360 | } else { 361 | self.sshApplyPassphraseButton.enabled = self.sshPassphraseTextField.stringValue.length > 0; 362 | } 363 | } 364 | 365 | @end 366 | -------------------------------------------------------------------------------- /teaBASE.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 77; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 63053F3C2D14AA8500E39578 /* make-clean-install-pack.sh in Copy Scripts to Bundle */ = {isa = PBXBuildFile; fileRef = 63053F3A2D14AA7500E39578 /* make-clean-install-pack.sh */; }; 11 | 634F60232CFE1F40006DF7B7 /* dotfile-sync.sh in Copy Scripts to Bundle */ = {isa = PBXBuildFile; fileRef = 634F60222CFE1F40006DF7B7 /* dotfile-sync.sh */; }; 12 | 6367D7312CDD28E300250E84 /* github-integration.sh in Copy Scripts to Bundle */ = {isa = PBXBuildFile; fileRef = 6367D72E2CDD28CA00250E84 /* github-integration.sh */; }; 13 | 63AA5EC82CEF5C6300BC8696 /* usr-local-install.sh in Copy Scripts to Bundle */ = {isa = PBXBuildFile; fileRef = 63AA5EC62CEF59DD00BC8696 /* usr-local-install.sh */; }; 14 | 63AE66812CF618DC00EB278B /* docker-version.sh in Copy Scripts to Bundle */ = {isa = PBXBuildFile; fileRef = 63AE667F2CF6173D00EB278B /* docker-version.sh */; }; 15 | 63FCE92E2D061CE1000DA22F /* install-docker.sh in Copy Scripts to Bundle */ = {isa = PBXBuildFile; fileRef = 63FCE92C2D061C15000DA22F /* install-docker.sh */; }; 16 | 63FF33402D07CDFF0021E1E2 /* self-update.sh in Copy Scripts to Bundle */ = {isa = PBXBuildFile; fileRef = 632E38D82D01C60C00B7A044 /* self-update.sh */; }; 17 | /* End PBXBuildFile section */ 18 | 19 | /* Begin PBXCopyFilesBuildPhase section */ 20 | 6367D72D2CDD286D00250E84 /* Copy Scripts to Bundle */ = { 21 | isa = PBXCopyFilesBuildPhase; 22 | buildActionMask = 2147483647; 23 | dstPath = ../Scripts; 24 | dstSubfolderSpec = 6; 25 | files = ( 26 | 63053F3C2D14AA8500E39578 /* make-clean-install-pack.sh in Copy Scripts to Bundle */, 27 | 63FF33402D07CDFF0021E1E2 /* self-update.sh in Copy Scripts to Bundle */, 28 | 634F60232CFE1F40006DF7B7 /* dotfile-sync.sh in Copy Scripts to Bundle */, 29 | 63FCE92E2D061CE1000DA22F /* install-docker.sh in Copy Scripts to Bundle */, 30 | 63AE66812CF618DC00EB278B /* docker-version.sh in Copy Scripts to Bundle */, 31 | 63AA5EC82CEF5C6300BC8696 /* usr-local-install.sh in Copy Scripts to Bundle */, 32 | 6367D7312CDD28E300250E84 /* github-integration.sh in Copy Scripts to Bundle */, 33 | ); 34 | name = "Copy Scripts to Bundle"; 35 | runOnlyForDeploymentPostprocessing = 0; 36 | }; 37 | /* End PBXCopyFilesBuildPhase section */ 38 | 39 | /* Begin PBXFileReference section */ 40 | 63053F3A2D14AA7500E39578 /* make-clean-install-pack.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = "make-clean-install-pack.sh"; sourceTree = ""; }; 41 | 632219852CB4173D00606A25 /* teaBASE.prefPane */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = teaBASE.prefPane; sourceTree = BUILT_PRODUCTS_DIR; }; 42 | 632895F92CD27FA80022D63D /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 43 | 632E38D82D01C60C00B7A044 /* self-update.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = "self-update.sh"; sourceTree = ""; }; 44 | 634F60222CFE1F40006DF7B7 /* dotfile-sync.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = "dotfile-sync.sh"; sourceTree = ""; }; 45 | 6367D72E2CDD28CA00250E84 /* github-integration.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = "github-integration.sh"; sourceTree = ""; }; 46 | 63AA5EC62CEF59DD00BC8696 /* usr-local-install.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = "usr-local-install.sh"; sourceTree = ""; }; 47 | 63AE667F2CF6173D00EB278B /* docker-version.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = "docker-version.sh"; sourceTree = ""; }; 48 | 63FCE92C2D061C15000DA22F /* install-docker.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = "install-docker.sh"; sourceTree = ""; }; 49 | /* End PBXFileReference section */ 50 | 51 | /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ 52 | 632E38D72D012FEA00B7A044 /* Exceptions for "Sundries" folder in "teaBASE" target */ = { 53 | isa = PBXFileSystemSynchronizedBuildFileExceptionSet; 54 | membershipExceptions = ( 55 | Info.plist, 56 | ); 57 | target = 632219842CB4173D00606A25 /* teaBASE */; 58 | }; 59 | 632E390C2D033B4600B7A044 /* Exceptions for "Resources" folder in "teaBASE" target */ = { 60 | isa = PBXFileSystemSynchronizedBuildFileExceptionSet; 61 | membershipExceptions = ( 62 | "dmg-bg@2x.png", 63 | ); 64 | target = 632219842CB4173D00606A25 /* teaBASE */; 65 | }; 66 | /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ 67 | 68 | /* Begin PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet section */ 69 | 63B2126F2D11ED9D00F5E466 /* Exceptions for "Sources" folder in "Headers" phase from "teaBASE" target */ = { 70 | isa = PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet; 71 | buildPhase = 632219802CB4173D00606A25 /* Headers */; 72 | membershipExceptions = ( 73 | "teaBASE+DevTools.m", 74 | ); 75 | }; 76 | /* End PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet section */ 77 | 78 | /* Begin PBXFileSystemSynchronizedRootGroup section */ 79 | 632219872CB4173D00606A25 /* Sources */ = { 80 | isa = PBXFileSystemSynchronizedRootGroup; 81 | exceptions = ( 82 | 63B2126F2D11ED9D00F5E466 /* Exceptions for "Sources" folder in "Headers" phase from "teaBASE" target */, 83 | ); 84 | path = Sources; 85 | sourceTree = ""; 86 | }; 87 | 632E38D62D012E4D00B7A044 /* Sundries */ = { 88 | isa = PBXFileSystemSynchronizedRootGroup; 89 | exceptions = ( 90 | 632E38D72D012FEA00B7A044 /* Exceptions for "Sundries" folder in "teaBASE" target */, 91 | ); 92 | path = Sundries; 93 | sourceTree = ""; 94 | }; 95 | 632E39042D033B3800B7A044 /* Resources */ = { 96 | isa = PBXFileSystemSynchronizedRootGroup; 97 | exceptions = ( 98 | 632E390C2D033B4600B7A044 /* Exceptions for "Resources" folder in "teaBASE" target */, 99 | ); 100 | path = Resources; 101 | sourceTree = ""; 102 | }; 103 | /* End PBXFileSystemSynchronizedRootGroup section */ 104 | 105 | /* Begin PBXFrameworksBuildPhase section */ 106 | 632219822CB4173D00606A25 /* Frameworks */ = { 107 | isa = PBXFrameworksBuildPhase; 108 | buildActionMask = 2147483647; 109 | files = ( 110 | ); 111 | runOnlyForDeploymentPostprocessing = 0; 112 | }; 113 | /* End PBXFrameworksBuildPhase section */ 114 | 115 | /* Begin PBXGroup section */ 116 | 6322197B2CB4173D00606A25 = { 117 | isa = PBXGroup; 118 | children = ( 119 | 632219872CB4173D00606A25 /* Sources */, 120 | 6367D72F2CDD28CA00250E84 /* Scripts */, 121 | 632E39042D033B3800B7A044 /* Resources */, 122 | 632E38D62D012E4D00B7A044 /* Sundries */, 123 | 632895F92CD27FA80022D63D /* README.md */, 124 | 632219862CB4173D00606A25 /* Products */, 125 | ); 126 | sourceTree = ""; 127 | }; 128 | 632219862CB4173D00606A25 /* Products */ = { 129 | isa = PBXGroup; 130 | children = ( 131 | 632219852CB4173D00606A25 /* teaBASE.prefPane */, 132 | ); 133 | name = Products; 134 | sourceTree = ""; 135 | }; 136 | 6367D72F2CDD28CA00250E84 /* Scripts */ = { 137 | isa = PBXGroup; 138 | children = ( 139 | 63053F3A2D14AA7500E39578 /* make-clean-install-pack.sh */, 140 | 63FCE92C2D061C15000DA22F /* install-docker.sh */, 141 | 632E38D82D01C60C00B7A044 /* self-update.sh */, 142 | 63AE667F2CF6173D00EB278B /* docker-version.sh */, 143 | 63AA5EC62CEF59DD00BC8696 /* usr-local-install.sh */, 144 | 6367D72E2CDD28CA00250E84 /* github-integration.sh */, 145 | 634F60222CFE1F40006DF7B7 /* dotfile-sync.sh */, 146 | ); 147 | path = Scripts; 148 | sourceTree = ""; 149 | }; 150 | /* End PBXGroup section */ 151 | 152 | /* Begin PBXHeadersBuildPhase section */ 153 | 632219802CB4173D00606A25 /* Headers */ = { 154 | isa = PBXHeadersBuildPhase; 155 | buildActionMask = 2147483647; 156 | files = ( 157 | ); 158 | runOnlyForDeploymentPostprocessing = 0; 159 | }; 160 | /* End PBXHeadersBuildPhase section */ 161 | 162 | /* Begin PBXNativeTarget section */ 163 | 632219842CB4173D00606A25 /* teaBASE */ = { 164 | isa = PBXNativeTarget; 165 | buildConfigurationList = 632219932CB4173D00606A25 /* Build configuration list for PBXNativeTarget "teaBASE" */; 166 | buildPhases = ( 167 | 632219802CB4173D00606A25 /* Headers */, 168 | 632219812CB4173D00606A25 /* Sources */, 169 | 632219822CB4173D00606A25 /* Frameworks */, 170 | 632219832CB4173D00606A25 /* Resources */, 171 | 63B188092CBD6AE30081E570 /* Download pkgx */, 172 | 63AE66822CF754D200EB278B /* Build bpb */, 173 | 6347BBC62CBD6ECA00DA6544 /* Copy Binaries to Bundle */, 174 | 6367D72D2CDD286D00250E84 /* Copy Scripts to Bundle */, 175 | 63A57FAE2D1F672200AB393B /* Bundle `cd to.app` */, 176 | ); 177 | buildRules = ( 178 | ); 179 | dependencies = ( 180 | ); 181 | fileSystemSynchronizedGroups = ( 182 | 632219872CB4173D00606A25 /* Sources */, 183 | 632E38D62D012E4D00B7A044 /* Sundries */, 184 | 632E39042D033B3800B7A044 /* Resources */, 185 | ); 186 | name = teaBASE; 187 | packageProductDependencies = ( 188 | ); 189 | productName = teaBASE; 190 | productReference = 632219852CB4173D00606A25 /* teaBASE.prefPane */; 191 | productType = "com.apple.product-type.bundle"; 192 | }; 193 | /* End PBXNativeTarget section */ 194 | 195 | /* Begin PBXProject section */ 196 | 6322197C2CB4173D00606A25 /* Project object */ = { 197 | isa = PBXProject; 198 | attributes = { 199 | BuildIndependentTargetsInParallel = 1; 200 | LastSwiftUpdateCheck = 1610; 201 | LastUpgradeCheck = 1610; 202 | TargetAttributes = { 203 | 632219842CB4173D00606A25 = { 204 | CreatedOnToolsVersion = 16.0; 205 | LastSwiftMigration = 1610; 206 | }; 207 | }; 208 | }; 209 | buildConfigurationList = 6322197F2CB4173D00606A25 /* Build configuration list for PBXProject "teaBASE" */; 210 | developmentRegion = en; 211 | hasScannedForEncodings = 0; 212 | knownRegions = ( 213 | en, 214 | Base, 215 | ); 216 | mainGroup = 6322197B2CB4173D00606A25; 217 | minimizedProjectReferenceProxies = 1; 218 | preferredProjectObjectVersion = 77; 219 | productRefGroup = 632219862CB4173D00606A25 /* Products */; 220 | projectDirPath = ""; 221 | projectRoot = ""; 222 | targets = ( 223 | 632219842CB4173D00606A25 /* teaBASE */, 224 | ); 225 | }; 226 | /* End PBXProject section */ 227 | 228 | /* Begin PBXResourcesBuildPhase section */ 229 | 632219832CB4173D00606A25 /* Resources */ = { 230 | isa = PBXResourcesBuildPhase; 231 | buildActionMask = 0; 232 | files = ( 233 | ); 234 | runOnlyForDeploymentPostprocessing = 0; 235 | }; 236 | /* End PBXResourcesBuildPhase section */ 237 | 238 | /* Begin PBXShellScriptBuildPhase section */ 239 | 6347BBC62CBD6ECA00DA6544 /* Copy Binaries to Bundle */ = { 240 | isa = PBXShellScriptBuildPhase; 241 | buildActionMask = 2147483647; 242 | files = ( 243 | ); 244 | inputFileListPaths = ( 245 | ); 246 | inputPaths = ( 247 | "$(DERIVED_FILE_DIR)/pkgx", 248 | "$(DERIVED_FILE_DIR)/bpb", 249 | ); 250 | name = "Copy Binaries to Bundle"; 251 | outputFileListPaths = ( 252 | ); 253 | outputPaths = ( 254 | "$(TARGET_BUILD_DIR)/$(EXECUTABLE_FOLDER_PATH)/bpb", 255 | "$(TARGET_BUILD_DIR)/$(EXECUTABLE_FOLDER_PATH)/pkgx", 256 | ); 257 | runOnlyForDeploymentPostprocessing = 0; 258 | shellPath = /bin/bash; 259 | shellScript = "set -exo pipefail\n\ncd \"${DERIVED_FILE_DIR}\"\n\ncp pkgx \"${TARGET_BUILD_DIR}/${EXECUTABLE_FOLDER_PATH}\"\ncp bpb/target/$CONFIGURATION/bpb \"${TARGET_BUILD_DIR}/${EXECUTABLE_FOLDER_PATH}\"\n"; 260 | showEnvVarsInLog = 0; 261 | }; 262 | 63A57FAE2D1F672200AB393B /* Bundle `cd to.app` */ = { 263 | isa = PBXShellScriptBuildPhase; 264 | buildActionMask = 2147483647; 265 | files = ( 266 | ); 267 | inputFileListPaths = ( 268 | ); 269 | inputPaths = ( 270 | ); 271 | name = "Bundle `cd to.app`"; 272 | outputFileListPaths = ( 273 | ); 274 | outputPaths = ( 275 | "$(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/cd to.app", 276 | ); 277 | runOnlyForDeploymentPostprocessing = 0; 278 | shellPath = /bin/sh; 279 | shellScript = "cd \"$DERIVED_FILE_DIR\"\n\nif ! test -d cdto; then\n git clone https://github.com/pkgxdev/cdto\n cd cdto\nelse\n cd cdto\n git fetch -pft\n git reset --hard origin/master\nfi\n\nif test $CONFIGURATION = Release; then\n xcodebuild \\\n -scheme \"cd to\" \\\n -configuration $CONFIGURATION \\\n -derivedDataPath ./build \\\n -destination \"generic/platform=macOS\" \\\n ARCHS=\"x86_64 arm64\" \\\n EXCLUDED_ARCHS=\"\" \\\n build\nelse\n xcodebuild \\\n -scheme \"cd to\" \\\n -configuration $CONFIGURATION \\\n -derivedDataPath ./build\nfi\n\nditto \\\n build/Build/Products/$CONFIGURATION/\"cd to.app\" \\\n $TARGET_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH/\"cd to.app\"\n"; 280 | showEnvVarsInLog = 0; 281 | }; 282 | 63AE66822CF754D200EB278B /* Build bpb */ = { 283 | isa = PBXShellScriptBuildPhase; 284 | buildActionMask = 2147483647; 285 | files = ( 286 | ); 287 | inputFileListPaths = ( 288 | ); 289 | inputPaths = ( 290 | "$(DERIVED_FILE_DIR)/pkgx", 291 | ); 292 | name = "Build bpb"; 293 | outputFileListPaths = ( 294 | ); 295 | outputPaths = ( 296 | "$(DERIVED_FILE_DIR)/bpb/target/release/bpb", 297 | ); 298 | runOnlyForDeploymentPostprocessing = 0; 299 | shellPath = /bin/sh; 300 | shellScript = "cd \"$DERIVED_FILE_DIR\"\n\nif ! test -d bpb/.git; then\n rm -rf bpb # Xcode tries to be helpful by creating the intermediary directories for us but this breaks git clone\n git clone https://github.com/pkgxdev/bpb\n cd bpb\nelse\n cd bpb\n git fetch -pft\n git reset --hard origin/main\nfi\n\nif test \"$CONFIGURATION\" = \"Release\"; then\n ARG=--release\nfi\n\n\"${DERIVED_FILE_DIR}\"/pkgx cargo build $ARG\n"; 301 | showEnvVarsInLog = 0; 302 | }; 303 | 63B188092CBD6AE30081E570 /* Download pkgx */ = { 304 | isa = PBXShellScriptBuildPhase; 305 | buildActionMask = 2147483647; 306 | files = ( 307 | ); 308 | inputFileListPaths = ( 309 | ); 310 | inputPaths = ( 311 | ); 312 | name = "Download pkgx"; 313 | outputFileListPaths = ( 314 | ); 315 | outputPaths = ( 316 | "$(DERIVED_FILE_DIR)/pkgx", 317 | ); 318 | runOnlyForDeploymentPostprocessing = 0; 319 | shellPath = /bin/sh; 320 | shellScript = "curl -o \"${DERIVED_FILE_DIR}\"/pkgx --compressed -f --proto '=https' https://pkgx.sh/$(uname)/$(uname -m)\nchmod +x \"${DERIVED_FILE_DIR}\"/pkgx\n"; 321 | showEnvVarsInLog = 0; 322 | }; 323 | /* End PBXShellScriptBuildPhase section */ 324 | 325 | /* Begin PBXSourcesBuildPhase section */ 326 | 632219812CB4173D00606A25 /* Sources */ = { 327 | isa = PBXSourcesBuildPhase; 328 | buildActionMask = 2147483647; 329 | files = ( 330 | ); 331 | runOnlyForDeploymentPostprocessing = 0; 332 | }; 333 | /* End PBXSourcesBuildPhase section */ 334 | 335 | /* Begin XCBuildConfiguration section */ 336 | 632219942CB4173D00606A25 /* Debug */ = { 337 | isa = XCBuildConfiguration; 338 | buildSettings = { 339 | CLANG_ENABLE_MODULES = YES; 340 | CODE_SIGN_ENTITLEMENTS = Sundries/teaBASE.entitlements; 341 | CODE_SIGN_IDENTITY = "Apple Development"; 342 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; 343 | CODE_SIGN_STYLE = Automatic; 344 | COMBINE_HIDPI_IMAGES = YES; 345 | DEAD_CODE_STRIPPING = YES; 346 | DEVELOPMENT_TEAM = 7WV56FL599; 347 | ENABLE_USER_SCRIPT_SANDBOXING = NO; 348 | GENERATE_INFOPLIST_FILE = YES; 349 | INFOPLIST_FILE = Sundries/Info.plist; 350 | INFOPLIST_KEY_NSMainNibFile = teaBASE; 351 | INFOPLIST_KEY_NSPrincipalClass = teaBASE; 352 | INSTALL_PATH = "$(HOME)/Library/PreferencePanes"; 353 | MACOSX_DEPLOYMENT_TARGET = 13.5; 354 | PRODUCT_BUNDLE_IDENTIFIER = xyz.tea.BASE.pane; 355 | PRODUCT_MODULE_NAME = teaBASE; 356 | PRODUCT_NAME = teaBASE; 357 | PROVISIONING_PROFILE_SPECIFIER = ""; 358 | SWIFT_EMIT_LOC_STRINGS = YES; 359 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 360 | SWIFT_VERSION = 6.0; 361 | WRAPPER_EXTENSION = prefPane; 362 | }; 363 | name = Debug; 364 | }; 365 | 632219952CB4173D00606A25 /* Release */ = { 366 | isa = XCBuildConfiguration; 367 | buildSettings = { 368 | CLANG_ENABLE_MODULES = YES; 369 | CODE_SIGN_ENTITLEMENTS = Sundries/teaBASE.entitlements; 370 | CODE_SIGN_IDENTITY = "Apple Distribution"; 371 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; 372 | CODE_SIGN_STYLE = Automatic; 373 | COMBINE_HIDPI_IMAGES = YES; 374 | DEAD_CODE_STRIPPING = YES; 375 | DEPLOYMENT_POSTPROCESSING = YES; 376 | DEVELOPMENT_TEAM = 7WV56FL599; 377 | ENABLE_USER_SCRIPT_SANDBOXING = NO; 378 | GENERATE_INFOPLIST_FILE = YES; 379 | INFOPLIST_FILE = Sundries/Info.plist; 380 | INFOPLIST_KEY_NSMainNibFile = teaBASE; 381 | INFOPLIST_KEY_NSPrincipalClass = teaBASE; 382 | INSTALL_PATH = "$(HOME)/Library/PreferencePanes"; 383 | MACOSX_DEPLOYMENT_TARGET = 13.5; 384 | PRODUCT_BUNDLE_IDENTIFIER = xyz.tea.BASE.pane; 385 | PRODUCT_MODULE_NAME = teaBASE; 386 | PRODUCT_NAME = teaBASE; 387 | PROVISIONING_PROFILE_SPECIFIER = ""; 388 | SWIFT_EMIT_LOC_STRINGS = YES; 389 | SWIFT_VERSION = 6.0; 390 | WRAPPER_EXTENSION = prefPane; 391 | }; 392 | name = Release; 393 | }; 394 | 632219962CB4173D00606A25 /* Debug */ = { 395 | isa = XCBuildConfiguration; 396 | buildSettings = { 397 | ALWAYS_SEARCH_USER_PATHS = NO; 398 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 399 | CLANG_ANALYZER_NONNULL = YES; 400 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 401 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 402 | CLANG_ENABLE_MODULES = YES; 403 | CLANG_ENABLE_OBJC_ARC = YES; 404 | CLANG_ENABLE_OBJC_WEAK = YES; 405 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 406 | CLANG_WARN_BOOL_CONVERSION = YES; 407 | CLANG_WARN_COMMA = YES; 408 | CLANG_WARN_CONSTANT_CONVERSION = YES; 409 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 410 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 411 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 412 | CLANG_WARN_EMPTY_BODY = YES; 413 | CLANG_WARN_ENUM_CONVERSION = YES; 414 | CLANG_WARN_INFINITE_RECURSION = YES; 415 | CLANG_WARN_INT_CONVERSION = YES; 416 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 417 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 418 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 419 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 420 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 421 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 422 | CLANG_WARN_STRICT_PROTOTYPES = YES; 423 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 424 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 425 | CLANG_WARN_UNREACHABLE_CODE = YES; 426 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 427 | COPY_PHASE_STRIP = NO; 428 | DEAD_CODE_STRIPPING = YES; 429 | DEBUG_INFORMATION_FORMAT = dwarf; 430 | ENABLE_STRICT_OBJC_MSGSEND = YES; 431 | ENABLE_TESTABILITY = YES; 432 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 433 | GCC_C_LANGUAGE_STANDARD = gnu17; 434 | GCC_DYNAMIC_NO_PIC = NO; 435 | GCC_NO_COMMON_BLOCKS = YES; 436 | GCC_OPTIMIZATION_LEVEL = 0; 437 | GCC_PREPROCESSOR_DEFINITIONS = ( 438 | "DEBUG=1", 439 | "$(inherited)", 440 | ); 441 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 442 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 443 | GCC_WARN_UNDECLARED_SELECTOR = YES; 444 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 445 | GCC_WARN_UNUSED_FUNCTION = YES; 446 | GCC_WARN_UNUSED_VARIABLE = YES; 447 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 448 | MACOSX_DEPLOYMENT_TARGET = 15.0; 449 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 450 | MTL_FAST_MATH = YES; 451 | ONLY_ACTIVE_ARCH = YES; 452 | SDKROOT = macosx; 453 | }; 454 | name = Debug; 455 | }; 456 | 632219972CB4173D00606A25 /* Release */ = { 457 | isa = XCBuildConfiguration; 458 | buildSettings = { 459 | ALWAYS_SEARCH_USER_PATHS = NO; 460 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 461 | CLANG_ANALYZER_NONNULL = YES; 462 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 463 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 464 | CLANG_ENABLE_MODULES = YES; 465 | CLANG_ENABLE_OBJC_ARC = YES; 466 | CLANG_ENABLE_OBJC_WEAK = YES; 467 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 468 | CLANG_WARN_BOOL_CONVERSION = YES; 469 | CLANG_WARN_COMMA = YES; 470 | CLANG_WARN_CONSTANT_CONVERSION = YES; 471 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 472 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 473 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 474 | CLANG_WARN_EMPTY_BODY = YES; 475 | CLANG_WARN_ENUM_CONVERSION = YES; 476 | CLANG_WARN_INFINITE_RECURSION = YES; 477 | CLANG_WARN_INT_CONVERSION = YES; 478 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 479 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 480 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 481 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 482 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 483 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 484 | CLANG_WARN_STRICT_PROTOTYPES = YES; 485 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 486 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 487 | CLANG_WARN_UNREACHABLE_CODE = YES; 488 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 489 | COPY_PHASE_STRIP = NO; 490 | DEAD_CODE_STRIPPING = YES; 491 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 492 | ENABLE_NS_ASSERTIONS = NO; 493 | ENABLE_STRICT_OBJC_MSGSEND = YES; 494 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 495 | GCC_C_LANGUAGE_STANDARD = gnu17; 496 | GCC_NO_COMMON_BLOCKS = YES; 497 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 498 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 499 | GCC_WARN_UNDECLARED_SELECTOR = YES; 500 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 501 | GCC_WARN_UNUSED_FUNCTION = YES; 502 | GCC_WARN_UNUSED_VARIABLE = YES; 503 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 504 | MACOSX_DEPLOYMENT_TARGET = 15.0; 505 | MTL_ENABLE_DEBUG_INFO = NO; 506 | MTL_FAST_MATH = YES; 507 | SDKROOT = macosx; 508 | SWIFT_COMPILATION_MODE = wholemodule; 509 | }; 510 | name = Release; 511 | }; 512 | /* End XCBuildConfiguration section */ 513 | 514 | /* Begin XCConfigurationList section */ 515 | 6322197F2CB4173D00606A25 /* Build configuration list for PBXProject "teaBASE" */ = { 516 | isa = XCConfigurationList; 517 | buildConfigurations = ( 518 | 632219962CB4173D00606A25 /* Debug */, 519 | 632219972CB4173D00606A25 /* Release */, 520 | ); 521 | defaultConfigurationIsVisible = 0; 522 | defaultConfigurationName = Release; 523 | }; 524 | 632219932CB4173D00606A25 /* Build configuration list for PBXNativeTarget "teaBASE" */ = { 525 | isa = XCConfigurationList; 526 | buildConfigurations = ( 527 | 632219942CB4173D00606A25 /* Debug */, 528 | 632219952CB4173D00606A25 /* Release */, 529 | ); 530 | defaultConfigurationIsVisible = 0; 531 | defaultConfigurationName = Release; 532 | }; 533 | /* End XCConfigurationList section */ 534 | }; 535 | rootObject = 6322197C2CB4173D00606A25 /* Project object */; 536 | } 537 | --------------------------------------------------------------------------------