Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions CONTRIBUTING-bundles.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,36 @@ A **bundle** is a curated one-click stack (packages) with post-install guidance,
This also regenerates `bundles.json`. **Commit `bundles.json` along with your recipe.**
4. **Open a PR.** The `Validate Recipes` CI check runs the same validator and posts a pass/fail table. Green = mergeable.

## Localizing bundle prose
Keep `recipes/*.json` and generated `bundles.json` as the stable source contract. Do **not** translate package tokens, formula/cask kinds, brew commands, URLs, ids, route/storage keys, model/provider/product names, or CLI flags there.

Locale-specific user-facing prose lives in overlay files keyed by stable recipe ids:

- Tauri/Rust: `src-tauri/data/bundles.<locale>.json`
- Native SwiftUI: `native/Sources/BrewBrowserKit/Resources/bundles.<locale>.json`

The overlay shape is:

```json
{
"locale": "ru",
"bundles": [{
"id": "local-llm",
"name": "…",
"tagline": "…",
"description": "…",
"capabilityNotes": { "16": "…" },
"setup": [{ "index": 0, "label": "…", "text": "…" }],
"caveats": "…",
"links": [{ "url": "https://docs.ollama.com", "label": "…" }]
}]
}
```

Overlay patches are deliberately narrow: recipe `id` selects the bundle, setup `index` selects a setup step, and link `url` selects a link label. Unknown or missing overlay fields fall back to the source `bundles.json`. Live bundle refresh applies a locale overlay only when the same live host serves `bundles.<locale>.json`; otherwise it keeps the live recipe prose untouched rather than mixing newer recipes with stale bundled translations.

When adding a new UI language, add the app UI messages in both shells (`src/lib/i18n/*` and native `Localizable.*`), add the bundle overlay resource to both builds, include the native resource in `native/Package.swift`, and run the normal Tauri/Rust/native checks.

