Marketplace: Tauri commands, store-owned fields and startup refresh

The 21 marketplace commands, registered and granted; marketplace fields
kept store-owned in update_settings/update_project; a background refresh
of every marketplace at app start.

- apply_marketplace_now emits marketplace-sync-finished per project (F4).
- Settings export carries marketplace account tokens in ExportedSecrets
  (account id -> token) and import restores them; imported accounts,
  marketplaces and global installs are validated with the commands' own
  rules before anything is written. The import preview discloses the
  marketplace count, token count and global hook installs, and warns on
  the latter (F10).
- refresh_pins and cache removal hold the repo lock (F11).
- ops::validate_host/validate_branch delegate to auth::valid_host and
  git::valid_branch (F13).
- A finished gh container login frees only its own cancel slot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-27 09:41:59 -07:00
co-authored by Claude Opus 5.5
parent 7a55c11b31
commit f4153dce42
17 changed files with 2111 additions and 6 deletions
@@ -32,6 +32,9 @@ const samplePreview: SettingsImportPreview = {
gateway_api_base: null,
image_source: "registry",
custom_image_name: null,
marketplace_count: 0,
global_hook_install_count: 0,
marketplace_account_token_count: 0,
};
function outcome(settings: AppSettings, secretRestoreWarnings: string[] = []): SettingsImportOutcome {
+19
View File
@@ -20,6 +20,9 @@ function preview(overrides: Partial<SettingsImportPreview> = {}): SettingsImport
gateway_api_base: null,
image_source: "registry",
custom_image_name: null,
marketplace_count: 0,
global_hook_install_count: 0,
marketplace_account_token_count: 0,
...overrides,
};
}
@@ -79,6 +82,13 @@ describe("describeImport", () => {
expect(items.some((i) => i.includes("OpenAI-compatible"))).toBe(false);
});
it("names marketplaces and marketplace account tokens, with counts", () => {
const items = describeImport(preview({ marketplace_count: 1, marketplace_account_token_count: 2 }));
expect(items).toContain("1 marketplace");
expect(items).toContain("2 marketplace account tokens");
expect(describeImport(preview()).some((i) => i.includes("marketplace"))).toBe(false);
});
it("names a custom Docker image when set, falling back to a placeholder if unnamed", () => {
expect(
describeImport(preview({ image_source: "custom", custom_image_name: "ghcr.io/me/triple-c" })),
@@ -116,6 +126,15 @@ describe("describeImportWarnings", () => {
]);
});
it("warns when the import installs hooks for every project", () => {
expect(describeImportWarnings(preview({ global_hook_install_count: 1 }))).toEqual([
"Installs 1 marketplace hook for all projects. Hooks run commands in every project container, and these skip the confirmation an install from the Marketplace tab asks for.",
]);
expect(describeImportWarnings(preview({ global_hook_install_count: 3 }))[0]).toMatch(
/^Installs 3 marketplace hooks for all projects\./,
);
});
it("warns about a custom Docker image every time, not only when it changes", () => {
expect(
describeImportWarnings(preview({ image_source: "custom", custom_image_name: "evil:latest" })),
+17
View File
@@ -28,6 +28,13 @@ export function describeImport(preview: SettingsImportPreview): string[] {
if (preview.image_source === "custom") {
items.push(`Docker image: ${preview.custom_image_name ?? "(no image name set)"}`);
}
if (preview.marketplace_count > 0) {
items.push(`${preview.marketplace_count} marketplace${preview.marketplace_count === 1 ? "" : "s"}`);
}
if (preview.marketplace_account_token_count > 0) {
const n = preview.marketplace_account_token_count;
items.push(`${n} marketplace account token${n === 1 ? "" : "s"}`);
}
return items;
}
@@ -45,6 +52,10 @@ export function describeImport(preview: SettingsImportPreview): string[] {
* through the UI, with no import-time signal that it wasn't freshly
* generated.
*
* Global marketplace hooks get one too: a hook runs commands in every
* project container, and an imported install never passed the hook-confirm
* step an install from the Marketplace tab shows.
*
* A custom Docker image gets a warning every time, not just on change: it's
* the image every project container is created from, so it's worth calling
* out regardless of what was configured before the import.
@@ -58,6 +69,12 @@ export function describeImportWarnings(preview: SettingsImportPreview): string[]
"Includes a web terminal access token that will activate the next time the web terminal is turned on.",
);
}
if (preview.global_hook_install_count > 0) {
const n = preview.global_hook_install_count;
warnings.push(
`Installs ${n} marketplace hook${n === 1 ? "" : "s"} for all projects. Hooks run commands in every project container, and these skip the confirmation an install from the Marketplace tab asks for.`,
);
}
if (preview.image_source === "custom") {
warnings.push(
`Runs every project container from a custom Docker image: ${preview.custom_image_name ?? "(no image name set)"}.`,
+8
View File
@@ -404,6 +404,14 @@ export interface SettingsImportPreview {
* more attention than an ordinary setting. */
image_source: ImageSource;
custom_image_name: string | null;
/** Marketplaces the import configures. */
marketplace_count: number;
/** Hooks the import installs for all projects — each runs commands in
* every project container, without the confirm step a Marketplace-tab
* install shows, so the preview warns about them. */
global_hook_install_count: number;
/** Marketplace account tokens the import restores to the keychain. */
marketplace_account_token_count: number;
}
/** What `apply_settings_import` returns: the settings that were actually