chore(sdk): drop dsh-plugin-fetch (giget/pacote) — #2 will use native npm/pnpm deps

External-plugin creation will add a package-manager-native dependency
(github:owner/repo#ref or pkg@version) plus a cordis mount instead of fetching
tarballs into a temp dir, so the giget/pacote fetch package is no longer needed.
This commit is contained in:
imccyu
2026-07-18 16:11:56 +08:00
parent 472a683bdc
commit 8052370155
22 files changed
-1971

No files matched your search

-3
View File
@@ -138,7 +138,6 @@ flowchart TD
end
subgraph group_sdk["packages/sdk"]
pkg_helper["helper"]
pkg_plugin_fetch["plugin-fetch"]
pkg_scripts["scripts"]
pkg_telemetry["telemetry"]
end
@@ -154,7 +153,6 @@ flowchart TD
pkg_llm --> pkg_brand
pkg_code_runtime_worker --> pkg_code_runtime
pkg_helper --> pkg_brand
pkg_plugin_fetch --> pkg_brand
pkg_scripts --> pkg_app_boot
pkg_telemetry --> pkg_brand
pkg_llm_deepseek --> pkg_llm
@@ -446,7 +444,6 @@ flowchart TD
| [`llm`](../packages/llm/llm) | `llm` | [`brand`](../packages/util/brand) |
| [`code-runtime-worker`](../packages/code-runtime/code-runtime-worker) | `code-runtime` | [`code-runtime`](../packages/code-runtime/code-runtime) |
| [`helper`](../packages/sdk/helper) | `sdk` | [`brand`](../packages/util/brand) |
| [`plugin-fetch`](../packages/sdk/plugin-fetch) | `sdk` | [`brand`](../packages/util/brand) |
| [`scripts`](../packages/sdk/scripts) | `sdk` | [`app-boot`](../packages/ui/app-boot) |
| [`telemetry`](../packages/sdk/telemetry) | `sdk` | [`brand`](../packages/util/brand) |
| [`llm-deepseek`](../packages/llm/llm-deepseek) | `llm` | [`llm`](../packages/llm/llm) |
-1
View File
@@ -9,7 +9,6 @@ The [feature RFC](../../docs/rfc/proposed/feature/2026-07-14-sdk-developer-proje
| [`helper`](helper/README.md) | Project aggregate, edit session, builtin features, project documents, templates, package managers, and prompt abstraction |
| [`scripts`](scripts/README.md) | The `dsh-sdk` launcher: `start`, `dev`, `build`, and interactive `config` |
| [`create-sdk`](create-sdk/README.md) | The `npm create @deepseek-ai/sdk` initializer |
| [`plugin-fetch`](plugin-fetch/README.md) | Fetch an external plugin (github/npm) into a temp dir — pinned and un-executed — for `dsh-sdk create` |
`@deepseek-ai/create-sdk` is the one package-name exception to the repository's `@deepseek-ai/dsh-*` rule: npm's scoped initializer convention requires that name for `npm create @deepseek-ai/sdk`.
-32
View File
@@ -1,32 +0,0 @@
# `@deepseek-ai/dsh-plugin-fetch`
Fetch an external Cordis plugin into a temp directory — pinned to an immutable commit or integrity and never executed — for the forthcoming `dsh-sdk create <source>` command.
The package parses a source spec into a `PluginSource`, dispatches to the matching `PluginFetcher`, and returns a common `FetchedPlugin` (temp dir + immutable provenance) that the wiring step pins into `package.json`, mounts in `cordis.yml`, and installs with `--ignore-scripts`.
| Export | Role |
|---|---|
| `resolvePluginSource(spec)``PluginSource` | Parse `owner/repo[/subdir]#ref` (github) or `pkg@version` (npm); fail loud on an ambiguous or malformed spec |
| `PluginFetcher<S>` | The fetch seam: resolve the pin BEFORE download, extract without executing pulled code |
| `GigetFetcher` / `createGigetFetcher()` | Github fetcher over `@bluwy/giget-core`: resolve `#ref` to a commit SHA, download that SHA |
| `PacoteFetcher` / `createPacoteFetcher()` | Npm fetcher over `pacote`: resolve the manifest, then extract the tarball verified against its integrity |
| `fetchPlugin(source, fetchers)``FetchedPlugin` | Dispatch one source to its fetcher by discriminant tag |
## Safety model — confirm-before-run, not run-on-fetch
A fetch only downloads and unpacks; it runs no install, no `postinstall`/`prepare`, and no degit-style template actions.
- **github** uses `@bluwy/giget-core` (one runtime dependency, `modern-tar`; no CLI, install, or JSON-registry surface) so a fetch can only download and untar a tarball. The commit is pinned first: `GigetFetcher` resolves `#ref` — or the default branch when absent — to an immutable SHA via the GitHub commits API, then downloads that SHA. Provenance carries the SHA so wiring pins `github:owner/repo#<sha>`.
- **npm** uses `pacote`. Registry-only is enforced upstream: `resolvePluginSource` produces only a `name@version` registry spec, so pacote never sees a git/file/dir spec whose lifecycle scripts would run, and a registry tarball extract is a plain untar. The manifest is resolved first so extract verifies the artifact against the registry-published integrity (a mismatch raises `EINTEGRITY`). Provenance carries the exact version, resolved URL, and integrity.
Both network boundaries (giget download, GitHub ref resolution, pacote, temp-dir allocation) are constructor-injected, so the fetch logic is unit-tested without network; the `create*Fetcher()` factories wire the real libraries.
## Model Experience
None, as this developer tooling acquires plugin sources for the SDK launcher and registers no live agent or model surface.
## Known Limitations and Deferred Work
- **Wiring is not here yet** — pinning `package.json`, mounting `cordis.yml` through `ProjectEditSession` with a confirmed diff, and `install --ignore-scripts` land with the `dsh-sdk create` command. This package stops at a fetched, pinned temp directory.
- **npm registry authentication** — `PacoteFetcher` targets a public or default-configured registry; private-registry auth beyond pacote's ambient npm config is deferred.
- **Template-repo initialization** — the whole-project init mode (`dsh-sdk create` from a template repository) is out of scope; this package fetches a single plugin into an existing project.
-37
View File
@@ -1,37 +0,0 @@
{
"name": "@deepseek-ai/dsh-plugin-fetch",
"description": "Fetch an external Cordis plugin (github or npm) into a temp dir, pinned and un-executed, for dsh-sdk create",
"version": "0.0.1",
"private": true,
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
"exports": {
".": {
"types": "./lib/types/index.d.ts",
"default": "./lib/index.js"
},
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"files": [
"lib/index.js",
"lib/types/**/*.d.ts",
"lib/types/**/*.d.ts.map",
"src"
],
"license": "BSD-3-Clause",
"dependencies": {
"@bluwy/giget-core": "^0.1.7",
"pacote": "^22.0.0"
},
"peerDependencies": {
"@deepseek-ai/dsh-brand": "^0.0.1",
"cordis": "^4.0.0-rc.7"
},
"devDependencies": {
"@deepseek-ai/dsh-brand": "workspace:^",
"@types/pacote": "^11.1.8",
"cordis": "^4.0.0-rc.7"
}
}
-90
View File
@@ -1,90 +0,0 @@
/**
* 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))
}
@@ -1,116 +0,0 @@
/**
* The github {@link PluginFetcher}, backed by `@bluwy/giget-core`.
*
* `@bluwy/giget-core` is chosen over unjs `giget`: it carries a single runtime
* dependency (`modern-tar`) versus giget's CLI/registry stack, and it dropped
* the `install` and JSON-registry options entirely, so a fetch can only ever
* download and untar a tarball — never run install or degit-style actions. That
* is exactly the "extract, never execute" guarantee this feature needs.
*
* The commit is pinned BEFORE download: {@link GigetFetcher} resolves `#ref` to
* an immutable SHA (default via the GitHub commits API), then downloads that
* SHA. Provenance carries the SHA so wiring pins `github:owner/repo#<sha>`.
*
* @module @deepseek-ai/dsh-plugin-fetch/giget-fetcher
*/
import { downloadTemplate } from '@bluwy/giget-core'
import { createTempDir, type FetchedPlugin, type PluginFetcher } from './fetcher.ts'
import { commitSha, type CommitSha } from './ids.ts'
import type { GithubSource } from './source.ts'
/** Temp-dir name prefix for github fetches. */
export const GITHUB_TEMP_PREFIX = 'dsh-plugin-github-'
/** Downloads a giget input string into `dir`; the tarball-extraction seam. */
export type DownloadTemplate = (
input: string,
options: { dir: string; force: 'clean' },
) => Promise<{ dir: string }>
/** Resolves a github source's ref to an immutable commit SHA before download. */
export type ResolveRef = (source: GithubSource) => Promise<CommitSha>
/** The injected collaborators a {@link GigetFetcher} needs. */
export interface GigetFetcherDeps {
/** Downloads a pinned giget input into a directory. */
download: DownloadTemplate
/** Resolves `source.ref` (or the default branch) to a commit SHA. */
resolveRef: ResolveRef
/** Allocates the fresh temp directory to download into. */
createTempDir: (prefix: string) => Promise<string>
}
/** Build the giget input string that pins a github source to a commit SHA. */
function gigetInput(source: GithubSource, sha: CommitSha): string {
const path = source.subdir ? `${source.owner}/${source.repo}/${source.subdir}` : `${source.owner}/${source.repo}`
return `${path}#${sha}`
}
/** A human-readable label for one github source, for error messages. */
function githubLabel(source: GithubSource): string {
return `${source.owner}/${source.repo}#${source.ref ?? 'HEAD'}`
}
/**
* Resolve a github source's ref to an immutable SHA via the GitHub commits API.
* Uses the `application/vnd.github.sha` media type, which returns the resolved
* commit id as plain text.
* @param source - the github source; an absent `ref` resolves the default branch (`HEAD`).
* @param token - optional bearer token for private repositories.
* @returns the resolved immutable commit SHA.
* @throws if the GitHub API rejects the request.
*/
export async function defaultResolveRef(source: GithubSource, token?: string): Promise<CommitSha> {
const ref = source.ref ?? 'HEAD'
const url = `https://api.github.com/repos/${source.owner}/${source.repo}/commits/${ref}`
const headers: Record<string, string> = { Accept: 'application/vnd.github.sha' }
if (token !== undefined) headers.Authorization = `Bearer ${token}`
const response = await fetch(url, { headers })
if (!response.ok) {
throw new Error(`cannot resolve github ref ${githubLabel(source)}: HTTP ${response.status}`)
}
return commitSha((await response.text()).trim())
}
/** Fetches a github plugin source by pinning `#ref` to a commit SHA, then downloading it. */
export class GigetFetcher implements PluginFetcher<GithubSource> {
readonly kind = 'github' as const
private readonly deps: GigetFetcherDeps
/** Construct with injected download, ref-resolution, and temp-dir seams. */
constructor(deps: GigetFetcherDeps) {
this.deps = deps
}
async fetch(source: GithubSource): Promise<FetchedPlugin> {
const sha = await this.deps.resolveRef(source)
const dir = await this.deps.createTempDir(GITHUB_TEMP_PREFIX)
await this.deps.download(gigetInput(source, sha), { dir, force: 'clean' })
return { dir, source, provenance: { kind: 'github', sha } }
}
}
/** Options for the production github fetcher. */
export interface GithubFetchOptions {
/** Bearer token for private repositories; defaults to `GITHUB_TOKEN`. */
token?: string
}
/**
* Build the production github fetcher wired to `@bluwy/giget-core` and the
* GitHub commits API.
* @param options - optional token override (else `process.env.GITHUB_TOKEN`).
* @returns a {@link GigetFetcher} using the real download and ref-resolution seams.
*/
export function createGigetFetcher(options: GithubFetchOptions = {}): GigetFetcher {
const token = options.token ?? process.env.GITHUB_TOKEN
return new GigetFetcher({
download: (input, downloadOptions) => downloadTemplate(input, {
...downloadOptions,
...token !== undefined ? { providerOptions: { auth: token } } : {},
}),
resolveRef: source => defaultResolveRef(source, token),
createTempDir,
})
}
-39
View File
@@ -1,39 +0,0 @@
/**
* Branded provenance identities owned by the plugin-fetch layer. Both cross the
* fetch → wiring boundary and are opaque tokens that must not be confused with
* ordinary strings (a package name, a URL) at that seam.
*
* @module @deepseek-ai/dsh-plugin-fetch/ids
*/
import type { Branded } from '@deepseek-ai/dsh-brand'
/** An immutable git commit object id a github fetch pins to. */
export type CommitSha = Branded<'CommitSha'>
/**
* Construct a {@link CommitSha}, validating the hexadecimal object-id shape.
* @param value - lowercase hex of an abbreviated or full commit id (764 chars, covering SHA-1 and SHA-256).
* @returns the branded commit id.
*/
export function commitSha(value: string): CommitSha {
if (!/^[0-9a-f]{7,64}$/.test(value)) {
throw new Error(`invalid commit sha: ${JSON.stringify(value)}`)
}
return value as CommitSha
}
/** A Subresource Integrity string an npm fetch pins to. */
export type Integrity = Branded<'Integrity'>
/**
* Construct an {@link Integrity}, validating the SRI `<algorithm>-<base64>` shape.
* @param value - a single SRI entry using sha256, sha384, or sha512.
* @returns the branded integrity string.
*/
export function integrity(value: string): Integrity {
if (!/^sha(256|384|512)-[A-Za-z0-9+/]+={0,2}$/.test(value)) {
throw new Error(`invalid subresource integrity: ${JSON.stringify(value)}`)
}
return value as Integrity
}
-48
View File
@@ -1,48 +0,0 @@
/**
* Fetch an external Cordis plugin (github or npm) into a temp directory —
* pinned to an immutable commit/integrity and never executed — for the
* `dsh-sdk create <source>` command. Parses a source spec, dispatches to the
* matching fetcher, and returns a common {@link FetchedPlugin} the wiring step
* pins and mounts.
*
* @module @deepseek-ai/dsh-plugin-fetch
*/
export { resolvePluginSource } from './source.ts'
export type { GithubSource, NpmSource, PluginSource } from './source.ts'
export { commitSha, integrity } from './ids.ts'
export type { CommitSha, Integrity } from './ids.ts'
export { createTempDir, fetchPlugin } from './fetcher.ts'
export type {
FetchedPlugin,
GithubProvenance,
NpmProvenance,
PluginFetcher,
PluginFetchers,
PluginProvenance,
} from './fetcher.ts'
export {
createGigetFetcher,
defaultResolveRef,
GigetFetcher,
GITHUB_TEMP_PREFIX,
} from './giget-fetcher.ts'
export type {
DownloadTemplate,
GigetFetcherDeps,
GithubFetchOptions,
ResolveRef,
} from './giget-fetcher.ts'
export {
createPacoteFetcher,
NPM_TEMP_PREFIX,
PacoteFetcher,
} from './pacote-fetcher.ts'
export type {
NpmFetchOptions,
PacoteApi,
PacoteExtractResult,
PacoteFetcherDeps,
PacoteFetchOptions,
PacoteResolution,
} from './pacote-fetcher.ts'
-19
View File
@@ -1,19 +0,0 @@
/**
* Exhaustiveness helper for this package's closed unions. Kept local so the
* SDK plugin-fetch tooling stays free of the model-runtime `dsh-llm` dependency
* that owns the shared `assertNever`.
*
* @module @deepseek-ai/dsh-plugin-fetch/never
*/
/**
* Mark an unreachable closed-union branch. A newly unhandled variant fails
* compilation at the call site; a value that escaped its type throws at runtime.
* @param value - the impossible value; typed `never` so a new variant fails to compile at every call site.
* @param context - optional label prefixed into the throw message.
* @returns never — it always throws, rendering the offending value.
*/
export function assertNever(value: never, context?: string): never {
const rendered = (JSON.stringify(value) as string | undefined) ?? String(value)
throw new Error(`unreachable variant${context ? ` in ${context}` : ''}: ${rendered}`)
}
@@ -1,129 +0,0 @@
/**
* The npm {@link PluginFetcher}, backed by `pacote`.
*
* Supply-chain safety comes from three layers: (1) {@link resolvePluginSource}
* only ever produces a registry `name@version` spec, so pacote classifies it as
* a registry source and cannot be steered to a git/file/dir spec whose
* lifecycle scripts would run; (2) a registry tarball extract is a plain untar —
* pacote runs no `prepare`/`postinstall` during {@link PacoteFetcher.fetch}; and
* (3) the later wiring step installs with `--ignore-scripts`. The manifest is
* resolved first so extract verifies the tarball against the registry-published
* integrity (a mismatch raises `EINTEGRITY`).
*
* @module @deepseek-ai/dsh-plugin-fetch/pacote-fetcher
*/
import { extract as pacoteExtract, manifest as pacoteManifest } from 'pacote'
import { createTempDir, type FetchedPlugin, type PluginFetcher } from './fetcher.ts'
import { integrity } from './ids.ts'
import type { NpmSource } from './source.ts'
/** Temp-dir name prefix for npm fetches. */
export const NPM_TEMP_PREFIX = 'dsh-plugin-npm-'
/** The subset of pacote options this fetcher passes through. */
export interface PacoteFetchOptions {
/** Registry to resolve against; absent uses pacote's default. */
registry?: string
/** Known resolved tarball URL, forwarded to extract. */
resolved?: string
/** Expected integrity, forwarded to extract for `EINTEGRITY` verification. */
integrity?: string
}
/** The resolved registry manifest fields this fetcher pins from. */
export interface PacoteResolution {
/** Resolved tarball URL. */
_resolved: string
/** Registry-published integrity. */
_integrity: string
/** Concrete resolved version. */
version: string
}
/** The extract result fields this fetcher pins from. */
export interface PacoteExtractResult {
/** Resolved tarball URL of the extracted artifact. */
resolved: string
/** Integrity of the extracted artifact. */
integrity: string
}
/** The pacote surface a {@link PacoteFetcher} depends on; the fetch seam. */
export interface PacoteApi {
/** Resolve a registry spec to its pinned manifest fields. */
manifest: (spec: string, options?: PacoteFetchOptions) => Promise<PacoteResolution>
/** Untar a registry spec into `dest`, verifying integrity when supplied. */
extract: (spec: string, dest: string, options?: PacoteFetchOptions) => Promise<PacoteExtractResult>
}
/** The injected collaborators a {@link PacoteFetcher} needs. */
export interface PacoteFetcherDeps {
/** The pacote resolve/extract surface. */
pacote: PacoteApi
/** Allocates the fresh temp directory to extract into. */
createTempDir: (prefix: string) => Promise<string>
/** Registry to resolve against; absent uses pacote's default. */
registry?: string
}
/** Fetches an npm plugin source by resolving its manifest, then extracting the verified tarball. */
export class PacoteFetcher implements PluginFetcher<NpmSource> {
readonly kind = 'npm' as const
private readonly deps: PacoteFetcherDeps
/** Construct with injected pacote, temp-dir, and optional registry seams. */
constructor(deps: PacoteFetcherDeps) {
this.deps = deps
}
async fetch(source: NpmSource): Promise<FetchedPlugin> {
const spec = `${source.name}@${source.version}`
const registryOptions: PacoteFetchOptions = this.deps.registry !== undefined
? { registry: this.deps.registry }
: {}
const resolution = await this.deps.pacote.manifest(spec, registryOptions)
const dir = await this.deps.createTempDir(NPM_TEMP_PREFIX)
const extracted = await this.deps.pacote.extract(spec, dir, {
...registryOptions,
resolved: resolution._resolved,
integrity: resolution._integrity,
})
return {
dir,
source,
provenance: {
kind: 'npm',
version: resolution.version,
resolved: extracted.resolved,
integrity: integrity(extracted.integrity),
},
}
}
}
/** Options for the production npm fetcher. */
export interface NpmFetchOptions {
/** Registry to resolve against; absent uses pacote's default. */
registry?: string
}
/**
* Build the production npm fetcher wired to `pacote`.
* @param options - optional registry override.
* @returns a {@link PacoteFetcher} using the real pacote resolve/extract seam.
*/
export function createPacoteFetcher(options: NpmFetchOptions = {}): PacoteFetcher {
const pacote: PacoteApi = {
manifest: async (spec, pacoteOptions) => {
const resolved = await pacoteManifest(spec, pacoteOptions)
return { _resolved: resolved._resolved, _integrity: resolved._integrity, version: resolved.version }
},
extract: (spec, dest, pacoteOptions) => pacoteExtract(spec, dest, pacoteOptions),
}
return new PacoteFetcher({
pacote,
createTempDir,
...options.registry !== undefined ? { registry: options.registry } : {},
})
}
-126
View File
@@ -1,126 +0,0 @@
/**
* The `PluginSource` discriminated union and the resolver that parses one CLI
* spec string into it. Ambiguous or malformed specs fail loud here — the single
* earliest resolvable point — rather than surfacing as a confusing fetch error.
*
* Grammar:
* - github: `owner/repo[/subdir]#ref` — a `#` unambiguously marks a github ref;
* `ref` is optional and, when omitted, the fetcher pins the default branch.
* - npm: `pkg@version` (scoped `@scope/pkg@version`) — the `@version` is the
* only disambiguator from a bare `owner/repo` github locator.
*
* @module @deepseek-ai/dsh-plugin-fetch/source
*/
/** A plugin pulled from a github (git tarball) repository. */
export interface GithubSource {
readonly kind: 'github'
/** Repository owner (user or org). */
readonly owner: string
/** Repository name. */
readonly repo: string
/** Path within the repository to extract; absent means the repository root. */
readonly subdir?: string
/** Branch, tag, or commit; absent means the repository's default branch. */
readonly ref?: string
}
/** A plugin pulled from an npm registry by exact package and version spec. */
export interface NpmSource {
readonly kind: 'npm'
/** Package name, including any `@scope/` prefix. */
readonly name: string
/** Registry version, range, or dist-tag (non-empty). */
readonly version: string
}
/** Every plugin origin `dsh-sdk create <source>` understands. */
export type PluginSource = GithubSource | NpmSource
/** One `/`-separated github name segment (owner, repo, or subdir component). */
function isNameSegment(segment: string): boolean {
return /^[A-Za-z0-9._-]+$/.test(segment) && segment !== '.' && segment !== '..'
}
/** A git ref: branch, tag, or commit; permits `/`-nested names, rejects traversal. */
function isGitRef(ref: string): boolean {
return /^[A-Za-z0-9._/-]+$/.test(ref)
&& !ref.includes('..')
&& !ref.startsWith('/')
&& !ref.endsWith('/')
}
/** An npm package name, scoped (`@scope/name`) or unscoped. */
function isNpmPackageName(name: string): boolean {
const segment = /^[a-z0-9][a-z0-9._-]*$/
if (name.startsWith('@')) {
const slash = name.indexOf('/')
if (slash < 2 || slash === name.length - 1) return false
return segment.test(name.slice(1, slash)) && segment.test(name.slice(slash + 1))
}
return segment.test(name)
}
/** Parse a `owner/repo[/subdir]` locator with an optional already-split ref. */
function tryParseGithubLocator(locator: string, ref: string | undefined): GithubSource | undefined {
if (!/^[^\s@#]+$/.test(locator)) return undefined
const [owner, repo, ...subdirSegments] = locator.split('/')
if (owner === undefined || repo === undefined) return undefined
if (!isNameSegment(owner) || !isNameSegment(repo)) return undefined
if (subdirSegments.some(segment => !isNameSegment(segment))) return undefined
if (ref !== undefined && !isGitRef(ref)) return undefined
const subdir = subdirSegments.join('/')
return {
kind: 'github',
owner,
repo,
...subdir.length > 0 ? { subdir } : {},
...ref !== undefined ? { ref } : {},
}
}
/** Parse `pkg@version` (scoped or unscoped); undefined when it is not npm-shaped. */
function tryParseNpmSource(spec: string): NpmSource | undefined {
if (/[\s#]/.test(spec)) return undefined
// A scoped spec's version `@` follows the scope's `/`; an unscoped spec's is
// the first `@`. A leading `@` with no version `@` yields index 0 (rejected).
const versionAt = spec.startsWith('@') ? spec.indexOf('@', spec.indexOf('/') + 1) : spec.indexOf('@')
if (versionAt <= 0) return undefined
const name = spec.slice(0, versionAt)
const version = spec.slice(versionAt + 1)
if (version.length === 0 || version.includes('/')) return undefined
if (!isNpmPackageName(name)) return undefined
return { kind: 'npm', name, version }
}
/**
* Parse one `dsh-sdk create <source>` spec into a {@link PluginSource}.
* @param spec - the raw source argument.
* @returns the discriminated source.
* @throws if the spec is empty, malformed, or ambiguous between github and npm.
*/
export function resolvePluginSource(spec: string): PluginSource {
const trimmed = spec.trim()
if (trimmed.length === 0) throw new Error('plugin source must not be empty')
const hashIndex = trimmed.indexOf('#')
if (hashIndex !== -1) {
const ref = trimmed.slice(hashIndex + 1)
if (ref.length === 0) {
throw new Error(`github plugin source is missing a ref after '#': ${JSON.stringify(spec)}`)
}
const source = tryParseGithubLocator(trimmed.slice(0, hashIndex), ref)
if (!source) {
throw new Error(`invalid github plugin source: ${JSON.stringify(spec)} — expected "owner/repo[/subdir]#ref"`)
}
return source
}
const npm = tryParseNpmSource(trimmed)
if (npm) return npm
const github = tryParseGithubLocator(trimmed, undefined)
if (github) return github
throw new Error(
`unrecognized plugin source: ${JSON.stringify(spec)} — expected "owner/repo[/subdir]#ref" (github) or "pkg@version" (npm)`,
)
}
@@ -1,64 +0,0 @@
import { rm, stat } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { describe, expect, it, vi } from 'vitest'
import {
createTempDir,
fetchPlugin,
type FetchedPlugin,
type PluginFetchers,
} from '../src/fetcher.ts'
import { commitSha } from '../src/ids.ts'
import type { GithubSource, NpmSource, PluginSource } from '../src/source.ts'
function stubFetchers(): { fetchers: PluginFetchers; github: ReturnType<typeof vi.fn>; npm: ReturnType<typeof vi.fn> } {
const result = (dir: string): FetchedPlugin => ({
dir,
source: { kind: 'github', owner: 'o', repo: 'r' },
provenance: { kind: 'github', sha: commitSha('a'.repeat(40)) },
})
const github = vi.fn(async (source: GithubSource) => result(`github:${source.repo}`))
const npm = vi.fn(async (source: NpmSource) => result(`npm:${source.name}`))
return {
fetchers: { github: { kind: 'github', fetch: github }, npm: { kind: 'npm', fetch: npm } },
github,
npm,
}
}
describe('fetchPlugin', () => {
it('routes a github source to the github fetcher', async () => {
const { fetchers, github, npm } = stubFetchers()
const source: GithubSource = { kind: 'github', owner: 'o', repo: 'r' }
const result = await fetchPlugin(source, fetchers)
expect(github).toHaveBeenCalledWith(source)
expect(npm).not.toHaveBeenCalled()
expect(result.dir).toBe('github:r')
})
it('routes an npm source to the npm fetcher', async () => {
const { fetchers, github, npm } = stubFetchers()
const source: NpmSource = { kind: 'npm', name: 'plugin', version: '1.0.0' }
const result = await fetchPlugin(source, fetchers)
expect(npm).toHaveBeenCalledWith(source)
expect(github).not.toHaveBeenCalled()
expect(result.dir).toBe('npm:plugin')
})
it('throws on an unknown source kind', () => {
const { fetchers } = stubFetchers()
const bogus = { kind: 'svn' } as unknown as PluginSource
expect(() => fetchPlugin(bogus, fetchers)).toThrow(/unreachable variant in fetchPlugin/)
})
})
describe('createTempDir', () => {
it('creates a fresh empty directory under the OS temp root', async () => {
const dir = await createTempDir('dsh-plugin-fetch-test-')
try {
expect(dir.startsWith(tmpdir())).toBe(true)
expect((await stat(dir)).isDirectory()).toBe(true)
} finally {
await rm(dir, { recursive: true, force: true })
}
})
})
@@ -1,145 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, type Mock, vi } from 'vitest'
import { downloadTemplate } from '@bluwy/giget-core'
import {
createGigetFetcher,
defaultResolveRef,
GigetFetcher,
GITHUB_TEMP_PREFIX,
type GigetFetcherDeps,
} from '../src/giget-fetcher.ts'
import type { CommitSha } from '../src/ids.ts'
import type { GithubSource } from '../src/source.ts'
vi.mock('@bluwy/giget-core', () => ({ downloadTemplate: vi.fn(async (_input: string, options: { dir: string }) => ({ dir: options.dir, source: '', info: { name: '', tar: '' } })) }))
const SHA = 'a'.repeat(40)
/** A `fetch` mock typed with the call signature the assertions destructure. */
function fetchReturning(response: Response): Mock<(url: string, init?: RequestInit) => Promise<Response>> {
return vi.fn((_url: string, _init?: RequestInit) => Promise.resolve(response))
}
function fakeDeps(overrides: Partial<GigetFetcherDeps> = {}): {
deps: GigetFetcherDeps
download: ReturnType<typeof vi.fn>
resolveRef: ReturnType<typeof vi.fn>
createTempDir: ReturnType<typeof vi.fn>
} {
const download = vi.fn(async () => ({ dir: '/tmp/x' }))
const resolveRef = vi.fn(async () => SHA as CommitSha)
const createTempDir = vi.fn(async () => '/tmp/dsh-plugin-github-abc')
return { deps: { download, resolveRef, createTempDir, ...overrides }, download, resolveRef, createTempDir }
}
describe('GigetFetcher.fetch', () => {
it('pins the ref to a SHA, downloads that SHA, and reports provenance', async () => {
const { deps, download, resolveRef, createTempDir } = fakeDeps()
const source: GithubSource = { kind: 'github', owner: 'unjs', repo: 'template', ref: 'main' }
const result = await new GigetFetcher(deps).fetch(source)
expect(resolveRef).toHaveBeenCalledWith(source)
expect(createTempDir).toHaveBeenCalledWith(GITHUB_TEMP_PREFIX)
expect(download).toHaveBeenCalledWith(`unjs/template#${SHA}`, { dir: '/tmp/dsh-plugin-github-abc', force: 'clean' })
expect(result).toEqual({
dir: '/tmp/dsh-plugin-github-abc',
source,
provenance: { kind: 'github', sha: SHA },
})
})
it('includes the subdir in the download input', async () => {
const { deps, download } = fakeDeps()
const source: GithubSource = { kind: 'github', owner: 'o', repo: 'r', subdir: 'packages/plugin' }
await new GigetFetcher(deps).fetch(source)
expect(download).toHaveBeenCalledWith(`o/r/packages/plugin#${SHA}`, expect.anything())
})
it('exposes its source kind', () => {
expect(new GigetFetcher(fakeDeps().deps).kind).toBe('github')
})
})
describe('defaultResolveRef', () => {
afterEach(() => vi.unstubAllGlobals())
it('resolves the default branch (HEAD) with no auth header', async () => {
const fetchMock = fetchReturning(new Response(`${SHA}\n`, { status: 200 }))
vi.stubGlobal('fetch', fetchMock)
const sha = await defaultResolveRef({ kind: 'github', owner: 'o', repo: 'r' })
expect(sha).toBe(SHA)
const [url, init] = fetchMock.mock.calls[0]!
expect(url).toBe('https://api.github.com/repos/o/r/commits/HEAD')
expect((init as RequestInit).headers).toEqual({ Accept: 'application/vnd.github.sha' })
})
it('resolves an explicit ref and sends a bearer token', async () => {
const fetchMock = fetchReturning(new Response(SHA, { status: 200 }))
vi.stubGlobal('fetch', fetchMock)
const sha = await defaultResolveRef({ kind: 'github', owner: 'o', repo: 'r', ref: 'v1.2.3' }, 'secret')
expect(sha).toBe(SHA)
const [url, init] = fetchMock.mock.calls[0]!
expect(url).toBe('https://api.github.com/repos/o/r/commits/v1.2.3')
expect((init as RequestInit).headers).toEqual({
Accept: 'application/vnd.github.sha',
Authorization: 'Bearer secret',
})
})
it('throws with the HEAD label when the API rejects an unref-ed source', async () => {
vi.stubGlobal('fetch', fetchReturning(new Response('', { status: 404 })))
await expect(defaultResolveRef({ kind: 'github', owner: 'o', repo: 'r' })).rejects.toThrow(
/cannot resolve github ref o\/r#HEAD: HTTP 404/,
)
})
it('throws with the explicit-ref label when the API rejects', async () => {
vi.stubGlobal('fetch', fetchReturning(new Response('', { status: 403 })))
await expect(
defaultResolveRef({ kind: 'github', owner: 'o', repo: 'r', ref: 'main' }),
).rejects.toThrow(/cannot resolve github ref o\/r#main: HTTP 403/)
})
})
describe('createGigetFetcher', () => {
const downloadMock = vi.mocked(downloadTemplate)
let savedToken: string | undefined
beforeEach(() => {
downloadMock.mockClear()
savedToken = process.env.GITHUB_TOKEN
delete process.env.GITHUB_TOKEN
})
afterEach(() => {
vi.unstubAllGlobals()
if (savedToken === undefined) delete process.env.GITHUB_TOKEN
else process.env.GITHUB_TOKEN = savedToken
})
it('wires the real download without provider auth when no token is present', async () => {
vi.stubGlobal('fetch', fetchReturning(new Response(SHA, { status: 200 })))
await createGigetFetcher().fetch({ kind: 'github', owner: 'o', repo: 'r', ref: 'main' })
const [input, options] = downloadMock.mock.calls[0]!
expect(input).toBe(`o/r#${SHA}`)
expect(options?.dir).toContain(GITHUB_TEMP_PREFIX)
expect(options?.force).toBe('clean')
expect(options?.providerOptions).toBeUndefined()
})
it('passes an explicit token to both ref resolution and provider auth', async () => {
const fetchMock = fetchReturning(new Response(SHA, { status: 200 }))
vi.stubGlobal('fetch', fetchMock)
await createGigetFetcher({ token: 'tok' }).fetch({ kind: 'github', owner: 'o', repo: 'r' })
expect((fetchMock.mock.calls[0]![1] as RequestInit).headers).toMatchObject({ Authorization: 'Bearer tok' })
const [, options] = downloadMock.mock.calls[0]!
expect(options).toMatchObject({ providerOptions: { auth: 'tok' } })
})
it('reads GITHUB_TOKEN from the environment', async () => {
process.env.GITHUB_TOKEN = 'from-env'
vi.stubGlobal('fetch', fetchReturning(new Response(SHA, { status: 200 })))
await createGigetFetcher().fetch({ kind: 'github', owner: 'o', repo: 'r' })
const [, options] = downloadMock.mock.calls[0]!
expect(options).toMatchObject({ providerOptions: { auth: 'from-env' } })
})
})
@@ -1,39 +0,0 @@
import { describe, expect, it } from 'vitest'
import { commitSha, integrity } from '../src/ids.ts'
describe('commitSha', () => {
it('accepts abbreviated and full lowercase hex object ids', () => {
expect(commitSha('abc1234')).toBe('abc1234')
expect(commitSha('a'.repeat(40))).toBe('a'.repeat(40))
expect(commitSha('0'.repeat(64))).toBe('0'.repeat(64))
})
it.each([
['too short', 'abc123'],
['uppercase', 'ABCDEF1'],
['non-hex', 'ghijklm'],
['too long', 'a'.repeat(65)],
['empty', ''],
])('rejects an invalid sha (%s)', (_label, value) => {
expect(() => commitSha(value)).toThrow(/invalid commit sha/)
})
})
describe('integrity', () => {
it.each([
'sha512-abcABC123+/==',
'sha384-abcABC123+/',
'sha256-Zm9vYmFy',
])('accepts a valid SRI entry (%s)', (value) => {
expect(integrity(value)).toBe(value)
})
it.each([
['missing algorithm', 'abcABC123'],
['unsupported algorithm', 'sha1-abcABC123'],
['illegal base64 char', 'sha512-abc*def'],
['empty', ''],
])('rejects an invalid integrity (%s)', (_label, value) => {
expect(() => integrity(value)).toThrow(/invalid subresource integrity/)
})
})
@@ -1,19 +0,0 @@
import { describe, expect, it } from 'vitest'
import { assertNever } from '../src/never.ts'
describe('assertNever', () => {
it('throws with the rendered value and a context label', () => {
expect(() => assertNever('surprise' as never, 'demo')).toThrow(
/unreachable variant in demo: "surprise"/,
)
})
it('omits the context clause when none is given', () => {
expect(() => assertNever(7 as never)).toThrow(/unreachable variant: 7$/)
})
it('falls back to String() when the value is not JSON-serializable', () => {
// JSON.stringify(undefined) is undefined, exercising the String() fallback.
expect(() => assertNever(undefined as never)).toThrow(/unreachable variant: undefined$/)
})
})
@@ -1,108 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { extract as pacoteExtract, manifest as pacoteManifest } from 'pacote'
import {
createPacoteFetcher,
NPM_TEMP_PREFIX,
PacoteFetcher,
type PacoteApi,
type PacoteFetcherDeps,
} from '../src/pacote-fetcher.ts'
import type { NpmSource } from '../src/source.ts'
vi.mock('pacote', () => ({ manifest: vi.fn(), extract: vi.fn() }))
const INTEGRITY = 'sha512-abcABC123+/=='
const RESOLVED = 'https://registry.npmjs.org/plugin/-/plugin-1.2.3.tgz'
function fakePacote(): PacoteApi {
return {
manifest: vi.fn(async () => ({ _resolved: RESOLVED, _integrity: INTEGRITY, version: '1.2.3' })),
extract: vi.fn(async () => ({ resolved: RESOLVED, integrity: INTEGRITY })),
}
}
function deps(overrides: Partial<PacoteFetcherDeps> = {}): PacoteFetcherDeps {
return {
pacote: fakePacote(),
createTempDir: vi.fn(async () => '/tmp/dsh-plugin-npm-abc'),
...overrides,
}
}
const SOURCE: NpmSource = { kind: 'npm', name: 'plugin', version: '^1.0.0' }
describe('PacoteFetcher.fetch', () => {
it('resolves the manifest, extracts with integrity, and reports provenance', async () => {
const d = deps()
const result = await new PacoteFetcher(d).fetch(SOURCE)
expect(d.pacote.manifest).toHaveBeenCalledWith('plugin@^1.0.0', {})
expect(d.createTempDir).toHaveBeenCalledWith(NPM_TEMP_PREFIX)
expect(d.pacote.extract).toHaveBeenCalledWith('plugin@^1.0.0', '/tmp/dsh-plugin-npm-abc', {
resolved: RESOLVED,
integrity: INTEGRITY,
})
expect(result).toEqual({
dir: '/tmp/dsh-plugin-npm-abc',
source: SOURCE,
provenance: { kind: 'npm', version: '1.2.3', resolved: RESOLVED, integrity: INTEGRITY },
})
})
it('forwards a configured registry to both manifest and extract', async () => {
const d = deps({ registry: 'https://npm.internal/' })
await new PacoteFetcher(d).fetch(SOURCE)
expect(d.pacote.manifest).toHaveBeenCalledWith('plugin@^1.0.0', { registry: 'https://npm.internal/' })
expect(d.pacote.extract).toHaveBeenCalledWith('plugin@^1.0.0', '/tmp/dsh-plugin-npm-abc', {
registry: 'https://npm.internal/',
resolved: RESOLVED,
integrity: INTEGRITY,
})
})
it('rejects a registry integrity that is not a valid SRI', async () => {
const pacote = fakePacote()
pacote.extract = vi.fn(async () => ({ resolved: RESOLVED, integrity: 'not-sri' }))
await expect(new PacoteFetcher(deps({ pacote })).fetch(SOURCE)).rejects.toThrow(
/invalid subresource integrity/,
)
})
it('exposes its source kind', () => {
expect(new PacoteFetcher(deps()).kind).toBe('npm')
})
})
describe('createPacoteFetcher', () => {
const manifestMock = vi.mocked(pacoteManifest)
const extractMock = vi.mocked(pacoteExtract)
beforeEach(() => {
manifestMock.mockReset()
extractMock.mockReset()
// The real overloaded pacote manifest returns a much wider shape; the fetcher reads only these fields.
manifestMock.mockResolvedValue(
{ _resolved: RESOLVED, _integrity: INTEGRITY, version: '1.2.3' } as unknown as Awaited<
ReturnType<typeof pacoteManifest>
>,
)
extractMock.mockResolvedValue({ from: 'plugin@1.2.3', resolved: RESOLVED, integrity: INTEGRITY })
})
afterEach(() => vi.clearAllMocks())
it('wires the real pacote resolve/extract surface', async () => {
const result = await createPacoteFetcher().fetch(SOURCE)
expect(manifestMock).toHaveBeenCalledWith('plugin@^1.0.0', {})
expect(extractMock).toHaveBeenCalledWith('plugin@^1.0.0', expect.stringContaining(NPM_TEMP_PREFIX), {
resolved: RESOLVED,
integrity: INTEGRITY,
})
expect(result.provenance).toEqual({ kind: 'npm', version: '1.2.3', resolved: RESOLVED, integrity: INTEGRITY })
})
it('forwards a configured registry through the real surface', async () => {
await createPacoteFetcher({ registry: 'https://npm.internal/' }).fetch(SOURCE)
expect(manifestMock).toHaveBeenCalledWith('plugin@^1.0.0', { registry: 'https://npm.internal/' })
})
})
@@ -1,107 +0,0 @@
import { describe, expect, it } from 'vitest'
import { resolvePluginSource, type GithubSource, type NpmSource } from '../src/source.ts'
describe('resolvePluginSource — github', () => {
it('parses owner/repo with a ref', () => {
expect(resolvePluginSource('unjs/template#main')).toEqual<GithubSource>({
kind: 'github', owner: 'unjs', repo: 'template', ref: 'main',
})
})
it('parses a bare owner/repo without a ref', () => {
expect(resolvePluginSource('deepseek-ai/plugin')).toEqual<GithubSource>({
kind: 'github', owner: 'deepseek-ai', repo: 'plugin',
})
})
it('parses a nested subdir with a ref', () => {
expect(resolvePluginSource('owner/repo/packages/plugin#v1.2.3')).toEqual<GithubSource>({
kind: 'github', owner: 'owner', repo: 'repo', subdir: 'packages/plugin', ref: 'v1.2.3',
})
})
it('parses a subdir without a ref', () => {
expect(resolvePluginSource('owner/repo/sub')).toEqual<GithubSource>({
kind: 'github', owner: 'owner', repo: 'repo', subdir: 'sub',
})
})
it('accepts a slash-nested ref', () => {
expect(resolvePluginSource('owner/repo#feature/x')).toEqual<GithubSource>({
kind: 'github', owner: 'owner', repo: 'repo', ref: 'feature/x',
})
})
it('trims surrounding whitespace before parsing', () => {
expect(resolvePluginSource(' owner/repo#main ')).toEqual<GithubSource>({
kind: 'github', owner: 'owner', repo: 'repo', ref: 'main',
})
})
it.each([
['empty ref after hash', 'owner/repo#'],
['single locator segment with hash', 'owner#main'],
['owner with @ and a hash', 'own@er/repo#main'],
['ref with whitespace', 'owner/repo#bad ref'],
['ref with traversal', 'owner/repo#a..b'],
['ref with a leading slash', 'owner/repo#/main'],
['ref with a trailing slash', 'owner/repo#main/'],
['ref with an illegal char', 'owner/repo#ma:in'],
])('rejects a malformed github spec (%s)', (_label, spec) => {
expect(() => resolvePluginSource(spec)).toThrow(/github plugin source|missing a ref/)
})
})
describe('resolvePluginSource — npm', () => {
it('parses an unscoped name@version', () => {
expect(resolvePluginSource('react@18.2.0')).toEqual<NpmSource>({
kind: 'npm', name: 'react', version: '18.2.0',
})
})
it('parses a scoped name@version', () => {
expect(resolvePluginSource('@deepseek-ai/dsh-tool-foo@0.0.1')).toEqual<NpmSource>({
kind: 'npm', name: '@deepseek-ai/dsh-tool-foo', version: '0.0.1',
})
})
it('accepts a dist-tag as the version', () => {
expect(resolvePluginSource('some-plugin@latest')).toEqual<NpmSource>({
kind: 'npm', name: 'some-plugin', version: 'latest',
})
})
it('accepts a range as the version', () => {
expect(resolvePluginSource('some-plugin@^1.0.0')).toEqual<NpmSource>({
kind: 'npm', name: 'some-plugin', version: '^1.0.0',
})
})
})
describe('resolvePluginSource — failures', () => {
it.each([
['empty', ''],
['whitespace only', ' '],
])('rejects a blank spec (%s)', (_label, spec) => {
expect(() => resolvePluginSource(spec)).toThrow(/must not be empty/)
})
it.each([
['bare word', 'plugin'],
['internal whitespace', 'owner repo'],
['empty npm version', 'pkg@'],
['scoped without version', '@scope/pkg'],
['scoped with empty scope', '@/pkg@1'],
['unscoped name with slash and version', 'foo/bar@1'],
['version containing a slash', 'foo@1/2'],
['uppercase unscoped name', 'FOO@1.0.0'],
['uppercase scope segment', '@Scope/pkg@1'],
['uppercase scoped name segment', '@scope/PKG@1'],
['empty scoped name segment', '@scope/@1'],
['dot-only owner', './repo'],
['traversal subdir segment', 'owner/repo/../x'],
['double slash subdir', 'owner/repo//sub'],
])('rejects an unrecognized/ambiguous spec (%s)', (_label, spec) => {
expect(() => resolvePluginSource(spec)).toThrow(/unrecognized plugin source|github plugin source/)
})
})
-15
View File
@@ -1,15 +0,0 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib/types"
},
"include": [
"src"
],
"references": [
{
"path": "../../util/brand"
}
]
}
-831
View File
File diff suppressed because it is too large. Load diff
@@ -52,7 +52,6 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'packages/sandbox/sandbox-local': { kind: 'indirect', reason: 'The provider backend delegates model rendering to dsh-bash-sandbox and dsh-tool-bash.' },
'packages/sdk/create-sdk': { kind: 'indirect', reason: 'The initializer only writes project files; selected runtime plugins provide the generated project model surface.' },
'packages/sdk/helper': { kind: 'none', reason: 'The project domain edits files and registers no live agent or model surface.' },
'packages/sdk/plugin-fetch': { kind: 'none', reason: 'The fetcher acquires plugin sources into a temp dir and registers no live agent or model surface.' },
'packages/sdk/scripts': { kind: 'indirect', reason: 'The launcher delegates model context to the loaded project plugin tree.' },
'packages/sdk/telemetry': { kind: 'none', reason: 'The launcher-side reporter sends developer-cycle telemetry and registers no live agent or model surface.' },
'packages/session-query/session-query': { kind: 'none', reason: 'The trusted query service exposes cloned records only to callers and registers no model surface.' },
-1
View File
@@ -99,7 +99,6 @@
{ "path": "./packages/sdk/helper" },
{ "path": "./packages/sdk/scripts" },
{ "path": "./packages/sdk/create-sdk" },
{ "path": "./packages/sdk/plugin-fetch" },
{ "path": "./packages/sdk/telemetry" }
]
}
-1
View File
@@ -110,7 +110,6 @@
{ "path": "./packages/sdk/helper" },
{ "path": "./packages/sdk/scripts" },
{ "path": "./packages/sdk/create-sdk" },
{ "path": "./packages/sdk/plugin-fetch" },
{ "path": "./packages/sdk/telemetry" }
]
}