Surface unmanaged .app bundles during sync as Markdown report

`ssr sync` now scans /Applications (and ~/Applications, configurable via
SSR_APP_DIRS) for top-level .app bundles that are neither Mac App Store
installs (Contents/_MASReceipt/receipt) nor Homebrew Cask installs, and
writes <backup_dir>/unmanaged-apps.md alongside the Brewfile. This closes
a long-standing gap in the restore story: such apps need manual reinstall
on a fresh machine, and were previously invisible to the sync workflow.

Cask detection runs fully offline against the local Caskroom and unions
four complementary strategies:

  1. Caskroom walk for .app symlinks/bundles under
     $(brew --caskroom)/<cask>/<version>/.
  2. `app "Foo.app"` declarations parsed from .rb formula receipts.
  3. `name "Foo"` field from .rb receipts (handles pkg-based casks like
     Microsoft Edge, Teams, Zoom, Google Drive, etc., that never drop
     a .app under the caskroom).
  4. `"name":["Foo"]` field from modern .json receipts (multi-line aware).

For each unmanaged app the report includes version and bundle id from the
Info.plist plus a best-effort installer link, in preference order:

  1. Sparkle SUFeedURL → derive the vendor homepage from the URL host.
  2. Bundle-id prefix match against a curated vendor map (Adobe,
     Microsoft, Docker, Cursor, Anthropic, OpenAI, JetBrains,
     GitKraken, Signal, Zoom, Telegram, Slack, Synology, Jabra, ...).
  3. DuckDuckGo search fallback (`<App name> macOS download`).

Toggles in ssr.conf.example: SSR_SCAN_UNMANAGED_APPS=true (default),
SSR_APP_DIRS=/Applications:~/Applications.

ssr-status gains a one-line summary referencing the report file.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-11 18:04:30 +02:00
parent a4465f37c6
commit d681792148
5 changed files with 351 additions and 1 deletions
+18 -1
View File
@@ -70,11 +70,25 @@ Output structure in the cloud drive:
├── BACKUP/
│ └── <hostname> - <hardware-uuid>/
│ ├── Brewfile
── Brewfile.bak
── Brewfile.bak
│ └── unmanaged-apps.md # apps installed manually (not brew/MAS)
└── Mackup/
└── … (managed by mackup)
```
During each sync the unmanaged-apps scan compares every `.app` bundle in `/Applications` (and `~/Applications`) against:
1. **Mac App Store** — bundle contains `Contents/_MASReceipt/receipt`
2. **Homebrew Cask** — bundle filename matches the `.app` artifact of any installed cask
Anything not matched is written to `unmanaged-apps.md` (Markdown table, one row per app, with version, bundle id and a best-effort installer link) and printed to the sync log as a warning. Disable with `SSR_SCAN_UNMANAGED_APPS=false`.
Link selection for each app uses, in order:
1. **Sparkle feed**`SUFeedURL` from the bundle's `Info.plist` → linked vendor homepage.
2. **Known vendor map** — bundle-id prefix maps to the official download page (Adobe, Microsoft, Docker, Cursor, Anthropic, JetBrains, GitKraken, Signal, Zoom, etc.).
3. **Web search** — DuckDuckGo query for `<App name> macOS download`.
### Restore on a new machine
```bash
@@ -103,6 +117,8 @@ Config lives in `~/.config/ssr/ssr.conf` (override with `--config` or `$SSR_CONF
| `SSR_MACKUP_SUBDIR` | `Mackup` | Subfolder used by mackup |
| `SSR_BREW_UPGRADE` | `true` | Run `brew upgrade` during sync |
| `SSR_BREW_CLEANUP` | `true` | Run `brew cleanup` after sync |
| `SSR_SCAN_UNMANAGED_APPS` | `true` | List `.app` bundles not from brew/MAS during sync |
| `SSR_APP_DIRS` | `/Applications:~/Applications` | Colon-separated dirs scanned for unmanaged apps |
| `SSR_DISABLE_GATEKEEPER` | `false` | Run `spctl --global-disable` during restore (security trade-off) |
| `SSR_OPEN_CASKS` | `false` | Open every installed cask after restore for first-run config |
| `SSR_MACOS_DEFAULTS` | repo's `config/macos-defaults.sh` | Override path to your own defaults script |
@@ -217,6 +233,7 @@ ssr update --dry-run # show what would change, don't pull
│ ├── cloud.sh # Provider abstraction
│ ├── brew.sh # Homebrew install + Brewfile ops
│ ├── mackup.sh # Mackup install + backup/restore
│ ├── apps.sh # Detect unmanaged .app bundles (not brew/MAS)
│ └── macos.sh # macOS-specific actions (Rosetta, defaults, etc.)
├── config/
│ ├── ssr.conf.example # Reference user config