@chatcode/cco-market 1.45.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +146 -0
- package/README.zh.md +143 -0
- package/UPDATE-API-V1.md +132 -0
- package/client/client.js +11534 -0
- package/cordis.patch.yml +5 -0
- package/lib/accelerate.js +184 -0
- package/lib/agents.js +36 -0
- package/lib/backup.js +572 -0
- package/lib/catalog-local-match.js +126 -0
- package/lib/catalog-npm.js +111 -0
- package/lib/changelog.js +242 -0
- package/lib/channels.js +64 -0
- package/lib/check.js +1067 -0
- package/lib/compatibility.js +198 -0
- package/lib/diagnostics.js +49 -0
- package/lib/discovery-compatibility.js +260 -0
- package/lib/dsh-cli.js +949 -0
- package/lib/dsh-install.js +108 -0
- package/lib/gist.js +352 -0
- package/lib/groups.js +97 -0
- package/lib/home-paths.js +42 -0
- package/lib/hot.js +519 -0
- package/lib/http.js +39 -0
- package/lib/index.js +97 -0
- package/lib/install.js +387 -0
- package/lib/log.js +197 -0
- package/lib/ndjson.js +154 -0
- package/lib/net.js +98 -0
- package/lib/order.js +284 -0
- package/lib/patch.js +521 -0
- package/lib/pnpm-compat.js +490 -0
- package/lib/presets.js +278 -0
- package/lib/profile.js +911 -0
- package/lib/region-probe.js +93 -0
- package/lib/regions.js +253 -0
- package/lib/registry.js +204 -0
- package/lib/restart.js +373 -0
- package/lib/routes.js +4372 -0
- package/lib/settings.js +109 -0
- package/lib/skill-market/api.js +119 -0
- package/lib/skill-market/config.js +29 -0
- package/lib/skill-market/installer.js +352 -0
- package/lib/skill-market/runtime.js +13 -0
- package/lib/skill-market/tui.js +119 -0
- package/lib/skill-market/types.js +1 -0
- package/lib/skill-market/web-routes.js +123 -0
- package/lib/snapshot.js +500 -0
- package/lib/source-migration.js +54 -0
- package/lib/sources.js +596 -0
- package/lib/store.js +91 -0
- package/lib/themes.js +102 -0
- package/lib/trial.js +116 -0
- package/lib/types/accelerate.d.ts +85 -0
- package/lib/types/agents.d.ts +22 -0
- package/lib/types/backup.d.ts +131 -0
- package/lib/types/catalog-local-match.d.ts +50 -0
- package/lib/types/catalog-npm.d.ts +47 -0
- package/lib/types/changelog.d.ts +102 -0
- package/lib/types/channels.d.ts +56 -0
- package/lib/types/check.d.ts +266 -0
- package/lib/types/compatibility.d.ts +64 -0
- package/lib/types/diagnostics.d.ts +29 -0
- package/lib/types/discovery-compatibility.d.ts +80 -0
- package/lib/types/dsh-cli.d.ts +324 -0
- package/lib/types/dsh-install.d.ts +46 -0
- package/lib/types/gist.d.ts +53 -0
- package/lib/types/groups.d.ts +34 -0
- package/lib/types/home-paths.d.ts +16 -0
- package/lib/types/hot.d.ts +221 -0
- package/lib/types/http.d.ts +12 -0
- package/lib/types/index.d.ts +14 -0
- package/lib/types/install.d.ts +172 -0
- package/lib/types/log.d.ts +43 -0
- package/lib/types/ndjson.d.ts +52 -0
- package/lib/types/net.d.ts +58 -0
- package/lib/types/order.d.ts +100 -0
- package/lib/types/patch.d.ts +129 -0
- package/lib/types/pnpm-compat.d.ts +93 -0
- package/lib/types/presets.d.ts +90 -0
- package/lib/types/profile.d.ts +253 -0
- package/lib/types/region-probe.d.ts +50 -0
- package/lib/types/regions.d.ts +122 -0
- package/lib/types/registry.d.ts +86 -0
- package/lib/types/restart.d.ts +194 -0
- package/lib/types/routes.d.ts +67 -0
- package/lib/types/settings.d.ts +78 -0
- package/lib/types/skill-market/api.d.ts +10 -0
- package/lib/types/skill-market/config.d.ts +3 -0
- package/lib/types/skill-market/installer.d.ts +23 -0
- package/lib/types/skill-market/runtime.d.ts +10 -0
- package/lib/types/skill-market/tui.d.ts +4 -0
- package/lib/types/skill-market/types.d.ts +71 -0
- package/lib/types/skill-market/web-routes.d.ts +19 -0
- package/lib/types/snapshot.d.ts +93 -0
- package/lib/types/source-migration.d.ts +11 -0
- package/lib/types/sources.d.ts +216 -0
- package/lib/types/store.d.ts +30 -0
- package/lib/types/themes.d.ts +40 -0
- package/lib/types/trial.d.ts +61 -0
- package/lib/types/update-api-v1.d.ts +66 -0
- package/lib/types/updates.d.ts +123 -0
- package/lib/types/verify.d.ts +139 -0
- package/lib/update-api-v1.js +215 -0
- package/lib/updates.js +361 -0
- package/lib/verify.js +453 -0
- package/package.json +121 -0
- package/src/accelerate.ts +213 -0
- package/src/agents.ts +43 -0
- package/src/backup.ts +583 -0
- package/src/catalog-local-match.ts +144 -0
- package/src/catalog-npm.ts +120 -0
- package/src/changelog.ts +282 -0
- package/src/channels.ts +70 -0
- package/src/check.ts +1239 -0
- package/src/client/CommentsModal.tsx +119 -0
- package/src/client/Diagnostics.tsx +907 -0
- package/src/client/ErrorBoundary.tsx +111 -0
- package/src/client/InstallToast.tsx +31 -0
- package/src/client/Market.module.css +830 -0
- package/src/client/MarketSection.tsx +5301 -0
- package/src/client/OperationsPanel.tsx +365 -0
- package/src/client/SettingsCard.tsx +621 -0
- package/src/client/SkillMarket.module.css +21 -0
- package/src/client/SkillMarketSection.tsx +162 -0
- package/src/client/comments.ts +54 -0
- package/src/client/globals.d.ts +13 -0
- package/src/client/index.ts +180 -0
- package/src/client/locales.ts +1112 -0
- package/src/client/market-data.ts +1321 -0
- package/src/client/operations.ts +201 -0
- package/src/client/preset-panel.tsx +263 -0
- package/src/client/primitives.d.ts +140 -0
- package/src/client/self-check.ts +147 -0
- package/src/client/snapshot-panel.tsx +244 -0
- package/src/compatibility.ts +237 -0
- package/src/diagnostics.ts +84 -0
- package/src/discovery-compatibility.ts +315 -0
- package/src/dsh-cli.ts +1126 -0
- package/src/dsh-install.ts +118 -0
- package/src/gist.ts +362 -0
- package/src/groups.ts +111 -0
- package/src/home-paths.ts +53 -0
- package/src/hot.ts +628 -0
- package/src/http.ts +41 -0
- package/src/index.ts +128 -0
- package/src/install.ts +420 -0
- package/src/log.ts +206 -0
- package/src/ndjson.ts +185 -0
- package/src/net.ts +106 -0
- package/src/order.ts +303 -0
- package/src/patch.ts +522 -0
- package/src/pnpm-compat.ts +527 -0
- package/src/presets.ts +344 -0
- package/src/profile.ts +940 -0
- package/src/region-probe.ts +97 -0
- package/src/regions.ts +310 -0
- package/src/registry.ts +250 -0
- package/src/restart.ts +396 -0
- package/src/routes.ts +4506 -0
- package/src/settings.ts +141 -0
- package/src/skill-market/api.ts +130 -0
- package/src/skill-market/config.ts +32 -0
- package/src/skill-market/installer.ts +337 -0
- package/src/skill-market/runtime.ts +14 -0
- package/src/skill-market/tui.ts +157 -0
- package/src/skill-market/types.ts +80 -0
- package/src/skill-market/web-routes.ts +132 -0
- package/src/snapshot.ts +532 -0
- package/src/source-migration.ts +61 -0
- package/src/sources.ts +565 -0
- package/src/store.ts +89 -0
- package/src/themes.ts +125 -0
- package/src/trial.ts +156 -0
- package/src/update-api-v1.ts +277 -0
- package/src/updates.ts +400 -0
- package/src/verify.ts +492 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState, useSyncExternalStore } from 'react'
|
|
2
|
+
import { Button, Input, Pill } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
3
|
+
import { api, type Translate } from './market-data.ts'
|
|
4
|
+
import css from './SkillMarket.module.css'
|
|
5
|
+
|
|
6
|
+
interface SkillItem {
|
|
7
|
+
readonly id: number
|
|
8
|
+
readonly name: string
|
|
9
|
+
readonly description: string
|
|
10
|
+
readonly category: string
|
|
11
|
+
readonly downloads: number
|
|
12
|
+
readonly version: string
|
|
13
|
+
readonly author: string
|
|
14
|
+
readonly creatorName: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ListResult {
|
|
18
|
+
readonly items: readonly SkillItem[]
|
|
19
|
+
readonly total: number
|
|
20
|
+
readonly page: number
|
|
21
|
+
readonly pageSize: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface InstalledSkill {
|
|
25
|
+
readonly name: string
|
|
26
|
+
readonly location: 'project' | 'user'
|
|
27
|
+
readonly path: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface SkillMarketSessions {
|
|
31
|
+
readonly list: {
|
|
32
|
+
subscribe(callback: () => void): () => void
|
|
33
|
+
getSnapshot(): {
|
|
34
|
+
readonly current?: string
|
|
35
|
+
readonly byId: Readonly<Record<string, { readonly cwd?: string }>>
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Browser skill catalog backed by the host's authoritative session workspace. */
|
|
41
|
+
export function SkillMarketSection({ t, sessions }: { readonly t: Translate; readonly sessions: SkillMarketSessions }) {
|
|
42
|
+
const subscribe = useMemo(() => (callback: () => void) => sessions.list.subscribe(callback), [sessions])
|
|
43
|
+
const getSnapshot = useCallback(() => sessions.list.getSnapshot(), [sessions])
|
|
44
|
+
const sessionList = useSyncExternalStore(subscribe, getSnapshot, getSnapshot)
|
|
45
|
+
const sessionId = sessionList.current
|
|
46
|
+
const cwd = sessionId === undefined ? undefined : sessionList.byId[sessionId]?.cwd
|
|
47
|
+
const [query, setQuery] = useState('')
|
|
48
|
+
const [deferredQuery, setDeferredQuery] = useState('')
|
|
49
|
+
const [page, setPage] = useState(1)
|
|
50
|
+
const [result, setResult] = useState<ListResult>({ items: [], total: 0, page: 1, pageSize: 12 })
|
|
51
|
+
const [installed, setInstalled] = useState<readonly InstalledSkill[]>([])
|
|
52
|
+
const [loading, setLoading] = useState(true)
|
|
53
|
+
const [working, setWorking] = useState<string | undefined>()
|
|
54
|
+
const [error, setError] = useState<string | undefined>()
|
|
55
|
+
const [notice, setNotice] = useState<string | undefined>()
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
const timer = setTimeout(() => { setDeferredQuery(query); setPage(1) }, 300)
|
|
59
|
+
return () => clearTimeout(timer)
|
|
60
|
+
}, [query])
|
|
61
|
+
|
|
62
|
+
const loadInstalled = useCallback(async () => {
|
|
63
|
+
if (sessionId === undefined || cwd === undefined) {
|
|
64
|
+
setInstalled([])
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
const response = await fetch(api(`dsh-market/skills/installed?sessionId=${encodeURIComponent(sessionId)}`))
|
|
68
|
+
const body = await readResponse<{ items: readonly InstalledSkill[] }>(response)
|
|
69
|
+
setInstalled(body.items)
|
|
70
|
+
}, [cwd, sessionId])
|
|
71
|
+
|
|
72
|
+
const refresh = useCallback(async () => {
|
|
73
|
+
setLoading(true)
|
|
74
|
+
setError(undefined)
|
|
75
|
+
const params = new URLSearchParams({ page: String(page), pageSize: '12', sort: 'latest' })
|
|
76
|
+
if (deferredQuery.trim()) params.set('search', deferredQuery.trim())
|
|
77
|
+
try {
|
|
78
|
+
const [catalog] = await Promise.all([
|
|
79
|
+
fetch(api(`dsh-market/skills?${params.toString()}`)).then(response => readResponse<ListResult>(response)),
|
|
80
|
+
loadInstalled(),
|
|
81
|
+
])
|
|
82
|
+
setResult(catalog)
|
|
83
|
+
} catch (cause) {
|
|
84
|
+
setError(cause instanceof Error ? cause.message : String(cause))
|
|
85
|
+
} finally {
|
|
86
|
+
setLoading(false)
|
|
87
|
+
}
|
|
88
|
+
}, [deferredQuery, loadInstalled, page])
|
|
89
|
+
|
|
90
|
+
useEffect(() => { void refresh() }, [refresh])
|
|
91
|
+
const installedNames = useMemo(() => new Set(installed.map(item => item.name)), [installed])
|
|
92
|
+
const pages = Math.max(1, Math.ceil(result.total / result.pageSize))
|
|
93
|
+
|
|
94
|
+
async function install(item: SkillItem, location: 'project' | 'user'): Promise<void> {
|
|
95
|
+
if (sessionId === undefined || cwd === undefined) return
|
|
96
|
+
setWorking(`${item.id}:${location}`)
|
|
97
|
+
setError(undefined)
|
|
98
|
+
setNotice(undefined)
|
|
99
|
+
try {
|
|
100
|
+
const response = await fetch(api('dsh-market/skills/install'), {
|
|
101
|
+
method: 'POST',
|
|
102
|
+
headers: { 'content-type': 'application/json' },
|
|
103
|
+
body: JSON.stringify({ id: item.id, name: item.name, version: item.version, location, sessionId }),
|
|
104
|
+
})
|
|
105
|
+
const installedResult = await readResponse<{ installPath: string }>(response)
|
|
106
|
+
setNotice(`${t('skillInstalledAt')} ${installedResult.installPath}`)
|
|
107
|
+
await loadInstalled()
|
|
108
|
+
} catch (cause) {
|
|
109
|
+
setError(cause instanceof Error ? cause.message : String(cause))
|
|
110
|
+
} finally {
|
|
111
|
+
setWorking(undefined)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return <section className={css.root}>
|
|
116
|
+
<header className={css.head}>
|
|
117
|
+
<div>
|
|
118
|
+
<h2 className={css.title}>{t('skillNav')}</h2>
|
|
119
|
+
<p className={css.subtitle}>{t('skillSubtitle')}</p>
|
|
120
|
+
</div>
|
|
121
|
+
<Input value={query} onChange={event => setQuery(event.currentTarget.value)} placeholder={t('skillSearch')} />
|
|
122
|
+
</header>
|
|
123
|
+
<p className={css.workspace}>{cwd === undefined ? t('skillNeedSession') : `${t('skillWorkspace')} ${cwd}`}</p>
|
|
124
|
+
<p className={css.warning}>{t('skillTrustWarning')}</p>
|
|
125
|
+
{error && <div className={css.error}>{error} <Button size="sm" variant="outline" onClick={() => void refresh()}>{t('skillRetry')}</Button></div>}
|
|
126
|
+
{notice && <div className={css.notice}>{notice}</div>}
|
|
127
|
+
{loading ? <p className={css.state}>{t('skillLoading')}</p> : result.items.length === 0 ? <p className={css.state}>{t('skillEmpty')}</p> : <div className={css.grid}>
|
|
128
|
+
{result.items.map(item => <article className={css.card} key={item.id}>
|
|
129
|
+
<div className={css.cardHead}>
|
|
130
|
+
<h3>{item.name}</h3>
|
|
131
|
+
{installedNames.has(item.name) && <Pill>{t('skillInstalled')}</Pill>}
|
|
132
|
+
</div>
|
|
133
|
+
<p className={css.description}>{item.description || t('skillNoDescription')}</p>
|
|
134
|
+
<p className={css.meta}>{item.version ? `v${item.version}` : ''}{item.author || item.creatorName ? ` · ${item.author || item.creatorName}` : ''}{` · ${item.downloads} ${t('skillDownloads')}`}</p>
|
|
135
|
+
<div className={css.actions}>
|
|
136
|
+
<Button className={css.projectInstall} size="sm" variant="primary" disabled={cwd === undefined || working !== undefined} onClick={() => void install(item, 'project')}>
|
|
137
|
+
{working === `${item.id}:project` ? t('skillInstalling') : t('skillInstallProject')}
|
|
138
|
+
</Button>
|
|
139
|
+
<Button size="sm" variant="outline" disabled={cwd === undefined || working !== undefined} onClick={() => void install(item, 'user')}>
|
|
140
|
+
{working === `${item.id}:user` ? t('skillInstalling') : t('skillInstallUser')}
|
|
141
|
+
</Button>
|
|
142
|
+
</div>
|
|
143
|
+
</article>)}
|
|
144
|
+
</div>}
|
|
145
|
+
<footer className={css.pager}>
|
|
146
|
+
<Button size="sm" variant="outline" disabled={page <= 1 || loading} onClick={() => setPage(value => value - 1)}>{t('skillPrevious')}</Button>
|
|
147
|
+
<span>{page} / {pages} · {result.total}</span>
|
|
148
|
+
<Button size="sm" variant="outline" disabled={page >= pages || loading} onClick={() => setPage(value => value + 1)}>{t('skillNext')}</Button>
|
|
149
|
+
</footer>
|
|
150
|
+
</section>
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function readResponse<T>(response: Response): Promise<T> {
|
|
154
|
+
const body = await response.json() as unknown
|
|
155
|
+
if (!response.ok) {
|
|
156
|
+
const message = typeof body === 'object' && body !== null && 'error' in body && typeof body.error === 'string'
|
|
157
|
+
? body.error
|
|
158
|
+
: `HTTP ${response.status}`
|
|
159
|
+
throw new Error(message)
|
|
160
|
+
}
|
|
161
|
+
return body as T
|
|
162
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* giscus wiring for the in-market comment thread.
|
|
3
|
+
*
|
|
4
|
+
* A plugin has ONE discussion, whether the reader reached it from
|
|
5
|
+
* dshmarket.com, from the awesome-dsh-plugin catalog, or from inside the
|
|
6
|
+
* market itself. That is only true as long as three things agree across the
|
|
7
|
+
* three surfaces: the repository ids below, the `plugin:<slug>` term, and the
|
|
8
|
+
* slug those terms are built from. The ids here mirror site/comments.mjs and
|
|
9
|
+
* the slug mirrors scripts/build-site.mjs; both are asserted in
|
|
10
|
+
* tests/client/comments.client.spec.ts against the real files, because a
|
|
11
|
+
* silent disagreement does not fail — it forks the conversation in half and
|
|
12
|
+
* nobody notices until someone asks why their comment vanished.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export const GISCUS = Object.freeze({
|
|
16
|
+
repo: 'awesome-dsh-plugin/awesome-dsh-plugin',
|
|
17
|
+
repoId: 'R_kgDOT3ajCQ',
|
|
18
|
+
category: 'Plugins',
|
|
19
|
+
categoryId: 'DIC_kwDOT3ajCc4DES8_',
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The plugin's slug, derived from its GitHub URL exactly as the two site
|
|
24
|
+
* builders derive it. Entries that live in a subdirectory of a monorepo
|
|
25
|
+
* (`.../tree/<ref>/packages/x`) get the `owner/repo--packages-x` form, so two
|
|
26
|
+
* plugins from one repository do not share a thread.
|
|
27
|
+
*/
|
|
28
|
+
export function pluginSlug(url: string): string {
|
|
29
|
+
const repoPath = url.replace('https://github.com/', '')
|
|
30
|
+
const repo = repoPath.split('/').slice(0, 2).join('/')
|
|
31
|
+
const sub = repoPath.includes('/tree/')
|
|
32
|
+
? repoPath.split('/tree/')[1]!.replace(/^[^/]+\//, '')
|
|
33
|
+
: null
|
|
34
|
+
return sub ? `${repo}--${sub.replaceAll('/', '-')}` : repo
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The giscus discussion term for a plugin. */
|
|
38
|
+
export const commentsTerm = (url: string): string => `plugin:${pluginSlug(url)}`
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* GitHub's discussion search narrowed to this plugin's exact shared term.
|
|
42
|
+
*
|
|
43
|
+
* This is deliberately composed from constants and the catalog URL only.
|
|
44
|
+
* In particular it never copies the current page URL: a local ChatCode CLI Web page
|
|
45
|
+
* may carry its one-time host token there, and sending that to GitHub would
|
|
46
|
+
* trade a comment-login inconvenience for credential disclosure.
|
|
47
|
+
*/
|
|
48
|
+
export function commentsDiscussionUrl(url: string): string {
|
|
49
|
+
const term = `"${commentsTerm(url)}"`
|
|
50
|
+
return `https://github.com/${GISCUS.repo}/discussions?discussions_q=${encodeURIComponent(term)}`
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** giscus's own locale codes, which are not the ones this plugin uses. */
|
|
54
|
+
export const giscusLang = (lang: string): string => (lang === 'zh' ? 'zh-CN' : 'en')
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Ambient declarations for the browser client bundle. */
|
|
2
|
+
|
|
3
|
+
declare module '*.module.css' {
|
|
4
|
+
const classes: Record<string, string>
|
|
5
|
+
export default classes
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface Window {
|
|
9
|
+
/** Boot manifest written by the host page for bundle-layer plugins. */
|
|
10
|
+
__DSH_BOOT__?: {
|
|
11
|
+
entries?: Array<{ id: string }>
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-market client: registers a "Market" settings section rendering the
|
|
3
|
+
* plugin market UI, plus the post-install toast in the shell overlay layer.
|
|
4
|
+
* Built by tsdown into the __ModuleLoader__ factory bundle at
|
|
5
|
+
* client/client.js; the only externals are the loader module table's react
|
|
6
|
+
* entries.
|
|
7
|
+
*/
|
|
8
|
+
import { createElement as h } from 'react'
|
|
9
|
+
import * as primitives from '@deepseek-ai/dsh-client-ui-primitives'
|
|
10
|
+
import { en, zh } from './locales.ts'
|
|
11
|
+
import { InstallToast } from './InstallToast.tsx'
|
|
12
|
+
import { MarketErrorBoundary } from './ErrorBoundary.tsx'
|
|
13
|
+
import { MarketSection } from './MarketSection.tsx'
|
|
14
|
+
import { exportMarketLog } from './self-check.ts'
|
|
15
|
+
import { SettingsCard } from './SettingsCard.tsx'
|
|
16
|
+
import type { ThemeSnapshot, Translate } from './market-data.ts'
|
|
17
|
+
import { SkillMarketSection, type SkillMarketSessions } from './SkillMarketSection.tsx'
|
|
18
|
+
|
|
19
|
+
const NS = 'dsh-market'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Primitives this bundle relies on that did not exist before rc.6. The
|
|
23
|
+
* primitives module is host-injected (external at build time), so on an
|
|
24
|
+
* older host the module resolves but these named exports are undefined —
|
|
25
|
+
* rendering would throw and blank the whole settings dialog. Returning the
|
|
26
|
+
* gaps lets apply() skip registration for a clean downgrade instead.
|
|
27
|
+
*/
|
|
28
|
+
export const REQUIRED_PRIMITIVES = ['Menu', 'DisclosureRow', 'Tooltip', 'Toast'] as const
|
|
29
|
+
|
|
30
|
+
export function missingPrimitives(mod: Record<string, unknown>, required: readonly string[] = REQUIRED_PRIMITIVES): string[] {
|
|
31
|
+
return required.filter(name => mod[name] === undefined)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The host surface the settings card needs, present only on rc.7+.
|
|
36
|
+
*
|
|
37
|
+
* The card no longer reads or writes settings — it manages the market's own
|
|
38
|
+
* package — but `settingsScope` stays as the INJECTION KEY, because its
|
|
39
|
+
* presence is what distinguishes a host that has the plugin configuration
|
|
40
|
+
* page from one that does not. The market's namespace (registered in
|
|
41
|
+
* settings.ts) is likewise still required: the page dispatches a card keyed
|
|
42
|
+
* by a namespace it serves, so dropping it would take the card with it.
|
|
43
|
+
*/
|
|
44
|
+
interface SettingsScopeHost {
|
|
45
|
+
slots: {
|
|
46
|
+
inject(name: string, register: () => unknown): void
|
|
47
|
+
register(options: Record<string, unknown>, render: () => unknown): unknown
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The subset of the theme service this plugin touches. */
|
|
52
|
+
interface ThemeService {
|
|
53
|
+
getTheme(): ThemeSnapshot | null
|
|
54
|
+
setTheme(id: string): void
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The subset of the locale service this plugin touches. */
|
|
58
|
+
interface LocaleService {
|
|
59
|
+
register(namespace: string, dicts: { zh: Record<string, string>; en: Record<string, string> }): unknown
|
|
60
|
+
bind(namespace: string): Translate
|
|
61
|
+
subscribe(callback: () => void): () => void
|
|
62
|
+
getSnapshot(): { active: string }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The subset of the slots service this plugin touches. */
|
|
66
|
+
interface SlotsService {
|
|
67
|
+
inject(slot: string, register: () => unknown): void
|
|
68
|
+
register(meta: Record<string, unknown>, component: () => unknown): unknown
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The client cordis context shape this plugin relies on (structural: the
|
|
72
|
+
* host provides the real Context; typing the touched surface keeps this
|
|
73
|
+
* external package free of monorepo-internal type dependencies). */
|
|
74
|
+
interface MarketClientContext {
|
|
75
|
+
effect(callback: () => unknown, label?: string): void
|
|
76
|
+
on(event: string, callback: () => void): () => void
|
|
77
|
+
locale: LocaleService
|
|
78
|
+
slots: SlotsService
|
|
79
|
+
theme: ThemeService
|
|
80
|
+
sessions: SkillMarketSessions
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const name = 'dsh-market'
|
|
84
|
+
// 'theme' is safe to require: ui-layout (mandatory in every web composition)
|
|
85
|
+
// already hard-depends on it. This cordis's object-form inject means
|
|
86
|
+
// intercept config, NOT {required,optional} — do not use it here.
|
|
87
|
+
export const inject = ['slots', 'locale', 'theme', 'sessions']
|
|
88
|
+
export function apply(ctx: MarketClientContext): void {
|
|
89
|
+
// Older hosts resolve the primitives module but lack the rc.6 exports the
|
|
90
|
+
// market renders with. Skip registration (market simply absent from the
|
|
91
|
+
// settings list) rather than throwing mid-render and blanking the dialog.
|
|
92
|
+
const gaps = missingPrimitives(primitives as unknown as Record<string, unknown>)
|
|
93
|
+
if (gaps.length > 0) {
|
|
94
|
+
console.warn('[ChatCode CLI Market] host ui-primitives missing ' + gaps.join(', ') + ' — market section disabled (ChatCode CLI >= 0.1.0-rc.6 required)')
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-market: dictionaries')
|
|
99
|
+
const t = ctx.locale.bind(NS)
|
|
100
|
+
|
|
101
|
+
// Kept so the removal flow can retire the market's own nav entry the
|
|
102
|
+
// moment the package is gone: leaving "插件市场" in the left menu after
|
|
103
|
+
// the user removed it is the card claiming something the profile no
|
|
104
|
+
// longer agrees with. `register` hands back its own disposer; calling it
|
|
105
|
+
// twice (here and again when the context unwinds) is harmless, but the
|
|
106
|
+
// reference is dropped after use so the intent stays readable.
|
|
107
|
+
let retireSection: (() => void) | null = null
|
|
108
|
+
|
|
109
|
+
ctx.slots.inject('settings.section', () => {
|
|
110
|
+
const off = ctx.slots.register({
|
|
111
|
+
name: 'settings.section',
|
|
112
|
+
id: 'market',
|
|
113
|
+
order: 40,
|
|
114
|
+
label: () => t('nav'),
|
|
115
|
+
locale: NS,
|
|
116
|
+
inject: () => ({ t }),
|
|
117
|
+
}, (ownerProps: { preferredSubsectionId?: string } = {}) => h(MarketErrorBoundary, {
|
|
118
|
+
// Wrapped at the registration point, so the boundary is OUTSIDE
|
|
119
|
+
// everything the section renders — including its portalled layers.
|
|
120
|
+
// A crash inside used to unmount the whole tree and leave an empty
|
|
121
|
+
// settings panel with no export-log button, which is how #293 went
|
|
122
|
+
// months without a usable report (#513 fixed that trigger; this
|
|
123
|
+
// covers the next one).
|
|
124
|
+
text: {
|
|
125
|
+
title: t('crashTitle'),
|
|
126
|
+
hint: t('crashHint'),
|
|
127
|
+
reload: t('crashReload'),
|
|
128
|
+
details: t('crashDetails'),
|
|
129
|
+
},
|
|
130
|
+
actions: h('button', {
|
|
131
|
+
type: 'button',
|
|
132
|
+
onClick: () => { void exportMarketLog().catch(() => {}) },
|
|
133
|
+
}, t('exportLog')),
|
|
134
|
+
}, h(MarketSection, {
|
|
135
|
+
t,
|
|
136
|
+
locale: ctx.locale,
|
|
137
|
+
theme: ctx.theme,
|
|
138
|
+
themeStore: {
|
|
139
|
+
subscribe: (cb: () => void) => ctx.on('theme/change', cb),
|
|
140
|
+
getSnapshot: () => ctx.theme.getTheme(),
|
|
141
|
+
},
|
|
142
|
+
preferredSubsectionId: ownerProps.preferredSubsectionId,
|
|
143
|
+
})))
|
|
144
|
+
if (typeof off === 'function') retireSection = off as () => void
|
|
145
|
+
return off
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
ctx.slots.inject('settings.section', () => ctx.slots.register({
|
|
149
|
+
name: 'settings.section',
|
|
150
|
+
id: 'skill-market',
|
|
151
|
+
order: 41,
|
|
152
|
+
label: () => t('skillNav'),
|
|
153
|
+
locale: NS,
|
|
154
|
+
inject: () => ({ t }),
|
|
155
|
+
}, () => h(SkillMarketSection, { t, sessions: ctx.sessions })))
|
|
156
|
+
|
|
157
|
+
// The settings card (dsh >= 0.1.0-rc.7). Registered through a NESTED
|
|
158
|
+
// inject on purpose: naming settingsScope in the module-level `inject`
|
|
159
|
+
// would keep this whole plugin unmounted on any host without that
|
|
160
|
+
// service — the market's own page would vanish on rc.6 to gain a card
|
|
161
|
+
// rc.6 cannot render. Nested, the card simply never appears there.
|
|
162
|
+
const settingsCtx = ctx as unknown as {
|
|
163
|
+
inject(services: string[], callback: (scoped: SettingsScopeHost) => void): void
|
|
164
|
+
}
|
|
165
|
+
settingsCtx.inject(['settingsScope'], (scoped) => {
|
|
166
|
+
scoped.slots.inject('settings.plugin.item', () => scoped.slots.register({
|
|
167
|
+
name: 'settings.plugin.item',
|
|
168
|
+
key: NS,
|
|
169
|
+
locale: NS,
|
|
170
|
+
inject: () => ({ t }),
|
|
171
|
+
}, () => h(SettingsCard, { t, onRemoved: () => { const off = retireSection; retireSection = null; off?.() } })))
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
const Toast = () => h(InstallToast, { t })
|
|
175
|
+
ctx.slots.inject('shell.overlay', () => ctx.slots.register({
|
|
176
|
+
name: 'shell.overlay',
|
|
177
|
+
id: 'dsh-market-toast',
|
|
178
|
+
label: () => 'dsh-market',
|
|
179
|
+
}, Toast))
|
|
180
|
+
}
|