Skip to content

fix(install): require only the distribution files a manifest declares - #182

Merged
dshplugin merged 3 commits into
dshplugin:mainfrom
SU-AN-coder:fix/declared-distribution-files
Oct 13, 2026
Merged

dshplugin merged 3 commits into
dshplugin:mainfrom
SU-AN-coder:fix/declared-distribution-files

Conversation

@SU-AN-coder

@SU-AN-coder SU-AN-coder commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #183

Summary

Both install channels reject a package whose manifest declares no entry file. A skill-only bundle declares dsh.bundle.patch and no main / exports at all; the entry check defaulted such a manifest to index.js and then demanded that file, so the git channel answered

[packaging] github:QingYunA/answer-me-with-html: plugin distribution is incomplete —
the entry file index.js declared in package.json is not present in the git distribution
(build output not committed). Report to the author.

for a distribution that is complete — and whose package.json never mentions index.js — and the failure dialog filed that on the author's repository: QingYunA/answer-me-with-html#209. The npm channel accepts the same shape by design, so the two channels disagreed about one package.

A host loads a bundle by reading its manifest and the patch that manifest names (dsh's own bundleManifest is documented as "Read bundle metadata without loading its JavaScript"), so the patch is what a bundle distribution has to contain; the JS entry is not needed at all for this shape.

The two bundle shapes have to be told apart, and the manifest alone cannot do it: a bundle whose patch names its own package — the pattern dsh's Packaging and installing a plugin guide shows (- insert: - name: <package>) — is imported by the host, so that one does need an entry after all. This branch reads the declared patches and asks for the entry only in that case.

Scope, from the catalogue (top 80 entries by stars; 32 of them have a readable root package.json): 31 declare dsh.bundle.patch; 4 declare no entry at all — QingYunA/answer-me-with-html, mnemon-dev/mnemon, thebiggavin/marketingdashboard, zuorn/tydora, and none of them resolves to an npm package whose metadata points back to its repository, which is what the market requires before it trusts the npm channel, so git is their only channel today; 4 declare an entry their git distribution does not contain (the guard's true positives — a prepare-built lib/ or dist/), one of which (hust-open-atom-club/oh-dsh) also declares a patch git does not contain, which the new patch check catches; 21 of the 31 mount themselves by name.

Changes

  • package-entry.ts: resolvePackageEntry returns null when the manifest declares neither exports["."] nor main (the index.js default is gone). New resolveBundlePatchFiles reads dsh.bundle.patch in both forms dsh's bundlePatchFiles accepts — a path, or a list of paths — and requiredDistributionFiles returns what a distribution must contain: the entry it declares, then the patches it declares.
  • A patch that names the package itself (patchRowNamesOf / mountsItself) puts the entry back into the requirement, so a bundle that mounts its own code is still checked. The reconnaissance is line-shaped, not a YAML parser, and whatever it cannot read is left unjudged rather than turned into a requirement.
  • preflight.ts (git channel) and verifyInstalledEntry (post-install, npm channel) both check those files, so the two channels keep agreeing; a bundle package now has to ship the patch it declares. The tar is listed once instead of once per file.
  • The preflight message, and the failure copy the dialog files on the author's repo (src/client/logic/urls.ts, src/client/locales.ts, both languages), name the file the package needs and where it comes from (its declared entry, an entry a patch mounts, or the declared patch) instead of calling every miss an entry file.
  • docs/architecture.md and docs/development.md: the pre-flight, post-install and test notes describe declared distribution files rather than "the entry file".

Type of change

  • Bug fix
  • New feature
  • Refactor
  • Documentation
  • Build / CI / release

How it was verified

  • npm run typecheck passes
  • npm test passes
  • npm run build passes

The decision itself, base (this branch's parent) vs this branch, computed with the hub's own functions on real codeload tarballs. preflightTarget cannot run directly from my network (its git ls-remote hits PREFLIGHT_TIMEOUT_MS, and that path returns ok: true by design), so I ran its steps against the downloaded archives:

distribution base this branch
QingYunA/answer-me-with-html@90c8d49d — dsh.bundle.patch, no main/exports entry=index.js → ok=false, missing=index.js required=['cordis.patch.yml'] → ok=true
wjvalue/dsh-answer-me-with-html@57fdca9d — same shape ok=false, missing=index.js ok=true
control: manifest declares main: ./lib/index.js, archive has no lib/ ok=false, missing=lib/index.js ok=false, missing=lib/index.js

A bundle whose declared patch is missing is now caught instead of passing unevaluated, which is the other half of the same rule.

  • node --test tests/package-entry.test.ts tests/preflight.test.ts — 14 tests, 14 pass. They pin each shape: a manifest that declares no entry has none, the patch rules, the archive-listing decision, and the self-mount rule (the skill-only patch from the report mounts nothing of its own, the guide's - insert: - name: <package> does).
  • npm run check on the final commit (dacafe6) — typecheck, 131 tests (129 pass, 0 fail, 2 skipped) and build: all pass.
  • Replay of the guard's decision on two real distributions (the root manifest and its declared patches read out of the codeload tarball) plus two controls:
case required verdict
QingYunA/answer-me-with-html@90c8d49d — patch rows ["@deepseek-ai/dsh-skill-filesystem"] ["cordis.patch.yml"] ok=true
wjvalue/dsh-answer-me-with-html@57fdca9d — same shape ["cordis.patch.yml"] ok=true
control: bundle whose patch names itself, no entry in the distribution ["index.js","cordis.patch.yml"] ok=false
control: the same bundle with index.js committed ["index.js","cordis.patch.yml"] ok=true
  • End to end on DSH 0.2.0-rc.2 with a clean DSH_HOME, installing the distribution from #209 (pnpm 10.34.7, the one the hub drives):
$ dsh plugin --profile web add github:QingYunA/answer-me-with-html
dependencies:
+ answer-me-with-html github:QingYunA/answer-me-with-html
Packages: +5
Done in 20.3s using pnpm v10.34.7

$ dsh plugin --profile web list
dsh-profile-web /tmp/dshhome2/profiles/web (PRIVATE)
│
│   dependencies:
└── answer-me-with-html@0.5.2

$ cat $DSH_HOME/profiles/web/package.json      # the bundle is registered, not a plain dependency
  "dsh": { "profile": { "bundles": ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app", "answer-me-with-html"] } }

$ ls $DSH_HOME/profiles/web/node_modules/answer-me-with-html
bin  cordis.patch.yml  docs  examples  locale  package.json  README.md  README.zh-CN.md  skills  src
                                                                (no index.js — as the manifest says)

$ dsh --profile web --dump-config | grep -A8 'answer-me-with-html'
# == answer-me-with-html
- id: answer-me-with-html-skill
  name: '@deepseek-ai/dsh-skill-filesystem'
  config:
    providerName: answer-me-with-html
    includeDefaultRoots: false
    bundledSkillDir: !!js >-
      process.getBuiltinModule('node:path').join(process.getBuiltinModule('node:path').dirname(process.getBuiltinModule('node:module').createRequire(baseUrl).resolve('answer-me-with-html/package.json')),
      'skills')

exit 0, no stderr: the bundle's patch composes into the plugin tree, and nothing imports the package.

Notes for reviewers

  • install.ts also gates the catalogue npm-target resolution on the preflight result (root.ok && root.name). An entry-less package now returns ok: true there, but that branch still requires a registry package under that name to point back to the repo and identify as a DSH plugin (isDshNpmPackageForRepo), so the outcome is unchanged for this shape — it only stops the guard from vetoing the lookup.
  • The self-mount rule is its own commit, so it can be dropped or split into a follow-up PR if you would rather review the two behaviours apart. Nothing in the catalogue combines "declares no entry" with "mounts itself" today, so it guards the shape rather than a live case.
  • The diff is ten files, +249/−69 (six source, two test, two docs); I read all of it before opening this.

A bundle that mounts a skill directory declares `dsh.bundle.patch` and no
`main` / `exports` at all. The entry check defaulted such a manifest to
`index.js` and then demanded that file, so the git channel rejected a complete
distribution with "[packaging] … the entry file index.js declared in
package.json is not present in the git distribution" and the feedback dialog
filed that as a bug on the author's repository
(QingYunA/answer-me-with-html#209).

A host loads a bundle by reading its manifest and the patch that manifest
names, never by importing the package (dsh's bundleManifest is documented as
"Read bundle metadata without loading its JavaScript"), and the npm channel
already accepts the same shape by design. Both channels now check the files the
manifest declares: the entry it names, when it names one, and the bundle patch
it declares.
The guard can now trip on a declared bundle patch as well as on a declared
entry, so the failure copy and the architecture notes say "a file declared in
package.json (main / exports / dsh.bundle.patch)" instead of calling every miss
an entry file.
A bundle's patch usually names the package it belongs to, and the host imports
that package when it does; a skill-only bundle names a provider instead and
needs no entry of its own. The guard now reads the declared patches and asks for
the entry only in the first case, so neither shape is judged by the other's
rules.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Pre-install guard invents an entry file for skill-only bundles: valid plugins are blocked on the git channel and reported to their authors

2 participants