Merge pull request #112 from y574444354/opt/mcp-detector
fix(favorite): sync load/unload state and improve error visibility
This commit is contained in:
commit
d98b4d0cde
|
|
@ -1,99 +1,93 @@
|
||||||
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { Box, Text, Dialog } from '@anthropic/ink'
|
import { Box, Text, Dialog } from '@anthropic/ink';
|
||||||
import { SelectMulti } from '../../components/CustomSelect/SelectMulti.js'
|
import { SelectMulti } from '../../components/CustomSelect/SelectMulti.js';
|
||||||
import {
|
import {
|
||||||
listFavoriteItems,
|
listFavoriteItems,
|
||||||
loadFavoriteItem,
|
loadFavoriteItem,
|
||||||
unloadFavoriteItem,
|
unloadFavoriteItem,
|
||||||
type FavoriteItemWithStatus,
|
type FavoriteItemWithStatus,
|
||||||
} from '../../costrict/favorite/favorite.js'
|
} from '../../costrict/favorite/favorite.js';
|
||||||
import type { LocalJSXCommandCall } from '../../types/command.js'
|
import type { LocalJSXCommandCall } from '../../types/command.js';
|
||||||
|
|
||||||
function CloudEnabledMenu({
|
function CloudEnabledMenu({
|
||||||
onDone,
|
onDone,
|
||||||
}: {
|
}: {
|
||||||
onDone: (
|
onDone: (result?: string, options?: { display?: 'skip' | 'system' | 'user' }) => void;
|
||||||
result?: string,
|
|
||||||
options?: { display?: 'skip' | 'system' | 'user' },
|
|
||||||
) => void
|
|
||||||
}): React.ReactNode {
|
}): React.ReactNode {
|
||||||
const [items, setItems] = useState<FavoriteItemWithStatus[] | null>(null)
|
const [items, setItems] = useState<FavoriteItemWithStatus[] | null>(null);
|
||||||
const previousSlugs = useRef<Set<string>>(new Set())
|
const previousSlugs = useRef<Set<string>>(new Set());
|
||||||
const isFirstChange = useRef(true)
|
const isFirstChange = useRef(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false
|
let cancelled = false;
|
||||||
listFavoriteItems()
|
listFavoriteItems()
|
||||||
.then((data) => {
|
.then(data => {
|
||||||
if (cancelled) return
|
if (cancelled) return;
|
||||||
if (data.length === 0) {
|
if (data.length === 0) {
|
||||||
onDone('No cloud favorites found', { display: 'system' })
|
onDone('No cloud favorites found', { display: 'system' });
|
||||||
} else {
|
} else {
|
||||||
setItems(data)
|
setItems(data);
|
||||||
previousSlugs.current = new Set(data.map((item) => item.slug))
|
previousSlugs.current = new Set(data.map(item => item.slug));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error: unknown) => {
|
.catch((error: unknown) => {
|
||||||
if (cancelled) return
|
if (cancelled) return;
|
||||||
const message = error instanceof Error ? error.message : String(error)
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
onDone(`Failed to load cloud favorites: ${message}`, {
|
onDone(`Failed to load cloud favorites: ${message}`, {
|
||||||
display: 'system',
|
display: 'system',
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true
|
cancelled = true;
|
||||||
}
|
};
|
||||||
}, [onDone])
|
}, [onDone]);
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
onDone('Cancelled', { display: 'system' })
|
onDone('Cancelled', { display: 'system' });
|
||||||
}
|
};
|
||||||
|
|
||||||
const options = useMemo(
|
const options = useMemo(
|
||||||
() =>
|
() =>
|
||||||
(items ?? []).map((item) => ({
|
(items ?? []).map(item => ({
|
||||||
label: `[${item.itemType}] ${item.name} (${item.status})`,
|
label: `[${item.itemType}] ${item.name} (${item.status})`,
|
||||||
value: item.slug,
|
value: item.slug,
|
||||||
})),
|
})),
|
||||||
[items],
|
[items],
|
||||||
)
|
);
|
||||||
|
|
||||||
const defaultValue = useMemo(
|
const defaultValue = useMemo(
|
||||||
() => (items ?? []).map((item) => item.slug),
|
() => (items ?? []).filter(item => item.status === 'Active').map(item => item.slug),
|
||||||
[items],
|
[items],
|
||||||
)
|
);
|
||||||
|
|
||||||
const handleChange = async (selectedSlugs: string[]) => {
|
const handleChange = async (selectedSlugs: string[]) => {
|
||||||
if (isFirstChange.current) {
|
if (isFirstChange.current) {
|
||||||
isFirstChange.current = false
|
isFirstChange.current = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const prevSet = previousSlugs.current
|
const prevSet = previousSlugs.current;
|
||||||
const nextSet = new Set(selectedSlugs)
|
const nextSet = new Set(selectedSlugs);
|
||||||
|
|
||||||
const toLoad = (items ?? []).filter(
|
const toLoad = (items ?? []).filter(item => nextSet.has(item.slug) && !prevSet.has(item.slug));
|
||||||
(item) => nextSet.has(item.slug) && !prevSet.has(item.slug),
|
const toUnload = (items ?? []).filter(item => !nextSet.has(item.slug) && prevSet.has(item.slug));
|
||||||
)
|
|
||||||
const toUnload = (items ?? []).filter(
|
|
||||||
(item) => !nextSet.has(item.slug) && prevSet.has(item.slug),
|
|
||||||
)
|
|
||||||
|
|
||||||
previousSlugs.current = nextSet
|
previousSlugs.current = nextSet;
|
||||||
|
|
||||||
if (toLoad.length === 0 && toUnload.length === 0) return
|
if (toLoad.length === 0 && toUnload.length === 0) return;
|
||||||
|
|
||||||
const results = await Promise.allSettled([
|
const results = await Promise.allSettled([
|
||||||
...toLoad.map((item) => loadFavoriteItem(item.slug)),
|
...toLoad.map(item => loadFavoriteItem(item.slug)),
|
||||||
...toUnload.map((item) => unloadFavoriteItem(item.slug)),
|
...toUnload.map(item => unloadFavoriteItem(item.slug)),
|
||||||
])
|
]);
|
||||||
|
|
||||||
const failures = results.filter((r) => r.status === 'rejected')
|
const failures = results.filter(r => r.status === 'rejected') as PromiseRejectedResult[];
|
||||||
if (failures.length > 0) {
|
if (failures.length > 0) {
|
||||||
onDone(`Cloud enabled items updated with ${failures.length} errors`, {
|
const reasons = failures.map(r => (r.reason instanceof Error ? r.reason.message : String(r.reason)));
|
||||||
|
onDone(`Cloud enabled items updated with ${failures.length} errors: ${reasons.join('; ')}`, {
|
||||||
display: 'system',
|
display: 'system',
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
if (items === null) {
|
if (items === null) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -106,7 +100,7 @@ function CloudEnabledMenu({
|
||||||
<Text>Loading cloud favorites...</Text>
|
<Text>Loading cloud favorites...</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -124,9 +118,9 @@ function CloudEnabledMenu({
|
||||||
hideIndexes
|
hideIndexes
|
||||||
/>
|
/>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const call: LocalJSXCommandCall = async (onDone) => {
|
export const call: LocalJSXCommandCall = async onDone => {
|
||||||
return <CloudEnabledMenu onDone={onDone} />
|
return <CloudEnabledMenu onDone={onDone} />;
|
||||||
}
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,12 @@
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
import { mkdir, readFile, readdir, rm, writeFile, copyFile } from 'node:fs/promises'
|
import {
|
||||||
|
mkdir,
|
||||||
|
readFile,
|
||||||
|
readdir,
|
||||||
|
rm,
|
||||||
|
writeFile,
|
||||||
|
copyFile,
|
||||||
|
} from 'node:fs/promises'
|
||||||
import { createCoStrictFetch } from '../provider/fetch.js'
|
import { createCoStrictFetch } from '../provider/fetch.js'
|
||||||
import { getCoStrictBaseURL } from '../provider/auth.js'
|
import { getCoStrictBaseURL } from '../provider/auth.js'
|
||||||
import { getClaudeConfigHomeDir } from '../../utils/envUtils.js'
|
import { getClaudeConfigHomeDir } from '../../utils/envUtils.js'
|
||||||
|
|
@ -140,7 +147,10 @@ async function fetchWithTimeout(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function listRemoteCandidates(storeType?: string, extraParams?: Record<string, string>) {
|
async function listRemoteCandidates(
|
||||||
|
storeType?: string,
|
||||||
|
extraParams?: Record<string, string>,
|
||||||
|
) {
|
||||||
const baseUrl = getCoStrictBaseURL()
|
const baseUrl = getCoStrictBaseURL()
|
||||||
const costrictFetch = createCoStrictFetch()
|
const costrictFetch = createCoStrictFetch()
|
||||||
const result: Array<{ id: string } & Record<string, unknown>> = []
|
const result: Array<{ id: string } & Record<string, unknown>> = []
|
||||||
|
|
@ -171,7 +181,9 @@ async function listRemoteCandidates(storeType?: string, extraParams?: Record<str
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseFavoriteListItem(data: Record<string, unknown>): FavoriteItem | undefined {
|
function parseFavoriteListItem(
|
||||||
|
data: Record<string, unknown>,
|
||||||
|
): FavoriteItem | undefined {
|
||||||
const storeType = String(data.itemType ?? '')
|
const storeType = String(data.itemType ?? '')
|
||||||
const localType = STORE_TYPE_MAP[storeType]
|
const localType = STORE_TYPE_MAP[storeType]
|
||||||
if (!localType) return undefined
|
if (!localType) return undefined
|
||||||
|
|
@ -190,7 +202,8 @@ function parseFavoriteListItem(data: Record<string, unknown>): FavoriteItem | un
|
||||||
content: '',
|
content: '',
|
||||||
category: typeof data.category === 'string' ? data.category : undefined,
|
category: typeof data.category === 'string' ? data.category : undefined,
|
||||||
version: typeof data.version === 'string' ? data.version : undefined,
|
version: typeof data.version === 'string' ? data.version : undefined,
|
||||||
favoriteCount: typeof data.favoriteCount === 'number' ? data.favoriteCount : undefined,
|
favoriteCount:
|
||||||
|
typeof data.favoriteCount === 'number' ? data.favoriteCount : undefined,
|
||||||
favorited,
|
favorited,
|
||||||
createdBy: typeof data.createdBy === 'string' ? data.createdBy : undefined,
|
createdBy: typeof data.createdBy === 'string' ? data.createdBy : undefined,
|
||||||
createdAt: typeof data.createdAt === 'string' ? data.createdAt : undefined,
|
createdAt: typeof data.createdAt === 'string' ? data.createdAt : undefined,
|
||||||
|
|
@ -213,7 +226,9 @@ async function getRemoteItem(id: string): Promise<FavoriteItem> {
|
||||||
const storeType = String(data.itemType ?? '')
|
const storeType = String(data.itemType ?? '')
|
||||||
const localType = STORE_TYPE_MAP[storeType]
|
const localType = STORE_TYPE_MAP[storeType]
|
||||||
if (!localType) {
|
if (!localType) {
|
||||||
throw new Error(`Unsupported item type: ${storeType} (${String(data.slug ?? id)})`)
|
throw new Error(
|
||||||
|
`Unsupported item type: ${storeType} (${String(data.slug ?? id)})`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
id: String(data.id),
|
id: String(data.id),
|
||||||
|
|
@ -224,7 +239,8 @@ async function getRemoteItem(id: string): Promise<FavoriteItem> {
|
||||||
content: String(data.content ?? ''),
|
content: String(data.content ?? ''),
|
||||||
category: typeof data.category === 'string' ? data.category : undefined,
|
category: typeof data.category === 'string' ? data.category : undefined,
|
||||||
version: typeof data.version === 'string' ? data.version : undefined,
|
version: typeof data.version === 'string' ? data.version : undefined,
|
||||||
favoriteCount: typeof data.favoriteCount === 'number' ? data.favoriteCount : undefined,
|
favoriteCount:
|
||||||
|
typeof data.favoriteCount === 'number' ? data.favoriteCount : undefined,
|
||||||
favorited: typeof data.favorited === 'boolean' ? data.favorited : undefined,
|
favorited: typeof data.favorited === 'boolean' ? data.favorited : undefined,
|
||||||
createdBy: typeof data.createdBy === 'string' ? data.createdBy : undefined,
|
createdBy: typeof data.createdBy === 'string' ? data.createdBy : undefined,
|
||||||
createdAt: typeof data.createdAt === 'string' ? data.createdAt : undefined,
|
createdAt: typeof data.createdAt === 'string' ? data.createdAt : undefined,
|
||||||
|
|
@ -339,7 +355,7 @@ async function persistInstalledItem(item: FavoriteItem) {
|
||||||
) + '\n',
|
) + '\n',
|
||||||
)
|
)
|
||||||
|
|
||||||
await mutateState((state) => {
|
await mutateState(state => {
|
||||||
state.items[item.slug] = {
|
state.items[item.slug] = {
|
||||||
id: item.id,
|
id: item.id,
|
||||||
slug: item.slug,
|
slug: item.slug,
|
||||||
|
|
@ -347,7 +363,8 @@ async function persistInstalledItem(item: FavoriteItem) {
|
||||||
itemType: item.itemType,
|
itemType: item.itemType,
|
||||||
localPath: dir,
|
localPath: dir,
|
||||||
lifecycle: state.items[item.slug]?.lifecycle ?? 'downloaded',
|
lifecycle: state.items[item.slug]?.lifecycle ?? 'downloaded',
|
||||||
installedAt: state.items[item.slug]?.installedAt ?? new Date().toISOString(),
|
installedAt:
|
||||||
|
state.items[item.slug]?.installedAt ?? new Date().toISOString(),
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -355,7 +372,9 @@ async function persistInstalledItem(item: FavoriteItem) {
|
||||||
return dir
|
return dir
|
||||||
}
|
}
|
||||||
|
|
||||||
async function hasUsableLocalContent(hit: FavoriteStateRecord): Promise<boolean> {
|
async function hasUsableLocalContent(
|
||||||
|
hit: FavoriteStateRecord,
|
||||||
|
): Promise<boolean> {
|
||||||
const contentPath = (() => {
|
const contentPath = (() => {
|
||||||
switch (hit.itemType) {
|
switch (hit.itemType) {
|
||||||
case 'skill':
|
case 'skill':
|
||||||
|
|
@ -369,7 +388,9 @@ async function hasUsableLocalContent(hit: FavoriteStateRecord): Promise<boolean>
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
try {
|
try {
|
||||||
const { size } = await import('node:fs/promises').then((m) => m.stat(contentPath))
|
const { size } = await import('node:fs/promises').then(m =>
|
||||||
|
m.stat(contentPath),
|
||||||
|
)
|
||||||
return size > 0
|
return size > 0
|
||||||
} catch {
|
} catch {
|
||||||
return false
|
return false
|
||||||
|
|
@ -378,13 +399,17 @@ async function hasUsableLocalContent(hit: FavoriteStateRecord): Promise<boolean>
|
||||||
|
|
||||||
async function ensureInstalled(slugOrId: string): Promise<FavoriteStateRecord> {
|
async function ensureInstalled(slugOrId: string): Promise<FavoriteStateRecord> {
|
||||||
const state = await readState()
|
const state = await readState()
|
||||||
const hit = state.items[slugOrId] ?? Object.values(state.items).find((item) => item.id === slugOrId)
|
const hit =
|
||||||
|
state.items[slugOrId] ??
|
||||||
|
Object.values(state.items).find(item => item.id === slugOrId)
|
||||||
if (hit && (await hasUsableLocalContent(hit))) return hit
|
if (hit && (await hasUsableLocalContent(hit))) return hit
|
||||||
|
|
||||||
let remote = hit ? await getRemoteItem(hit.id).catch(() => undefined) : undefined
|
let remote = hit
|
||||||
|
? await getRemoteItem(hit.id).catch(() => undefined)
|
||||||
|
: undefined
|
||||||
if (!remote) {
|
if (!remote) {
|
||||||
const favorites = await listFavoriteItems()
|
const favorites = await listFavoriteItems()
|
||||||
const listed = favorites.find((f) => f.slug === slugOrId || f.id === slugOrId)
|
const listed = favorites.find(f => f.slug === slugOrId || f.id === slugOrId)
|
||||||
if (!listed) throw new Error(`Favorite item not found: ${slugOrId}`)
|
if (!listed) throw new Error(`Favorite item not found: ${slugOrId}`)
|
||||||
remote = await getRemoteItem(listed.id)
|
remote = await getRemoteItem(listed.id)
|
||||||
}
|
}
|
||||||
|
|
@ -413,11 +438,20 @@ const SUPPORTED_MCP_TYPES = [
|
||||||
'ws-ide',
|
'ws-ide',
|
||||||
]
|
]
|
||||||
|
|
||||||
function convertMcpConfig(config: Record<string, unknown>): Record<string, unknown> | undefined {
|
function convertMcpConfig(
|
||||||
if (typeof config.type === 'string' && SUPPORTED_MCP_TYPES.includes(config.type)) {
|
config: Record<string, unknown>,
|
||||||
|
): Record<string, unknown> | undefined {
|
||||||
|
if (
|
||||||
|
typeof config.type === 'string' &&
|
||||||
|
SUPPORTED_MCP_TYPES.includes(config.type)
|
||||||
|
) {
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
if (config.mcpServers && typeof config.mcpServers === 'object' && !Array.isArray(config.mcpServers)) {
|
if (
|
||||||
|
config.mcpServers &&
|
||||||
|
typeof config.mcpServers === 'object' &&
|
||||||
|
!Array.isArray(config.mcpServers)
|
||||||
|
) {
|
||||||
const servers = Object.entries(config.mcpServers as Record<string, unknown>)
|
const servers = Object.entries(config.mcpServers as Record<string, unknown>)
|
||||||
if (servers.length === 0) return undefined
|
if (servers.length === 0) return undefined
|
||||||
const [, server] = servers[0]
|
const [, server] = servers[0]
|
||||||
|
|
@ -429,7 +463,9 @@ function convertMcpConfig(config: Record<string, unknown>): Record<string, unkno
|
||||||
return convertSingleMcpServer(config)
|
return convertSingleMcpServer(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertSingleMcpServer(server: Record<string, unknown>): Record<string, unknown> | undefined {
|
function convertSingleMcpServer(
|
||||||
|
server: Record<string, unknown>,
|
||||||
|
): Record<string, unknown> | undefined {
|
||||||
const command = server.command
|
const command = server.command
|
||||||
const args = server.args
|
const args = server.args
|
||||||
if (!command && !args) return undefined
|
if (!command && !args) return undefined
|
||||||
|
|
@ -486,8 +522,11 @@ async function addItemToConfig(item: FavoriteItem, localPath: string) {
|
||||||
case 'agent': {
|
case 'agent': {
|
||||||
const mdPath = path.join(localPath, `${item.slug}.md`)
|
const mdPath = path.join(localPath, `${item.slug}.md`)
|
||||||
const content = await readFile(mdPath, 'utf-8')
|
const content = await readFile(mdPath, 'utf-8')
|
||||||
const { frontmatter, content: markdownContent } = parseFrontmatter(content, mdPath)
|
const { frontmatter, content: markdownContent } = parseFrontmatter(
|
||||||
saveGlobalConfig((cfg) => ({
|
content,
|
||||||
|
mdPath,
|
||||||
|
)
|
||||||
|
saveGlobalConfig(cfg => ({
|
||||||
...cfg,
|
...cfg,
|
||||||
agents: {
|
agents: {
|
||||||
...(cfg.agents ?? {}),
|
...(cfg.agents ?? {}),
|
||||||
|
|
@ -502,8 +541,11 @@ async function addItemToConfig(item: FavoriteItem, localPath: string) {
|
||||||
case 'command': {
|
case 'command': {
|
||||||
const mdPath = path.join(localPath, `${item.slug}.md`)
|
const mdPath = path.join(localPath, `${item.slug}.md`)
|
||||||
const content = await readFile(mdPath, 'utf-8')
|
const content = await readFile(mdPath, 'utf-8')
|
||||||
const { frontmatter, content: markdownContent } = parseFrontmatter(content, mdPath)
|
const { frontmatter, content: markdownContent } = parseFrontmatter(
|
||||||
saveGlobalConfig((cfg) => ({
|
content,
|
||||||
|
mdPath,
|
||||||
|
)
|
||||||
|
saveGlobalConfig(cfg => ({
|
||||||
...cfg,
|
...cfg,
|
||||||
commands: {
|
commands: {
|
||||||
...(cfg.commands ?? {}),
|
...(cfg.commands ?? {}),
|
||||||
|
|
@ -517,7 +559,9 @@ async function addItemToConfig(item: FavoriteItem, localPath: string) {
|
||||||
}
|
}
|
||||||
case 'mcp': {
|
case 'mcp': {
|
||||||
const mcpJsonPath = path.join(localPath, 'mcp.json')
|
const mcpJsonPath = path.join(localPath, 'mcp.json')
|
||||||
const configJson = JSON.parse(await readFile(mcpJsonPath, 'utf-8')) as Record<string, unknown>
|
const configJson = JSON.parse(
|
||||||
|
await readFile(mcpJsonPath, 'utf-8'),
|
||||||
|
) as Record<string, unknown>
|
||||||
const converted = convertMcpConfig(configJson)
|
const converted = convertMcpConfig(configJson)
|
||||||
if (!converted) {
|
if (!converted) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|
@ -526,7 +570,7 @@ async function addItemToConfig(item: FavoriteItem, localPath: string) {
|
||||||
`Supported formats: opencode native, VS Code / Claude Desktop style, or simplified { command, args }`,
|
`Supported formats: opencode native, VS Code / Claude Desktop style, or simplified { command, args }`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
saveGlobalConfig((cfg) => ({
|
saveGlobalConfig(cfg => ({
|
||||||
...cfg,
|
...cfg,
|
||||||
mcpServers: {
|
mcpServers: {
|
||||||
...(cfg.mcpServers ?? {}),
|
...(cfg.mcpServers ?? {}),
|
||||||
|
|
@ -538,7 +582,11 @@ async function addItemToConfig(item: FavoriteItem, localPath: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeItemFromConfig(itemType: FavoriteItemType, slug: string, _localPath: string) {
|
async function removeItemFromConfig(
|
||||||
|
itemType: FavoriteItemType,
|
||||||
|
slug: string,
|
||||||
|
_localPath: string,
|
||||||
|
) {
|
||||||
switch (itemType) {
|
switch (itemType) {
|
||||||
case 'skill': {
|
case 'skill': {
|
||||||
const destDir = path.join(skillsDir(), slug)
|
const destDir = path.join(skillsDir(), slug)
|
||||||
|
|
@ -547,7 +595,7 @@ async function removeItemFromConfig(itemType: FavoriteItemType, slug: string, _l
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'agent': {
|
case 'agent': {
|
||||||
saveGlobalConfig((cfg) => {
|
saveGlobalConfig(cfg => {
|
||||||
const agents = { ...(cfg.agents ?? {}) }
|
const agents = { ...(cfg.agents ?? {}) }
|
||||||
delete agents[slug]
|
delete agents[slug]
|
||||||
return { ...cfg, agents }
|
return { ...cfg, agents }
|
||||||
|
|
@ -555,7 +603,7 @@ async function removeItemFromConfig(itemType: FavoriteItemType, slug: string, _l
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'command': {
|
case 'command': {
|
||||||
saveGlobalConfig((cfg) => {
|
saveGlobalConfig(cfg => {
|
||||||
const commands = { ...(cfg.commands ?? {}) }
|
const commands = { ...(cfg.commands ?? {}) }
|
||||||
delete commands[slug]
|
delete commands[slug]
|
||||||
return { ...cfg, commands }
|
return { ...cfg, commands }
|
||||||
|
|
@ -563,7 +611,7 @@ async function removeItemFromConfig(itemType: FavoriteItemType, slug: string, _l
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'mcp': {
|
case 'mcp': {
|
||||||
saveGlobalConfig((cfg) => {
|
saveGlobalConfig(cfg => {
|
||||||
const mcpServers = { ...(cfg.mcpServers ?? {}) }
|
const mcpServers = { ...(cfg.mcpServers ?? {}) }
|
||||||
delete mcpServers[slug]
|
delete mcpServers[slug]
|
||||||
return { ...cfg, mcpServers }
|
return { ...cfg, mcpServers }
|
||||||
|
|
@ -573,7 +621,9 @@ async function removeItemFromConfig(itemType: FavoriteItemType, slug: string, _l
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function readItemForConfig(installed: FavoriteStateRecord): Promise<FavoriteItem> {
|
async function readItemForConfig(
|
||||||
|
installed: FavoriteStateRecord,
|
||||||
|
): Promise<FavoriteItem> {
|
||||||
const itemMetaPath = path.join(installed.localPath, 'item.json')
|
const itemMetaPath = path.join(installed.localPath, 'item.json')
|
||||||
let itemMeta: Record<string, unknown> = {}
|
let itemMeta: Record<string, unknown> = {}
|
||||||
try {
|
try {
|
||||||
|
|
@ -592,7 +642,9 @@ async function readItemForConfig(installed: FavoriteStateRecord): Promise<Favori
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listFavoriteItems(type?: FavoriteItemType): Promise<FavoriteItemWithStatus[]> {
|
export async function listFavoriteItems(
|
||||||
|
type?: FavoriteItemType,
|
||||||
|
): Promise<FavoriteItemWithStatus[]> {
|
||||||
const cacheKey = type ?? 'all'
|
const cacheKey = type ?? 'all'
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
if (
|
if (
|
||||||
|
|
@ -603,7 +655,9 @@ export async function listFavoriteItems(type?: FavoriteItemType): Promise<Favori
|
||||||
return _listFavoriteItemsCache.promise
|
return _listFavoriteItemsCache.promise
|
||||||
}
|
}
|
||||||
|
|
||||||
const storeTypes = type ? [LOCAL_TO_STORE_TYPE[type]] : Object.values(LOCAL_TO_STORE_TYPE)
|
const storeTypes = type
|
||||||
|
? [LOCAL_TO_STORE_TYPE[type]]
|
||||||
|
: Object.values(LOCAL_TO_STORE_TYPE)
|
||||||
const errors: Error[] = []
|
const errors: Error[] = []
|
||||||
const candidates: Array<{ id: string } & Record<string, unknown>> = []
|
const candidates: Array<{ id: string } & Record<string, unknown>> = []
|
||||||
|
|
||||||
|
|
@ -612,7 +666,9 @@ export async function listFavoriteItems(type?: FavoriteItemType): Promise<Favori
|
||||||
const page = await listRemoteCandidates(st, { favorited: 'true' })
|
const page = await listRemoteCandidates(st, { favorited: 'true' })
|
||||||
candidates.push(...page)
|
candidates.push(...page)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logForDebugging(`failed to fetch remote favorite candidates: type=${st}, error=${error instanceof Error ? error.message : String(error)}`)
|
logForDebugging(
|
||||||
|
`failed to fetch remote favorite candidates: type=${st}, error=${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
)
|
||||||
if (error instanceof Error) errors.push(error)
|
if (error instanceof Error) errors.push(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -622,7 +678,13 @@ export async function listFavoriteItems(type?: FavoriteItemType): Promise<Favori
|
||||||
throw new Error(`Cloud service unavailable (${errors[0].message})`)
|
throw new Error(`Cloud service unavailable (${errors[0].message})`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const [activeSkillSlugs, activeAgentNames, activeCommandNames, activeMcpNames, state] = await Promise.all([
|
const [
|
||||||
|
activeSkillSlugs,
|
||||||
|
activeAgentNames,
|
||||||
|
activeCommandNames,
|
||||||
|
activeMcpNames,
|
||||||
|
state,
|
||||||
|
] = await Promise.all([
|
||||||
getActiveSkillSlugs(),
|
getActiveSkillSlugs(),
|
||||||
getActiveAgentNames(),
|
getActiveAgentNames(),
|
||||||
getActiveCommandNames(),
|
getActiveCommandNames(),
|
||||||
|
|
@ -642,7 +704,13 @@ export async function listFavoriteItems(type?: FavoriteItemType): Promise<Favori
|
||||||
const local = state.items[item.slug]
|
const local = state.items[item.slug]
|
||||||
result.push({
|
result.push({
|
||||||
...item,
|
...item,
|
||||||
status: deriveStatus(local, activeSkillSlugs, activeAgentNames, activeCommandNames, activeMcpNames),
|
status: deriveStatus(
|
||||||
|
local,
|
||||||
|
activeSkillSlugs,
|
||||||
|
activeAgentNames,
|
||||||
|
activeCommandNames,
|
||||||
|
activeMcpNames,
|
||||||
|
),
|
||||||
localPath: local?.localPath,
|
localPath: local?.localPath,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -659,7 +727,13 @@ export async function listFavoriteItems(type?: FavoriteItemType): Promise<Favori
|
||||||
description: '',
|
description: '',
|
||||||
itemType: record.itemType,
|
itemType: record.itemType,
|
||||||
content: '',
|
content: '',
|
||||||
status: deriveStatus(record, activeSkillSlugs, activeAgentNames, activeCommandNames, activeMcpNames),
|
status: deriveStatus(
|
||||||
|
record,
|
||||||
|
activeSkillSlugs,
|
||||||
|
activeAgentNames,
|
||||||
|
activeCommandNames,
|
||||||
|
activeMcpNames,
|
||||||
|
),
|
||||||
localPath: record.localPath,
|
localPath: record.localPath,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -677,9 +751,11 @@ export async function listFavoriteItems(type?: FavoriteItemType): Promise<Favori
|
||||||
return sorted
|
return sorted
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function viewFavoriteItem(slugOrId: string): Promise<FavoriteItemWithStatus> {
|
export async function viewFavoriteItem(
|
||||||
|
slugOrId: string,
|
||||||
|
): Promise<FavoriteItemWithStatus> {
|
||||||
const favorites = await listFavoriteItems()
|
const favorites = await listFavoriteItems()
|
||||||
const item = favorites.find((f) => f.slug === slugOrId || f.id === slugOrId)
|
const item = favorites.find(f => f.slug === slugOrId || f.id === slugOrId)
|
||||||
if (!item) throw new Error(`Favorite item not found: ${slugOrId}`)
|
if (!item) throw new Error(`Favorite item not found: ${slugOrId}`)
|
||||||
|
|
||||||
const detail = await getRemoteItem(item.id)
|
const detail = await getRemoteItem(item.id)
|
||||||
|
|
@ -694,22 +770,28 @@ export async function loadFavoriteItem(slugOrId: string) {
|
||||||
const installed = await ensureInstalled(slugOrId)
|
const installed = await ensureInstalled(slugOrId)
|
||||||
const itemForConfig = await readItemForConfig(installed)
|
const itemForConfig = await readItemForConfig(installed)
|
||||||
await addItemToConfig(itemForConfig, installed.localPath)
|
await addItemToConfig(itemForConfig, installed.localPath)
|
||||||
await mutateState((state) => {
|
await mutateState(state => {
|
||||||
const record = state.items[installed.slug]
|
const record = state.items[installed.slug]
|
||||||
record.lifecycle = 'active'
|
record.lifecycle = 'active'
|
||||||
record.updatedAt = new Date().toISOString()
|
record.updatedAt = new Date().toISOString()
|
||||||
})
|
})
|
||||||
|
_listFavoriteItemsCache = null
|
||||||
return installed
|
return installed
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function unloadFavoriteItem(slugOrId: string) {
|
export async function unloadFavoriteItem(slugOrId: string) {
|
||||||
const installed = await ensureInstalled(slugOrId)
|
const installed = await ensureInstalled(slugOrId)
|
||||||
await removeItemFromConfig(installed.itemType, installed.slug, installed.localPath)
|
await removeItemFromConfig(
|
||||||
await mutateState((state) => {
|
installed.itemType,
|
||||||
|
installed.slug,
|
||||||
|
installed.localPath,
|
||||||
|
)
|
||||||
|
await mutateState(state => {
|
||||||
const record = state.items[installed.slug]
|
const record = state.items[installed.slug]
|
||||||
record.lifecycle = 'unloaded'
|
record.lifecycle = 'unloaded'
|
||||||
record.updatedAt = new Date().toISOString()
|
record.updatedAt = new Date().toISOString()
|
||||||
})
|
})
|
||||||
|
_listFavoriteItemsCache = null
|
||||||
return installed
|
return installed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -720,12 +802,10 @@ export async function unloadFavoriteItem(slugOrId: string) {
|
||||||
export async function autoEnableCloudFavorites(): Promise<void> {
|
export async function autoEnableCloudFavorites(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const items = await listFavoriteItems()
|
const items = await listFavoriteItems()
|
||||||
const toEnable = items.filter((item) => item.status !== 'Active')
|
const toEnable = items.filter(item => item.status !== 'Active')
|
||||||
if (toEnable.length === 0) return
|
if (toEnable.length === 0) return
|
||||||
|
|
||||||
await Promise.allSettled(
|
await Promise.allSettled(toEnable.map(item => loadFavoriteItem(item.slug)))
|
||||||
toEnable.map((item) => loadFavoriteItem(item.slug)),
|
|
||||||
)
|
|
||||||
} catch {
|
} catch {
|
||||||
// Silently ignore so a flaky cloud API doesn't break startup
|
// Silently ignore so a flaky cloud API doesn't break startup
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user