## The rules CI enforces
- Schema-valid; `id` unique + kebab-case.
- Every package resolves via `brew info` to its declared `kind`; third-party taps declared.
Expand Down
1 change: 1 addition & 0 deletions bundles.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"kind": "cask"
}
],
"tap": "anomalyco/tap",
"requires": {
"minRamGB": 8,
"recommendedRamGB": 16,
Expand Down
6 changes: 3 additions & 3 deletions memory-bank/backendApi.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Every command is `async`. Every command returns `Result<T, BrewError>` (serializ
|---|---|---|---|---|---|
| `brew_search` | `async fn brew_search(query: String) -> Result<SearchResults, BrewError>` | Searches the Homebrew index for matching formulae and casks. | `brew search --formula <q>` and `brew search --cask <q>` (parallel within the command; plain stdout — no `--json` support) | no | shared-read |
| `brew_search_desc` | `async fn brew_search_desc(query: String) -> Result<SearchResults, BrewError>` | Searches by description text. Optional / "Phase 2.1" if time permits. | `brew search --desc <q>` | no | shared-read |
| `local_search` | `async fn local_search(query: String, locale: Option<String>, state) -> Result<SearchResults, BrewError>` | In-memory search across package token/name, upstream desc, bundled enrichment, category labels, tags, and locale overlay search terms. `locale` selects a partial `enrichment.<locale>.json.gz` overlay when present, while English fields remain searchable fallbacks. | none — scans bundled/user-refreshed catalog already in memory | no | no |

### Phase 3 — Install / uninstall / upgrade (streaming)

Expand Down Expand Up @@ -1194,8 +1195,8 @@ All six commands share the `authed_gate` helper in `commands/github.rs:152` whic

| Command | Signature | Purpose | Paranoid gate | Auth | Source |
|---|---|---|---|---|---|
| `enrichment_data` | `async fn enrichment_data(state) -> Result<Arc<EnrichmentData>, BrewError>` | Returns the full bundled enrichment payload. Memoised on `AppState.enrichment_cache` — subsequent calls return an Arc clone, not a re-parse. Zero runtime LLM calls; the bundle is `include_bytes!`d at compile time. | no (bundled) | none | `commands/enrichment.rs:23` |
| `enrichment_lookup` | `async fn enrichment_lookup(name: String, state) -> Result<Option<EnrichmentEntry>, BrewError>` | Per-token lookup. `validate_package_name` gates the input. Returns `None` when the token is missing (placeholder bundle, unmapped package). | no | none | `commands/enrichment.rs:43` |
| `enrichment_data` | `async fn enrichment_data(locale: Option<String>, state) -> Result<Arc<EnrichmentData>, BrewError>` | Returns the full bundled enrichment payload, optionally merged with a small locale overlay such as `enrichment.ru.json.gz`. Base English data is memoised on `AppState.enrichment_cache`; localized copies are memoised by locale in `enrichment_locale_cache`. Zero runtime LLM calls; bundles are `include_bytes!`d at compile time. | no (bundled) | none | `commands/enrichment.rs:23` |
| `enrichment_lookup` | `async fn enrichment_lookup(name: String, locale: Option<String>, state) -> Result<Option<EnrichmentEntry>, BrewError>` | Per-token lookup with the same locale overlay semantics as `enrichment_data`. `validate_package_name` gates the input. Returns `None` when the token is missing (placeholder bundle, unmapped package). | no | none | `commands/enrichment.rs:65` |

### 13.10 New error variants

Expand Down Expand Up @@ -1364,4 +1365,3 @@ Critical invariants pinned:
- Cache corrupt → empty cache returned, no panic
- GHSA enrichment soft-fails when `github_enabled` is off
- Install-set fingerprint is deterministic across runs (sorted input → stable hash)

13 changes: 12 additions & 1 deletion native/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import PackageDescription
// `BrewBrowser` executable is wrapped into a launchable .app by build-app.sh.
let package = Package(
name: "BrewBrowser",
defaultLocalization: "en",
platforms: [.macOS(.v26)],
products: [
// Exposing the library as a product makes SwiftPM/Xcode generate a
Expand All @@ -34,7 +35,12 @@ let package = Package(
.executableTarget(
name: "BrewBrowser",
dependencies: ["BrewBrowserKit"],
path: "Sources/BrewBrowser"
path: "Sources/BrewBrowser",
resources: [
.process("Resources/Localizable.xcstrings"),
.process("Resources/en.lproj"),
.process("Resources/ru.lproj")
]
),
// Library: all views, AppModel, and the brew/vulns/github/trending/
// enrichment services. Bundled JSON resources live here alongside the
Expand All @@ -48,13 +54,18 @@ let package = Package(
],
path: "Sources/BrewBrowserKit",
resources: [
.process("Resources/Localizable.xcstrings"),
.process("Resources/en.lproj"),
.process("Resources/ru.lproj"),
.copy("Resources/categories.json"),
.copy("Resources/enrichment.json"),
.copy("Resources/enrichment.ru.json"),
// Curated Bundles catalog (M2) — the six first-party recipes,
// generated from recipes/*.json by scripts/validate-recipes.mjs.
// Same artifact the Tauri app bundles; parsed at launch by
// BundleCatalog (Bundles.swift) via Bundle.module, tolerant/lossy.
.copy("Resources/bundles.json"),
.copy("Resources/bundles.ru.json"),
// GitHub Octocat mark (vector PDF, Primer/Octicons MIT). Rendered
// as a template image in the toolbar's "connected" chip.
.copy("Resources/github-mark.pdf"),
Expand Down
30 changes: 15 additions & 15 deletions native/Sources/BrewBrowser/BrewBrowserApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ struct AppCommands: Commands {
// mirrors the Tauri AboutModal. A "Sponsor brew-browser…" item sits below
// so the donate CTA also lives in the app menu.
CommandGroup(replacing: .appInfo) {
Button("About Brew Browser") { model.aboutOpen = true }
Button("Sponsor Brew Browser…") {
Button(L10n.string("about.title")) { model.aboutOpen = true }
Button(L10n.string("chrome.donate.menu")) {
if let url = URL(string: AboutInfo.sponsorURL) {
NSWorkspace.shared.open(url)
}
Expand All @@ -79,52 +79,52 @@ struct AppCommands: Commands {
// Sparkle's documented SwiftUI integration. Mirrors the Tauri "Check now"
// (`SettingsSectionUpdates.svelte`).
CommandGroup(after: .appInfo) {
Button("Check for Updates…") { updater.checkForUpdates() }
Button(L10n.string("updates.checkForUpdates")) { updater.checkForUpdates() }
.disabled(!updater.canCheckForUpdates)
}

// A dedicated "Go" menu hosts the section-nav shortcuts (⌘0–6) so they
// appear together with their key equivalents, like a stock View menu.
CommandMenu("Go") {
Button("Dashboard") { model.go(toSectionNumber: 0) }
CommandMenu(L10n.string("menu.go")) {
Button(L10n.string("nav.dashboard")) { model.go(toSectionNumber: 0) }
.keyboardShortcut("0", modifiers: .command)
Button("Library") { model.go(toSectionNumber: 1) }
Button(L10n.string("nav.library")) { model.go(toSectionNumber: 1) }
.keyboardShortcut("1", modifiers: .command)
Button("Discover") { model.go(toSectionNumber: 2) }
Button(L10n.string("nav.discover")) { model.go(toSectionNumber: 2) }
.keyboardShortcut("2", modifiers: .command)
Button("Trending") { model.go(toSectionNumber: 3) }
Button(L10n.string("nav.trending")) { model.go(toSectionNumber: 3) }
.keyboardShortcut("3", modifiers: .command)
Button("Snapshots") { model.go(toSectionNumber: 4) }
Button(L10n.string("nav.snapshots")) { model.go(toSectionNumber: 4) }
.keyboardShortcut("4", modifiers: .command)
Button("Services") { model.go(toSectionNumber: 5) }
Button(L10n.string("nav.services")) { model.go(toSectionNumber: 5) }
.keyboardShortcut("5", modifiers: .command)
Button("Activity") { model.go(toSectionNumber: 6) }
Button(L10n.string("nav.activity")) { model.go(toSectionNumber: 6) }
.keyboardShortcut("6", modifiers: .command)
Button("Bundles") { model.go(toSectionNumber: 7) }
.keyboardShortcut("7", modifiers: .command)

Divider()

// ⌘K command palette.
Button("Command Palette…") { model.paletteOpen = true }
Button(L10n.string("palette.title.menu")) { model.paletteOpen = true }
.keyboardShortcut("k", modifiers: .command)
}

// ⌘R refresh + ⌘L drawer toggle replace the stock toolbar/sidebar
// command group so they slot into the View menu.
CommandGroup(after: .toolbar) {
Button("Refresh") {
Button(L10n.string("action.refresh")) {
Task { await model.refreshCurrent() }
}
.keyboardShortcut("r", modifiers: .command)
.disabled(model.isLoading)

Button(model.drawerOpen ? "Hide Activity Drawer" : "Show Activity Drawer") {
Button(model.drawerOpen ? L10n.string("activity.drawer.hide") : L10n.string("activity.drawer.show")) {
model.toggleDrawer()
}
.keyboardShortcut("l", modifiers: .command)

Button("Cycle Theme") { model.cycleTheme() }
Button(L10n.string("theme.cycle")) { model.cycleTheme() }
.keyboardShortcut("l", modifiers: [.command, .shift])
}
}
Expand Down
Loading