[eric] marketplace: the published .swarm catalog is the store tab, installed through the existing import door

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012G8kyALnPjsA7aJFmMBq3R
This commit is contained in:
ciregenz
2026-09-02 15:32:10 -07:00
co-authored by Claude Opus 5
parent f57e9509f8
commit 8afec84986
23 changed files with 2400 additions and 16 deletions
@@ -0,0 +1,62 @@
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { API_BASE } from '@/shared/config';
import type { Listing } from '@/app/pages/Directory/packages/catalog';
const MARKETPLACE_API = `${API_BASE}/marketplace`;
// 'cache' means the sheet was unreachable and these are the last listings we saw, which the tab says out loud.
export type CatalogSource = 'sheet' | 'cache' | 'empty';
interface CatalogState {
listings: Listing[];
source: CatalogSource;
loading: boolean;
loaded: boolean;
error: string;
}
const initialState: CatalogState = { listings: [], source: 'empty', loading: false, loaded: false, error: '' };
interface CatalogPayload {
source: CatalogSource;
count: number;
listings: Listing[];
error: string;
}
export const fetchMarketplaceListings = createAsyncThunk(
'marketplaceCatalog/fetch',
async (refresh: boolean = false) => {
const res = await fetch(`${MARKETPLACE_API}/listings${refresh ? '?refresh=true' : ''}`);
if (!res.ok) throw new Error(`Marketplace listings failed: ${res.status}`);
return (await res.json()) as CatalogPayload;
},
{ condition: (_, { getState }) => !(getState() as { marketplaceCatalog: CatalogState }).marketplaceCatalog.loading },
);
const marketplaceCatalogSlice = createSlice({
name: 'marketplaceCatalog',
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchMarketplaceListings.pending, (state) => {
state.loading = true;
})
.addCase(fetchMarketplaceListings.fulfilled, (state, action) => {
state.loading = false;
state.loaded = true;
state.listings = action.payload.listings;
state.source = action.payload.source;
state.error = action.payload.error;
})
.addCase(fetchMarketplaceListings.rejected, (state, action) => {
state.loading = false;
state.loaded = true;
// Keep whatever listings we already had: a failed refresh must not empty a populated store.
state.error = action.error.message || 'Could not reach the marketplace.';
});
},
});
export default marketplaceCatalogSlice.reducer;
+2
View File
@@ -8,6 +8,7 @@ import modesReducer from './modesSlice';
import settingsReducer from './settingsSlice';
import mcpRegistryReducer from './mcpRegistrySlice';
import skillRegistryReducer from './skillRegistrySlice';
import marketplaceCatalogReducer from './marketplaceCatalogSlice';
import outputsReducer from './outputsSlice';
import dashboardLayoutReducer from './dashboardLayoutSlice';
import dashboardsReducer from './dashboardsSlice';
@@ -31,6 +32,7 @@ export const store = configureStore({
settings: settingsReducer,
mcpRegistry: mcpRegistryReducer,
skillRegistry: skillRegistryReducer,
marketplaceCatalog: marketplaceCatalogReducer,
outputs: outputsReducer,
dashboardLayout: dashboardLayoutReducer,
dashboards: dashboardsReducer,