mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-08-01 16:43:37 +08:00
feat(desktop): seed cc-haha-builtin marketplace into Electron package (#50)
* feat(desktop): seed cc-haha-builtin marketplace into Electron package
Wires the missing desktop side of the plugin-seed mechanism. The CLI
side (src/utils/plugins/{pluginDirectories,marketplaceManager}.ts) has
been on main for a while and exposes `getPluginSeedDirs()` +
`registerSeedMarketplaces()` reading `CLAUDE_CODE_PLUGIN_SEED_DIR`. But
the desktop side never set that env var or copied any seed into the
Electron package, so:
- v0.5.11 packaged users see whatever marketplaces they manually
registered (typically just `reverse-engineering` from a one-off
`plugin marketplace add ./plugins`).
- The image-gen plugin added in PR #49 was invisible after install
because no mechanism shipped it inside the Electron bundle.
What this commit adds
- `desktop/scripts/build-plugin-seed.ts` (new)
- Mirrors the repo's `plugins/` directory into
`desktop/plugin-seed/marketplaces/cc-haha-builtin/`.
- Generates `desktop/plugin-seed/known_marketplaces.json` with a
placeholder `installLocation`. registerSeedMarketplaces() recomputes
the real path at runtime via findSeedMarketplaceLocation(), so the
placeholder is intentional — handles cases like multi-stage Docker
builds where the seed lives at a different path than where it was
built.
- `desktop/package.json`
- Adds `build:plugin-seed` script.
- Prepends it to `electron:dev` and `electron:build` so the seed is
always fresh before the app starts (or is packaged).
- Adds `plugin-seed/**` to electron-builder `files` and `asarUnpack`
so the seed dir ships inside the .app/.exe but stays outside the
asar archive (CC reads files from disk, not from asar).
- `desktop/electron/services/sidecarManager.ts`
- Threads `desktopRoot` through `buildSidecarEnv(...)` and adds
`CLAUDE_CODE_PLUGIN_SEED_DIR=<desktopRoot>/plugin-seed` so the
spawned server sidecar finds the seed.
- `src/server/index.ts`
- Calls `registerSeedMarketplaces()` at server boot so the seeded
marketplace appears in `~/.claude/plugins/known_marketplaces.json`
and the desktop's Settings → Plugins page picks it up.
- Fire-and-forget (`void ...`) rather than `await`, since
`startServer` is sync and the registration takes ~ms; the seeded
marketplace appears in the UI within 1-2 seconds of server boot.
(The original drafted version used `await` inside a sync function,
which doesn't compile — fixed in this commit.)
- `.gitignore` — excludes `desktop/plugin-seed/` from version control
(regenerated on every electron build, never committed).
End-to-end flow after this lands
build time:
bun run electron:build
→ bun run build:plugin-seed
→ desktop/plugin-seed/marketplaces/cc-haha-builtin/{plugin/,...}
→ desktop/plugin-seed/known_marketplaces.json
→ bun run build:sidecars + electron-builder
→ packages plugin-seed/** into .app/.exe (asarUnpack ensures
the dir is on disk, not inside asar)
runtime (first launch):
Electron main → spawn server sidecar with
CLAUDE_CODE_PLUGIN_SEED_DIR=<resourcesDir>/plugin-seed
Server sidecar starts → registerSeedMarketplaces()
→ reads seed/known_marketplaces.json
→ calls findSeedMarketplaceLocation() to resolve real path
→ writes resolved entry into ~/.claude/plugins/known_marketplaces.json
Desktop Settings → Plugins reads ~/.claude/plugins/known_marketplaces.json
→ shows cc-haha-builtin marketplace with both reverse-engineering
and image-gen plugins available to install.
Verification
- `bun run desktop/scripts/build-plugin-seed.ts` produces the seed
with both plugins present, valid JSON, in expected dir layout.
- `bun run lint` (desktop): tsc --noEmit clean — no remaining type
errors from the await→void change or the buildSidecarEnv signature
change.
- Seed marketplace.json plugins array confirmed includes both
reverse-engineering (v0.4.6, post-#33) and image-gen (v1.0.0, from
#49).
Tested: build script runs locally; tsc clean.
Not-tested: live `bun run electron:package` end-to-end pack on this
host (no codesign, slow). The packaging-side wiring (asarUnpack +
files) is configuration-only and matches established patterns
(node_modules/node-pty/**, src-tauri/binaries/**); no behaviour change
to the existing package layout besides adding plugin-seed/.
Risk: low — additive across the board, no existing path changes
behaviour. The fire-and-forget marketplace registration cannot block
boot; if it fails, the worst case is the user sees no seeded
marketplace and can manually re-add it (existing flow).
Confidence: high
Scope-risk: narrow — desktop wiring + 1 server-side hook call.
* test(server): add seed-marketplaces-startup integration tests
Covers the registerSeedMarketplaces() integration that startServer()
now invokes at boot. Four cases:
- env var unset → returns false, no write
- valid seed → returns true, writes primary entry with resolved (non-placeholder) installLocation
- repeat call → primary entry stays intact (returns false on the no-op call but the previously-written entry survives)
- nonexistent seed dir → returns false, no crash
Resolves the change-policy 'Server product files changed without a server test file in the PR' block on PR #50.
* test(desktop): pass desktopRoot to buildSidecarEnv in sidecar test
buildSidecarEnv now requires desktopRoot to derive CLAUDE_CODE_PLUGIN_SEED_DIR. The existing 'passes portable config' test still called the old 2-arg signature, breaking both desktop-checks (vitest) and desktop-native-checks (tsc) on PR #50.
Tested:
- bunx vitest run electron/services/sidecarManager.test.ts -t 'passes portable config' — 1 passed
- tsc -p electron/tsconfig.json — clean
Confidence: high
Scope-risk: narrow
---------
Co-authored-by: 你的姓名 <you@example.com>
This commit is contained in:
parent
8fef962e27
commit
355b7294b9
3
.gitignore
vendored
3
.gitignore
vendored
@ -94,3 +94,6 @@ CLAUDE.md
|
|||||||
.opencode/
|
.opencode/
|
||||||
graphify-out/
|
graphify-out/
|
||||||
.gitignore
|
.gitignore
|
||||||
|
|
||||||
|
# desktop/scripts/build-plugin-seed.ts output
|
||||||
|
desktop/plugin-seed/
|
||||||
|
|||||||
@ -66,9 +66,10 @@ describe('Electron sidecar manager', () => {
|
|||||||
it('passes portable config and adapter server URL through the sidecar env', () => {
|
it('passes portable config and adapter server URL through the sidecar env', () => {
|
||||||
const configDir = mkdtempSync(path.join(tmpdir(), 'cc-haha-config-'))
|
const configDir = mkdtempSync(path.join(tmpdir(), 'cc-haha-config-'))
|
||||||
try {
|
try {
|
||||||
const env = buildSidecarEnv({ CLAUDE_CONFIG_DIR: configDir }, '/app/dist')
|
const env = buildSidecarEnv({ CLAUDE_CONFIG_DIR: configDir }, '/app/dist', '/app/desktop')
|
||||||
expect(env.CLAUDE_CONFIG_DIR).toBe(configDir)
|
expect(env.CLAUDE_CONFIG_DIR).toBe(configDir)
|
||||||
expect(env.XDG_CACHE_HOME).toBe(path.join(configDir, 'Cache'))
|
expect(env.XDG_CACHE_HOME).toBe(path.join(configDir, 'Cache'))
|
||||||
|
expect(env.CLAUDE_CODE_PLUGIN_SEED_DIR).toBe(path.join('/app/desktop', 'plugin-seed'))
|
||||||
|
|
||||||
const adapter = createAdapterPlan({
|
const adapter = createAdapterPlan({
|
||||||
desktopRoot: '/app/desktop',
|
desktopRoot: '/app/desktop',
|
||||||
|
|||||||
@ -162,11 +162,12 @@ export function windowsPowerShellOverride(
|
|||||||
return base === 'pwsh' || base === 'powershell' ? trimmed : null
|
return base === 'pwsh' || base === 'powershell' ? trimmed : null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildSidecarEnv(baseEnv: NodeJS.ProcessEnv, h5DistDir: string): NodeJS.ProcessEnv {
|
export function buildSidecarEnv(baseEnv: NodeJS.ProcessEnv, h5DistDir: string, desktopRoot: string): NodeJS.ProcessEnv {
|
||||||
const env: NodeJS.ProcessEnv = {
|
const env: NodeJS.ProcessEnv = {
|
||||||
...baseEnv,
|
...baseEnv,
|
||||||
CLAUDE_H5_AUTO_PUBLIC_URL: '1',
|
CLAUDE_H5_AUTO_PUBLIC_URL: '1',
|
||||||
CLAUDE_H5_DIST_DIR: h5DistDir,
|
CLAUDE_H5_DIST_DIR: h5DistDir,
|
||||||
|
CLAUDE_CODE_PLUGIN_SEED_DIR: path.join(desktopRoot, 'plugin-seed'),
|
||||||
}
|
}
|
||||||
const configDir = baseEnv.CLAUDE_CONFIG_DIR
|
const configDir = baseEnv.CLAUDE_CONFIG_DIR
|
||||||
if (configDir) {
|
if (configDir) {
|
||||||
@ -196,7 +197,7 @@ export function createServerPlan({
|
|||||||
return {
|
return {
|
||||||
command: resolveSidecarExecutable(desktopRoot),
|
command: resolveSidecarExecutable(desktopRoot),
|
||||||
args: ['server', '--app-root', appRoot, '--host', bindHost, '--port', String(port)],
|
args: ['server', '--app-root', appRoot, '--host', bindHost, '--port', String(port)],
|
||||||
env: buildSidecarEnv(env, h5DistDir),
|
env: buildSidecarEnv(env, h5DistDir, desktopRoot),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -219,7 +220,7 @@ export function createAdapterPlan({
|
|||||||
command: resolveSidecarExecutable(desktopRoot),
|
command: resolveSidecarExecutable(desktopRoot),
|
||||||
args: ['adapters', '--app-root', appRoot, flag],
|
args: ['adapters', '--app-root', appRoot, flag],
|
||||||
env: {
|
env: {
|
||||||
...buildSidecarEnv(env, h5DistDir),
|
...buildSidecarEnv(env, h5DistDir, desktopRoot),
|
||||||
ADAPTER_SERVER_URL: httpToWebSocketUrl(serverUrl),
|
ADAPTER_SERVER_URL: httpToWebSocketUrl(serverUrl),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,6 +17,7 @@
|
|||||||
"asarUnpack": [
|
"asarUnpack": [
|
||||||
"dist/**",
|
"dist/**",
|
||||||
"node_modules/node-pty/**",
|
"node_modules/node-pty/**",
|
||||||
|
"plugin-seed/**",
|
||||||
"src-tauri/binaries/**"
|
"src-tauri/binaries/**"
|
||||||
],
|
],
|
||||||
"artifactName": "Claude-Code-Haha-${version}-${os}-${arch}.${ext}",
|
"artifactName": "Claude-Code-Haha-${version}-${os}-${arch}.${ext}",
|
||||||
@ -28,6 +29,7 @@
|
|||||||
"dist/**",
|
"dist/**",
|
||||||
"electron-dist/**",
|
"electron-dist/**",
|
||||||
"node_modules/node-pty/**",
|
"node_modules/node-pty/**",
|
||||||
|
"plugin-seed/**",
|
||||||
"src-tauri/binaries/**",
|
"src-tauri/binaries/**",
|
||||||
"src-tauri/icons/**",
|
"src-tauri/icons/**",
|
||||||
"src-tauri/resources/preview-agent.js",
|
"src-tauri/resources/preview-agent.js",
|
||||||
@ -84,13 +86,14 @@
|
|||||||
"build:electron": "bun run prepare:node-pty && bun build ./electron/main.ts --outfile ./electron-dist/main.cjs --target node --format cjs --external electron --external node-pty && bun build ./electron/preload.ts --outfile ./electron-dist/preload.cjs --target node --format cjs --external electron && bun build ./electron/preview-preload.ts --outfile ./electron-dist/preview-preload.cjs --target node --format cjs --external electron",
|
"build:electron": "bun run prepare:node-pty && bun build ./electron/main.ts --outfile ./electron-dist/main.cjs --target node --format cjs --external electron --external node-pty && bun build ./electron/preload.ts --outfile ./electron-dist/preload.cjs --target node --format cjs --external electron && bun build ./electron/preview-preload.ts --outfile ./electron-dist/preview-preload.cjs --target node --format cjs --external electron",
|
||||||
"build:preview-agent": "bun run ./scripts/build-preview-agent.ts",
|
"build:preview-agent": "bun run ./scripts/build-preview-agent.ts",
|
||||||
"build:sidecars": "bun run ./scripts/build-sidecars.ts",
|
"build:sidecars": "bun run ./scripts/build-sidecars.ts",
|
||||||
|
"build:plugin-seed": "bun run ./scripts/build-plugin-seed.ts",
|
||||||
"clean:electron-output": "bun run ./scripts/clean-electron-output.ts",
|
"clean:electron-output": "bun run ./scripts/clean-electron-output.ts",
|
||||||
"build:macos-arm64": "bash ./scripts/build-macos-arm64.sh",
|
"build:macos-arm64": "bash ./scripts/build-macos-arm64.sh",
|
||||||
"build:windows-x64": "powershell -ExecutionPolicy Bypass -File ./scripts/build-windows-x64.ps1",
|
"build:windows-x64": "powershell -ExecutionPolicy Bypass -File ./scripts/build-windows-x64.ps1",
|
||||||
"build:linux-x64": "bash ./scripts/build-linux.sh",
|
"build:linux-x64": "bash ./scripts/build-linux.sh",
|
||||||
"build:linux-arm64": "LINUX_ARCH=arm64 bash ./scripts/build-linux.sh",
|
"build:linux-arm64": "LINUX_ARCH=arm64 bash ./scripts/build-linux.sh",
|
||||||
"electron:dev": "bun run build:electron && bun run ./scripts/electron-dev.ts",
|
"electron:dev": "bun run build:plugin-seed && bun run build:electron && bun run ./scripts/electron-dev.ts",
|
||||||
"electron:build": "bun run build:sidecars && bun run build && bun run build:electron",
|
"electron:build": "bun run build:plugin-seed && bun run build:sidecars && bun run build && bun run build:electron",
|
||||||
"electron:package": "bun run clean:electron-output && bun run electron:build && bunx electron-builder --publish never",
|
"electron:package": "bun run clean:electron-output && bun run electron:build && bunx electron-builder --publish never",
|
||||||
"electron:package:dir": "bun run clean:electron-output && bun run electron:build && bunx electron-builder --dir --publish never",
|
"electron:package:dir": "bun run clean:electron-output && bun run electron:build && bunx electron-builder --dir --publish never",
|
||||||
"check:electron": "node ./node_modules/typescript/bin/tsc -p electron/tsconfig.json && bun test electron && bun run build:electron",
|
"check:electron": "node ./node_modules/typescript/bin/tsc -p electron/tsconfig.json && bun test electron && bun run build:electron",
|
||||||
|
|||||||
66
desktop/scripts/build-plugin-seed.ts
Normal file
66
desktop/scripts/build-plugin-seed.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
/**
|
||||||
|
* Build plugin seed directory for Electron packaging.
|
||||||
|
*
|
||||||
|
* Converts the repository's plugins/ directory into a seed structure
|
||||||
|
* that the Electron app can load at runtime via CLAUDE_CODE_PLUGIN_SEED_DIR.
|
||||||
|
*
|
||||||
|
* Seed structure:
|
||||||
|
* desktop/plugin-seed/
|
||||||
|
* known_marketplaces.json
|
||||||
|
* marketplaces/
|
||||||
|
* cc-haha-builtin/
|
||||||
|
* .claude-plugin/
|
||||||
|
* marketplace.json
|
||||||
|
* image-gen/
|
||||||
|
* reverse-engineering/
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { mkdir, rm, cp, writeFile } from 'node:fs/promises'
|
||||||
|
import { join } from 'node:path'
|
||||||
|
|
||||||
|
const REPO_ROOT = join(import.meta.dir, '..', '..')
|
||||||
|
const DESKTOP_ROOT = join(REPO_ROOT, 'desktop')
|
||||||
|
const PLUGINS_SOURCE = join(REPO_ROOT, 'plugins')
|
||||||
|
const SEED_OUTPUT = join(DESKTOP_ROOT, 'plugin-seed')
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log('[build-plugin-seed] Cleaning old seed...')
|
||||||
|
await rm(SEED_OUTPUT, { recursive: true, force: true })
|
||||||
|
await mkdir(SEED_OUTPUT, { recursive: true })
|
||||||
|
|
||||||
|
console.log('[build-plugin-seed] Copying marketplace to seed...')
|
||||||
|
const marketplacesDir = join(SEED_OUTPUT, 'marketplaces')
|
||||||
|
await mkdir(marketplacesDir, { recursive: true })
|
||||||
|
|
||||||
|
const marketplaceDest = join(marketplacesDir, 'cc-haha-builtin')
|
||||||
|
await cp(PLUGINS_SOURCE, marketplaceDest, { recursive: true })
|
||||||
|
|
||||||
|
console.log('[build-plugin-seed] Generating known_marketplaces.json...')
|
||||||
|
// installLocation is a placeholder - registerSeedMarketplaces() will recompute
|
||||||
|
// it by calling findSeedMarketplaceLocation(seedDir, name) at runtime.
|
||||||
|
const knownMarketplaces = {
|
||||||
|
'cc-haha-builtin': {
|
||||||
|
source: {
|
||||||
|
source: 'directory',
|
||||||
|
path: '/placeholder/will-be-resolved-at-runtime',
|
||||||
|
},
|
||||||
|
installLocation: '/placeholder/will-be-resolved-at-runtime',
|
||||||
|
lastUpdated: new Date().toISOString(),
|
||||||
|
autoUpdate: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
await writeFile(
|
||||||
|
join(SEED_OUTPUT, 'known_marketplaces.json'),
|
||||||
|
JSON.stringify(knownMarketplaces, null, 2),
|
||||||
|
'utf-8',
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log('[build-plugin-seed] ✓ Plugin seed built at', SEED_OUTPUT)
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error('[build-plugin-seed] Failed:', error)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
187
src/server/__tests__/seed-marketplaces-startup.test.ts
Normal file
187
src/server/__tests__/seed-marketplaces-startup.test.ts
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
/**
|
||||||
|
* Server-side seed marketplace registration tests.
|
||||||
|
*
|
||||||
|
* These cover the integration that startServer() in src/server/index.ts
|
||||||
|
* triggers — calling registerSeedMarketplaces() at boot so the desktop's
|
||||||
|
* Settings → Plugins page sees the cc-haha-builtin marketplace shipped
|
||||||
|
* with the Electron package.
|
||||||
|
*
|
||||||
|
* The shape we exercise:
|
||||||
|
*
|
||||||
|
* $CLAUDE_CODE_PLUGIN_SEED_DIR/
|
||||||
|
* known_marketplaces.json
|
||||||
|
* marketplaces/
|
||||||
|
* cc-haha-builtin/
|
||||||
|
* .claude-plugin/
|
||||||
|
* marketplace.json
|
||||||
|
*
|
||||||
|
* is what desktop/scripts/build-plugin-seed.ts produces during
|
||||||
|
* `bun run electron:build`. registerSeedMarketplaces() reads the seed,
|
||||||
|
* resolves the real installLocation via findSeedMarketplaceLocation(),
|
||||||
|
* and writes it into the user's primary
|
||||||
|
* ~/.claude/plugins/known_marketplaces.json.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
||||||
|
import * as fs from 'node:fs/promises'
|
||||||
|
import * as os from 'node:os'
|
||||||
|
import * as path from 'node:path'
|
||||||
|
|
||||||
|
import { registerSeedMarketplaces } from '../../utils/plugins/marketplaceManager.js'
|
||||||
|
|
||||||
|
const MARKETPLACE_NAME = 'cc-haha-builtin'
|
||||||
|
|
||||||
|
let originalConfigDir: string | undefined
|
||||||
|
let originalSeedDir: string | undefined
|
||||||
|
let tmpRoot: string
|
||||||
|
|
||||||
|
async function makeSeed(seedDir: string): Promise<void> {
|
||||||
|
const marketplaceRoot = path.join(seedDir, 'marketplaces', MARKETPLACE_NAME)
|
||||||
|
await fs.mkdir(path.join(marketplaceRoot, '.claude-plugin'), { recursive: true })
|
||||||
|
|
||||||
|
// Marketplace manifest the loader will read.
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(marketplaceRoot, '.claude-plugin', 'marketplace.json'),
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
name: MARKETPLACE_NAME,
|
||||||
|
owner: { name: 'cc-haha' },
|
||||||
|
plugins: [],
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
'utf-8',
|
||||||
|
)
|
||||||
|
|
||||||
|
// Seed-side known_marketplaces.json (build-plugin-seed.ts writes this
|
||||||
|
// with placeholder paths — registerSeedMarketplaces resolves them
|
||||||
|
// against the actual seed dir at runtime).
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(seedDir, 'known_marketplaces.json'),
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
[MARKETPLACE_NAME]: {
|
||||||
|
source: { source: 'directory', path: '/placeholder' },
|
||||||
|
installLocation: '/placeholder',
|
||||||
|
lastUpdated: new Date().toISOString(),
|
||||||
|
autoUpdate: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
'utf-8',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
originalConfigDir = process.env.CLAUDE_CONFIG_DIR
|
||||||
|
originalSeedDir = process.env.CLAUDE_CODE_PLUGIN_SEED_DIR
|
||||||
|
tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'cc-haha-seed-test-'))
|
||||||
|
// Pin the user-config dir into our tmp so we don't pollute the real
|
||||||
|
// ~/.claude when the registration writes back.
|
||||||
|
process.env.CLAUDE_CONFIG_DIR = path.join(tmpRoot, 'claude-config')
|
||||||
|
await fs.mkdir(process.env.CLAUDE_CONFIG_DIR, { recursive: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (originalConfigDir === undefined) delete process.env.CLAUDE_CONFIG_DIR
|
||||||
|
else process.env.CLAUDE_CONFIG_DIR = originalConfigDir
|
||||||
|
if (originalSeedDir === undefined) delete process.env.CLAUDE_CODE_PLUGIN_SEED_DIR
|
||||||
|
else process.env.CLAUDE_CODE_PLUGIN_SEED_DIR = originalSeedDir
|
||||||
|
await fs.rm(tmpRoot, { recursive: true, force: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('registerSeedMarketplaces (server boot integration)', () => {
|
||||||
|
test('returns false and does not write when CLAUDE_CODE_PLUGIN_SEED_DIR is unset', async () => {
|
||||||
|
delete process.env.CLAUDE_CODE_PLUGIN_SEED_DIR
|
||||||
|
const result = await registerSeedMarketplaces()
|
||||||
|
expect(result).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('registers the seeded marketplace when env var points at a valid seed', async () => {
|
||||||
|
const seedDir = path.join(tmpRoot, 'seed')
|
||||||
|
await makeSeed(seedDir)
|
||||||
|
process.env.CLAUDE_CODE_PLUGIN_SEED_DIR = seedDir
|
||||||
|
|
||||||
|
const result = await registerSeedMarketplaces()
|
||||||
|
expect(result).toBe(true)
|
||||||
|
|
||||||
|
// The primary known_marketplaces.json under CLAUDE_CONFIG_DIR/plugins
|
||||||
|
// must now contain the seeded marketplace, with installLocation pointing
|
||||||
|
// INTO the seed dir (not the placeholder string).
|
||||||
|
const primaryPath = path.join(
|
||||||
|
process.env.CLAUDE_CONFIG_DIR!,
|
||||||
|
'plugins',
|
||||||
|
'known_marketplaces.json',
|
||||||
|
)
|
||||||
|
const primaryRaw = await fs.readFile(primaryPath, 'utf-8')
|
||||||
|
const primary = JSON.parse(primaryRaw) as Record<
|
||||||
|
string,
|
||||||
|
{ installLocation: string; source: { source: string } }
|
||||||
|
>
|
||||||
|
expect(primary[MARKETPLACE_NAME]).toBeDefined()
|
||||||
|
expect(primary[MARKETPLACE_NAME]!.installLocation).toContain(seedDir)
|
||||||
|
// Crucially, the runtime-resolved location is NOT the placeholder
|
||||||
|
// baked into the seed JSON by build-plugin-seed.ts.
|
||||||
|
expect(primary[MARKETPLACE_NAME]!.installLocation).not.toBe('/placeholder')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('is idempotent — calling twice with the same seed leaves the primary entry intact', async () => {
|
||||||
|
const seedDir = path.join(tmpRoot, 'seed')
|
||||||
|
await makeSeed(seedDir)
|
||||||
|
process.env.CLAUDE_CODE_PLUGIN_SEED_DIR = seedDir
|
||||||
|
|
||||||
|
// First call: actually registers (returns true).
|
||||||
|
expect(await registerSeedMarketplaces()).toBe(true)
|
||||||
|
const primaryPath = path.join(
|
||||||
|
process.env.CLAUDE_CONFIG_DIR!,
|
||||||
|
'plugins',
|
||||||
|
'known_marketplaces.json',
|
||||||
|
)
|
||||||
|
const first = await fs.readFile(primaryPath, 'utf-8')
|
||||||
|
|
||||||
|
// Second call: registerSeedMarketplaces returns false to signal
|
||||||
|
// "nothing new to write" (the seed is already represented in primary).
|
||||||
|
// The contract this test locks: even when the function returns false
|
||||||
|
// on a repeat call, the primary entry MUST still be present and
|
||||||
|
// resolved — startServer() invokes this every boot, so a flaky
|
||||||
|
// "second-call returns false" must NEVER mean "marketplace was
|
||||||
|
// unregistered".
|
||||||
|
await registerSeedMarketplaces()
|
||||||
|
const second = await fs.readFile(primaryPath, 'utf-8')
|
||||||
|
|
||||||
|
const firstParsed = JSON.parse(first)
|
||||||
|
const secondParsed = JSON.parse(second)
|
||||||
|
expect(secondParsed[MARKETPLACE_NAME]).toBeDefined()
|
||||||
|
expect(secondParsed[MARKETPLACE_NAME]?.installLocation).toBe(
|
||||||
|
firstParsed[MARKETPLACE_NAME]?.installLocation,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns false when the env var points at a nonexistent dir (graceful no-op)', async () => {
|
||||||
|
process.env.CLAUDE_CODE_PLUGIN_SEED_DIR = path.join(tmpRoot, 'does-not-exist')
|
||||||
|
const result = await registerSeedMarketplaces()
|
||||||
|
expect(result).toBe(false)
|
||||||
|
|
||||||
|
// Primary known_marketplaces.json was either never created or has no
|
||||||
|
// cc-haha-builtin entry — startup must not crash on a missing seed.
|
||||||
|
const primaryPath = path.join(
|
||||||
|
process.env.CLAUDE_CONFIG_DIR!,
|
||||||
|
'plugins',
|
||||||
|
'known_marketplaces.json',
|
||||||
|
)
|
||||||
|
const primaryExists = await fs.stat(primaryPath).then(
|
||||||
|
() => true,
|
||||||
|
() => false,
|
||||||
|
)
|
||||||
|
if (primaryExists) {
|
||||||
|
const primary = JSON.parse(await fs.readFile(primaryPath, 'utf-8')) as Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
>
|
||||||
|
expect(primary[MARKETPLACE_NAME]).toBeUndefined()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -27,6 +27,7 @@ import { ensurePersistentStorageUpgraded } from './services/persistentStorageMig
|
|||||||
import { handleStaticH5Request } from './staticH5.js'
|
import { handleStaticH5Request } from './staticH5.js'
|
||||||
import { classifyH5Request, shouldBlockDisabledH5Access, shouldRequireH5Token } from './h5AccessPolicy.js'
|
import { classifyH5Request, shouldBlockDisabledH5Access, shouldRequireH5Token } from './h5AccessPolicy.js'
|
||||||
import { H5AccessService } from './services/h5AccessService.js'
|
import { H5AccessService } from './services/h5AccessService.js'
|
||||||
|
import { registerSeedMarketplaces } from '../utils/plugins/marketplaceManager.js'
|
||||||
|
|
||||||
function readArgValue(flag: string): string | undefined {
|
function readArgValue(flag: string): string | undefined {
|
||||||
const args = process.argv.slice(2)
|
const args = process.argv.slice(2)
|
||||||
@ -147,6 +148,16 @@ export function startServer(port = PORT, host = HOST) {
|
|||||||
process.env.SERVER_AUTH_REQUIRED === '1'
|
process.env.SERVER_AUTH_REQUIRED === '1'
|
||||||
const h5AccessService = new H5AccessService()
|
const h5AccessService = new H5AccessService()
|
||||||
|
|
||||||
|
// Register seed marketplaces (e.g. the cc-haha-builtin marketplace
|
||||||
|
// shipped with the Electron package via CLAUDE_CODE_PLUGIN_SEED_DIR)
|
||||||
|
// into ~/.claude/plugins/known_marketplaces.json so they're visible
|
||||||
|
// in the desktop's Settings → Plugins view. Fire-and-forget — startup
|
||||||
|
// shouldn't block on this; the marketplace appears in the UI within
|
||||||
|
// 1-2 seconds of server boot.
|
||||||
|
void registerSeedMarketplaces().catch((err) => {
|
||||||
|
console.error('[Server] Failed to register seed marketplaces:', err)
|
||||||
|
})
|
||||||
|
|
||||||
let server: ReturnType<typeof Bun.serve<WebSocketData>>
|
let server: ReturnType<typeof Bun.serve<WebSocketData>>
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user