Files
deepseek-harness/packages/sdk/plugin-fetch/src/fetcher.ts
T
imccyu 825a63ab01 feat(sdk): add dsh-plugin-fetch source + fetcher seam
Greenfield #2 "建插件" modules for the forthcoming `dsh-sdk create <source>`
command, in a new foundation-independent package so it touches none of the
dsh-scripts / dsh-helper / create-sdk hotspots the foundation refactor edits.

- `resolvePluginSource(spec)` parses `owner/repo[/subdir]#ref` (github) or
  `pkg@version` (npm) into a `PluginSource` discriminated union, failing loud on
  an ambiguous or malformed spec.
- `PluginFetcher<S>` seam + `fetchPlugin` tag dispatch returning a common
  `FetchedPlugin` (temp dir + immutable provenance).
- `GigetFetcher` (github) over @bluwy/giget-core: resolve `#ref` to a commit SHA
  first, download that SHA; provenance pins the SHA. Chosen over unjs/giget for
  its single runtime dep and absent install/action surface.
- `PacoteFetcher` (npm) over pacote: resolve the manifest, then extract the
  tarball verified against its registry integrity. Registry-only is enforced by
  the source resolver; extract runs no lifecycle scripts.
- Branded `CommitSha`/`Integrity`; network + temp-dir boundaries are injected so
  the logic is unit-tested at 100% per-file coverage without network.

Wiring (package.json pin, cordis.yml via ProjectEditSession with a confirmed
diff, install --ignore-scripts) and the launcher command registration land
later with the foundation.
2026-07-18 16:08:12 +08:00

91 lines
3.3 KiB
TypeScript

/**
* The `PluginFetcher` seam, its common `FetchedPlugin` result, and the
* tag-dispatched entry point. A fetcher acquires one plugin source into a fresh
* temp directory WITHOUT executing any pulled code, and reports immutable
* provenance the wiring step pins the dependency to.
*
* @module @deepseek-ai/dsh-plugin-fetch/fetcher
*/
import { mkdtemp } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { assertNever } from './never.ts'
import type { CommitSha, Integrity } from './ids.ts'
import type { GithubSource, NpmSource, PluginSource } from './source.ts'
/** Immutable pin for a github fetch: the resolved commit the tarball came from. */
export interface GithubProvenance {
readonly kind: 'github'
readonly sha: CommitSha
}
/** Immutable pin for an npm fetch: exact version, tarball URL, and integrity. */
export interface NpmProvenance {
readonly kind: 'npm'
/** Concrete resolved version (e.g. `1.2.3`), never the requested range/tag. */
readonly version: string
/** Tarball URL the artifact resolved to. */
readonly resolved: string
/** Subresource integrity the artifact was verified against. */
readonly integrity: Integrity
}
/** Provenance a fetch records so wiring can pin an immutable dependency. */
export type PluginProvenance = GithubProvenance | NpmProvenance
/** The common result of fetching any plugin source. */
export interface FetchedPlugin {
/** Absolute temp directory holding the extracted, UN-executed source. */
readonly dir: string
/** The source that produced this fetch, echoed for the wiring step. */
readonly source: PluginSource
/** Immutable provenance to pin the dependency during wiring. */
readonly provenance: PluginProvenance
}
/**
* A fetcher for one source kind. Implementations resolve the immutable pin
* BEFORE download and must never run lifecycle scripts or template actions.
*/
export interface PluginFetcher<S extends PluginSource = PluginSource> {
/** The single source kind this fetcher handles. */
readonly kind: S['kind']
/**
* Fetch one source into a fresh temp directory.
* @param source - the resolved source to fetch.
* @returns the temp dir plus immutable provenance.
*/
fetch(source: S): Promise<FetchedPlugin>
}
/** The per-kind fetchers {@link fetchPlugin} dispatches across. */
export interface PluginFetchers {
readonly github: PluginFetcher<GithubSource>
readonly npm: PluginFetcher<NpmSource>
}
/**
* Dispatch one source to its fetcher by discriminant tag.
* @param source - the resolved plugin source.
* @param fetchers - the per-kind fetchers to route across.
* @returns the fetch result from the matching fetcher.
*/
export function fetchPlugin(source: PluginSource, fetchers: PluginFetchers): Promise<FetchedPlugin> {
switch (source.kind) {
case 'github': return fetchers.github.fetch(source)
case 'npm': return fetchers.npm.fetch(source)
default: return assertNever(source, 'fetchPlugin')
}
}
/**
* Create a fresh, empty temp directory for one fetch — the default temp-dir
* seam shared by the concrete fetchers.
* @param prefix - a `mkdtemp` name prefix identifying the fetch kind.
* @returns the absolute path of the created directory.
*/
export function createTempDir(prefix: string): Promise<string> {
return mkdtemp(join(tmpdir(), prefix))
}