DeepSeek Harness(以下 dsh)のデプロイ実践において、最もよく聞かれる質問は「プラグインの書き方」ではなく、「設定したはずなのに、なぜ設定した値が有効にならないのか」というものです。この問題の背景には実は三つの経路が横断しています。プラグインが Config インターフェース + 同名の Schemastery schema を通じて注入可能なフィールドをどのように公開するのか。コンポジションパッケージ bundle と profile という二つの manifest がそれぞれどのような役割を担うのか。そして複数レイヤーの patch が重なる際、最終的に有効な設定がどのような順序で決定されるのか。本記事は「プラグイン設定と Bundle / Profile の階層化:dsh の有効設定は結局どこから来るのか」の第 1/2 段として、「設定はどこから来るのか」の前半を徹底的に解説します。cordis.yml からプラグイン apply へのフィールドマッピング、デフォルト値がなぜ schema フィールドに書かれるのか、設定とコードの分離の境界はどこにあるのか、bundle と profile の dsh キーがそれぞれ何に答えるのか、そして profile がなぜインストールディレクトリの外に置かれるのか。後半では続けて profile へのインストール、読み込み順序の階層化、pnpm 転送と bundles の自動メンテナンスの完全な経路を解説します。この段を理解してこそ、「設定を変えたのに有効にならない」というときに、まずコードを何度も書き換えるのではなく、レイヤーを特定しようとすることができるのです。

cordis.yml からプラグイン apply へ:Config インターフェースが注入可能フィールドをどのように定義するか

dsh のプラグインモデルは cordis の依存性注入コンテナの上に構築されています。プラグインが読み込まれる際、コンテナはそれがエクスポートする apply 関数を呼び出し、二つの引数を渡します。第一は Context(現在のプラグインから見えるコンテキスト。サービスのアクセス、コマンドの登録、子プラグインのマウントなどに使用)、第二が本記事の主役——検証済みの設定オブジェクトです。つまり、cordis.yml に書いたキーは最終的に解析・検証・デフォルト値の補完を経て、apply の第二引数としてあなたの手に渡されます。この経路の端点は非常に明確です:cordis.yml で渡されたキー → プラグインがエクスポートする Config インターフェース → apply の第二引数

この経路を成立させるには、プラグインは二つのものをエクスポートする必要があり、しかも名前が関連していなければなりません。Config インターフェースと、同名の Config schema です。前者は TypeScript コンパイラに見せる型契約であり、後者はランタイムに見せる検証とデフォルト値のソースです。多くの人が初めてプラグインを書くとき戸惑います:なぜインターフェースと schema は同名でなければならないのか?なぜなら dsh のローダーはランタイムで名前から schema を見つける必要があり、TypeScript 側では名前から型を見つける必要があるからです。同名にすることで「型」と「ランタイム schema」が同一シンボルの二つの形態となり、ローダーは schema を取得して検証し、エディタはインターフェースを取得して補完を行い、互いに衝突しません。

具体的な Config インターフェースを見てみましょう。ここではプラグインを my-plugin とし、greeting、maxRetries、verbose の3つのフィールドを受け取るとします。インターフェースは次のように書けます:

import type { Context } from '@deepseek-ai/cordis'
import Schema from '@deepseek-ai/schemastery'

export const name = 'my-plugin'

// Config 接口:定义插件接受哪些配置项
export interface Config {
  greeting: string // 问候语
  maxRetries: number // 最大重试次数
  verbose?: boolean // 是否输出详细日志(可选)
}

3つのフィールドのオプショナル性の違いに注意してください。ここが本節で最も見落とされやすく、最もハマりやすいポイントです。greeting と maxRetries は必須フィールド(疑問符なし)であり、型のレベルでは、Config 型の値は必ずこれらを提供しなければならないことを意味します。verbose はオプショナルフィールド(疑問符あり)であり、型としては省略を許容することを意味します。しかしこの「オプショナル」は、実行時に「放置してよい」という意味ではありません。それは単に、書かなくても TS コンパイラがエラーを出さないというだけのことです。「ユーザーが書かなかったときに何が起こるか」を実際に決めるのは、次節の schema のデフォルト値です。型レベルのオプショナル性と、実行時のデフォルト値の補完は別物であり、分けて理解しなければなりません。

もう一つ、マッピングの細かい点に注意してください。cordis.yml で渡されるキー名は、Config インターフェースのフィールド名に直接対応します。yml に greeting と書けば、schema にも greeting というキーがなければなりません。max_retries(アンダースコア形式)と書いても maxRetries にはマッピングされず、結果は検証失敗になるか、フィールドが値を受け取れずデフォルト値に落ちるかのどちらかです。dsh はキャメルケースとアンダースコアの自動変換を行いません。この点は一部のフレームワークとは異なり、設定キーとフィールド名は一字一句一致させる必要があります。

では、apply の中でこれらの設定をどう使うのでしょうか?非常に直接的です:

export function apply(ctx: Context, config: Config) {
  // 打印的是用户传入的值或 schema 默认值
  console.log(config.greeting)
}

ここでの config はすでに「マージ後の結果」です。ユーザーが greeting を明示的に渡していればユーザーの値、渡していなければ schema の default、渡したが型が正しくない場合(例えば maxRetries を文字列 "3" と書いた場合)は、apply に入る前に schema によって弾かれます。言い換えれば、apply が受け取るのは常に、すでに補完され、すでに検証済みで、構造が完全な設定です。プラグイン作者は apply の中で「渡されなかったら……」といったフォールバックロジックを書く必要はありません。フォールバックは schema の責務です。

なぜこの設計がエンジニアリング上重要なのか?それは「設定の正当性」と「ビジネスロジック」を完全に分離しているからです。schema がなかった時代を想像してみてください。プラグイン作者は apply の中に config.maxRetries ?? 3typeof config.verbose === 'boolean' ? config.verbose : false といったコードを大量に書き、各プラグインがそれぞれ独自に書き、エラー処理のスタイルも千差万別で、ユーザーが誤った型を渡したときの動作は予測不可能でした。今ではこれらはすべて schema の一箇所に収束し、プラグイン作者は「正当な設定を受け取った後に何をすべきか」だけを考えればよくなりました。

示意图
cordis.yml の設定キーから、Config インターフェースと Schemastery schema による検証・デフォルト値の補完を経て、最終的にプラグイン apply の第2引数へ注入されるまでの完全な経路。

この経路を理解した上で、上級読者がよく気にする点がもう一つあります。apply の第2引数は設定だが、「アプリケーションパラメータ」はどこへ行ったのか?この点は後で読み込み順序を説明する際に明確になります——アプリケーションパラメータは別の patch 層ではなく、通常のアプリケーション固有のサービスを通じて解決されます。つまり、コマンドラインパラメータと設定の階層化は二つの異なる仕組みであり、これらを混同してはいけません。現時点では、apply(ctx, config) の config は設定階層化体系の産物であり、コマンドラインから直接渡されたものではない、ということだけ覚えておけば十分です。

Schemastery 同名 schema:デフォルト値をなぜ apply ではなくフィールドに書くのか

前節で繰り返し「デフォルト値」に触れましたが、本節ではその帰属を明確にします。dsh プラグイン設定のデフォルト値は、apply 関数内ではなく、同名でエクスポートされる Config schema のフィールドに書きます。この規約の書き方は export const Config: Schema<Config> = Schema.object({ ... }) です。ここでの巧妙さに注目してください。インターフェース名は Config、定数名も Config で、前者は型、後者は値であり、TypeScript はこの同名の型/値の二重宣言を許可しています。ローダーが名前で取得するのはその定数(schema)であり、型システムが用いるのはそのインターフェースです。

完全な schema はおおよそ次のようになります:

// 同名的 Config schema:默认值写在这里
export const Config: Schema<Config> = Schema.object({
  greeting: Schema.string().default('Hello'),
  maxRetries: Schema.number().default(3),
  verbose: Schema.boolean().default(false),
})

この schema の情報量を行ごとに分解します:

  • Schema.object({...}) はこれがオブジェクト型の schema であることを宣言し、そのキー集合がプラグインが受け付ける設定フィールドの集合になります。このオブジェクトに含まれないキーは、最終的な config に入りません。
  • Schema.string().default('Hello') は greeting が文字列であり、デフォルト値が 'Hello' であることを宣言します。ユーザーが greeting を書かない場合、config.greeting は 'Hello' になります。ユーザーが文字列以外(例えば数値)を書いた場合、検証段階で失敗します。
  • Schema.number().default(3) は maxRetries が数値であり、デフォルト値が 3 であることを宣言します。これは素材で示された具体的な数値であり、リトライ系フィールドとしてごく自然なデフォルトの段階でもあります。
  • Schema.boolean().default(false) は verbose がブール値であり、デフォルト値が false であることを宣言します。インターフェース上で verbose がオプショナルであっても、schema はそれに明確なデフォルト値を与えているため、実行時の config.verbose は常に true か false になり、undefined にはなりません。

なぜデフォルト値は apply の中ではなくフィールドに書かなければならないのでしょうか?いくつかの段階的に積み重なる理由があり、後になるほどエンジニアリングの層面に近づきます:

  1. 単一の信頼できる情報源。デフォルト値を apply の中に書くと、「このプラグインがどのような設定をサポートし、それぞれのデフォルトが何か」という事柄がコードロジックの途中に散在し、ユーザーはドキュメント、schema、コードの三箇所の情報を突き合わせなければならなくなります。schema に書けば、schema が唯一の権威となり、ドキュメントは自動生成でき、型も推論できます。
  2. 検証と補完は apply に入る前に完了しなければならない。apply はビジネスロジックであり、それが実行を開始した時点で、設定はすでに合法であると認定されたことを意味します。もしデフォルト値の補完を apply の中に置くなら、apply 内部の補完前のあらゆるコードは「設定が不完全である」という前提で動作しなければならず、これによりプラグイン作者は大量の防御的コードを書くことになります。
  3. オプショナルフィールドの意味は schema が支える必要がある。verbose に戻ると、インターフェース上ではオプショナルですが、ビジネスコードでは必ずブール値であってほしいはずです。schema の .default(false) がちょうどこの変換を完了します——型の層では欠落を許容し、実行時の層では存在を保証します。これが「オプショナル」という設計意図の正しい実現方法です:インターフェースではオプショナルと注記し、schema でデフォルト値を補う、両者が協調するのであって互いに依存するのではありません。
  4. 階層的な上書きではデフォルト値の位置が挙動を決める。デフォルト値は schema が属する層に属し、ユーザー層は自分が関心のあるキーだけを書き、上書きされていないキーは自然にその層のデフォルト値に落ちます。もしデフォルト値を apply の中にハードコードすると、階層的な上書きの議論は「コード内の if 分岐」になってしまい、完全に制御不能になります。

もう一度強調します。素材が示す判断基準は、デフォルト値は schema フィールドに直接書くということです。これはスタイルの好みではなく、dsh の設定メカニズムの一部です。初心者によくある間違いは、Schema.string() を default なしの形で書き、そして「ユーザーが書かないとき config.greeting は何になるのか」と疑問に思うことです。答えは、それはバリデータがそのフィールドをどう扱うかに依存し、undefined になることもあれば、そのままエラーになることもあります——しかしどちらにせよ、あなたはそのような不確実性に依存すべきではありません。インターフェースでオプショナルとして宣言されているフィールド、または確定した初期値を持たせたいフィールドは、すべて schema 上で明示的に .default() を与えるべきです

ここでさらに一つの対比を挙げると、上級読者が直感を養う助けになります。プラグイン設定の schema 検証は、よくある「環境変数 + 手動 parse」の方案と比べて、差異は以下のとおりです:

次元Schemastery 同名 schema環境変数 + 手動 parse
型宣言の来源Config インターフェースと schema が同名で、型とランタイムが一致ドキュメントの約束に頼り、ランタイムは手書きの typeof 判断に頼る
デフォルト値の位置schema フィールド上の .default()各所に散らばった || と ??
検証のタイミングapply に入る前に、統一的に検証使うところだけを検証し、早さは不定
エラーフィードバック検証失敗で即拦截し、具体的なフィールドを特定多くの場合、ある行まで実行して初めて崩れ、スタックは設定を指さない
階層的オーバーライドpatch 層が行単位で置換し、schema がデフォルトを提供階層の概念がなく、後書きが先書きを上書きするのは順序次第
ドキュメント同期schema がすなわちドキュメントで、読めるし推論できるドキュメントとコードが乖離しやすい

この表の核心的な結論は、schema は「設定がどのような形をしているか」を実行可能な契約に変えるということです。一方、手動 parse は同じ情報をコードで展開するだけで、しかも階層体系の中で統一的に扱うことができません。dsh のような多層 patch の重ね合わせを重視するシステムでは、schema という統一的な入口がなければ、階層的オーバーライドはそもそも成り立ちません。

最後に実務上の細部を一つ注意喚起します。schema とインターフェースのフィールド集合は一致しているべきです。インターフェースにフィールドを追加したのに schema に追加し忘れると、ユーザーがそのキーを渡しても config には入りません。schema にフィールドを追加したのにインターフェースに追加していないと、TS 側ではそれが見えず、apply で使いたければキャストする必要があります。両者は同期して進化しなければならず、これがインターフェースと schema を同じファイルに、すぐ隣り合う形で書くエンジニアリング上の理由でもあります。

設定とコード分離の境界:どの値を schema に入れ、どの値をプラグイン内に残すべきか

素材の冒頭では、非常に典型的な反例が示されています:greet ツールが挨拶文をコード内にハードコードしており、異なるデプロイで変更したい場合はコードを修正する必要がある。この一文は、実は「設定とコードの分離」の動機のすべてを要約しています——異なるデプロイ間で異なる可能性があり、かつ変更する際にコードを修正すべきでないものは、設定として外部化すべきであり、プラグイン自身の固有の動作で、変更の有無がユーザーに影響を与えるべきでないものは、コード内に残すべきです。

ある値が schema に入れるべきか、プラグイン内に残すべきかを判断するには、以下の基準を順に確認します:

  • デプロイによって異なるか?挨拶文は教科書的な例です:テスト環境では 'Hello' と言いたいが、本番環境では 'こんにちは' と言いたい、異なる顧客のデプロイではそれぞれ異なる文言が必要かもしれません。それは変わるので、schema に入れます(greeting)。逆に、プラグイン内部のある定数が文字列プレフィックスの結合に使われ、すべてのデプロイで同じであれば、外部化する必要はありません。
  • 環境/負荷によって異なるか?maxRetries はこのカテゴリに属します:ネットワークが不安定な環境ではリトライ回数を増やしたい、安定した環境では時間を節約するために減らしたい。このような「同じコード、異なる実行条件で異なる値が必要」なパラメータは、自然に schema に入れるのに適しています。
  • トラブルシューティング/観測のスイッチか?verbose は典型的なデバッグスイッチです:普段はオフにしてログを節約し、トラブルシューティング時にオンにして詳細を確認します。このようなブールスイッチはほぼすべて外部化すべきであり、デフォルト値は通常「静か」な方——素材で示されているデフォルトは false です。
  • 鍵、パス、エンドポイントに関わるか?資格情報、ファイルパス、サービスアドレスは、ほぼ必然的にデプロイによって異なるため、必ず外部化し、リポジトリにハードコードしてはいけません。
  • アルゴリズムの不変量に属するか?ある内部状態機械の状態数、プロトコルで固定された magic number、ハッシュアルゴリズムの選定——これらは変更すれば動作が変わり、コードとテストの修正が必要になるため、「設定可能」にするのは適切ではありません。それらを外部化すると、かえって設定面が広がり、組み合わせが爆発し、保守が難しくなります。

この基準で素材内の 3 つのフィールドを振り返ると、境界は明確です:greeting は「デプロイによって異なる」文言なので外部化;maxRetries は「環境によって異なる」チューニングパラメータなので外部化し、デフォルトとして 3 を設定;verbose は「トラブルシューティングスイッチ」なので外部化し、デフォルトは false。3 つのフィールドはすべて外部化され、schema 上でそれぞれデフォルト値を持っています。サンプルコードの apply は 1 つのことだけを行います:console.log(config.greeting)——それは設定を消費するのであり、設定を決定するのではありません。これこそが「設定とコードの分離」が目指す状態です。

verbose のようなオプションのブール項目については、見落とされがちな処理方法の問題がもう 1 つあります。インターフェースにすでに verbose?: boolean と書かれているので、schema では省略できる、あるいは default を書かなくてよいと考える作者もいます。どちらの方法も推奨されません:

  • schema でオプションフィールドを省略しないでください。省略すると、ユーザーが verbose を渡しても認識されず、config にも入らないため、このスイッチは形骸化します。schema のフィールド集合はホワイトリストであり、列挙されていないキーは通過しません。
  • undefined で「オフ」を表現することを当てにしないでください。業務コードで if (config.verbose) と書けば一見動作しますが、ログロジックが「未設定」と「明示的な false」を明示的に区別する必要がある場合、undefined は曖昧さをもたらします。schema が .default(false) を与えた後は、セマンティクスは確定します。つまり true か false のいずれかです。
  • デフォルト値は「安全」な側を選ぶべきです。デバッグスイッチにとって安全側はオフ(false)であり、リトライにとって安全側は有限回数(3)であり、無限ではありません。デフォルト値の選択自体が一種のエンジニアリング判断であり、schema はその落とし所です。

ここでさらに比較表を示し、「外部化すべき」と「インラインにすべき」の判断を具体的な型に落とし込み、実際のプロジェクトで照合しやすくします:

値の型schema へ外部化するか典型的なフィールドとデフォルト値理由
ユーザー向けの文言はいgreeting、デフォルト 'Hello'デプロイ/顧客ごとに異なる可能性があり、文言の変更でコードを変えるべきではない
チューニングパラメータはいmaxRetries、デフォルト 3環境と負荷に応じて変化し、現場で調整可能である必要がある
トラブルシューティングスイッチはいverbose、デフォルト false普段は静か、調査時にオン、デフォルトは安全側を取る
シークレット/エンドポイント/パスはいデプロイごとに注入、通常は安全なデフォルトなしデプロイ間で必ず異なり、リポジトリに入れてはならない
プロトコル定数/magic numberいいえプラグイン内に残す変更は挙動の変更であり、コードとテストの変更が必要
アルゴリズム/状態機械の内在ロジックいいえプラグイン内に残す実装の不変条件に属し、外部化すると組み合わせが爆発する

一文にまとめると:設定面の大きさは設計上のトレードオフであり、外部化の基準は「デプロイ時に可変であり、コードを変えるべきではない」ことであって、「設定できるものは何でも設定する」ことではありません。外部化すべきでないものを設定にすると、短期的には柔軟に見えますが、長期的には設定の組み合わせが制御不能になり、デフォルト値のセマンティクスが曖昧になり、トラブルシューティングコストが上昇します。dsh の schema メカニズムは外部化する能力を与えますが、使うかどうか、どれだけ使うかは依然としてプラグイン作者の判断です。素材にあるその 3 つのフィールドはちょうど「文言、チューニング、スイッチ」という 3 つの典型的な外部化シナリオをカバーしており、非常に合理的な最小設定面です。

bundle と profile:dsh キー配下における 2 種類の manifest の役割分担

単一プラグインの設定について説明したので、視点を「これらのプラグインがどのようにパッケージ化され、インストールされ、起動されるか」へと引き上げる必要があります。dsh のインストール機構は 2 つの概念の上に成り立っています:bundle(バンドル)profile です。これらはどちらも 1 つの package.json によって記述されますが、dsh キー配下で保持する manifest の種類が異なり、答える問いも異なります。これは階層体系全体を理解するための鍵となる前提です。

まず 2 つの概念の定義を提示します:

  • バンドル(bundle)は、設定レイヤーを伴う npm パッケージです。その manifest は dsh.bundle を宣言し、「このパッケージが何を提供するか」という問いに答えます——具体的には、プラグイン行を挿入または上書きする patch ファイルを提供します。
  • profile は $DSH_HOME/profiles/<name> 配下に位置し、起動可能な構成を記述するディレクトリです。その manifest は dsh.profile を宣言し、「この設定がどのバンドルによって、どのような順序で構成されるか」という問いに答えます。

両者の役割分担は次のように覚えられます:bundle はあなたが作成し配布するもの;profile はユーザーが dsh --profile <name> で起動するものです。この一文は繰り返し読む価値があります。プラグイン作者は bundle を書き、patch ファイルをコードとともに npm(またはローカル checkout)へ公開します;ユーザーは bundle を手作業で組み立てる必要はなく、profile を起動します。profile には「どの bundle を、どの順序で読み込むか」が記録されています。資料には断固とした結論が 1 つあります:両方であるものは存在しない。あるパッケージは bundle か profile のどちらかであり、その身分は相互排他的です。両者の記述が package.json の dsh キー配下に隠れていても、dsh.bundle と dsh.profile は 2 つの異なる manifest 種別です。

対比表で両者の差異を確定します:

概念manifest キー答える問い誰が作成 / 誰が使用
bundle(バンドル)dsh.bundleこのパッケージが何を提供するか(1 つの patch ファイル)プラグイン作者が作成し、パッケージとともに配布
profiledsh.profileこの設定がどの bundle によって、どのような順序で構成されるかdsh plugin によって自動作成・保守され、ユーザーが起動

この表には、掘り下げる価値のある情報ポイントが三つある。第一に、manifest キーが異なる:bundle は dsh.bundle を、profile は dsh.profile を使い、ローダーはこれに基づいてそのパッケージが体系内でどの役割を担うかを判断する。第二に、答える問いが異なる:一方は「何を貢献するか」(内容の視点)であり、もう一方は「誰によって構成されるか」(編成の視点)である。第三に、作成者と利用者が異なる:bundle はプラグイン作者が作成し、パッケージに同梱して配布される公開成果物であり、profile は dsh plugin コマンドによって自動的に作成・保守されるユーザー側の実行設定である。

もう一つ混同されやすい細部がある:素材は「表層の組み合わせパッケージは、通常のアプリケーション固有のサービスを通じてそれらを解決できる」と一言で指摘している。この文の意味は、bundle が提供する patch が挿入または上書きするプラグイン行は、それ自体が通常どおりアプリケーションサービスの解決に参加でき、「bundle 内のプラグインは特殊な種族である」ということは存在しない、ということである。上級読者にとって、この点を理解することは正しいメンタルモデルを構築する助けになる:bundle は新しいランタイムではなく、既存のプラグイン行に設定レイヤーを一つ加えるものにすぎない

次にインストール機構の全体像を見る。インストール機構は二つの概念の上に成り立っており、その両方が一つの package.json によって記述される。これはつまり、あなたが dsh plugin add でとあるパッケージを指定したとき、dsh はそのパッケージの package.json を読み、それが dsh.bundle と dsh.profile のどちらを宣言しているかを見て、それを体系内のどこに配置するかを決定する、ということである。もし dsh.bundle を宣言していれば、dsh はそのパッケージを対象 profile の dsh.profile.bundles リストに追加する;profile 自体は dsh plugin によって初回使用時に初期化される。この「manifest を読んで役割を決定する」プロセスこそが、二つの manifest の分業が実現された姿である。

示意图
有効な設定は空のルートの上に、profile bundles、profile 自身の patch、home レベルの patch、コマンドライン overlay の四層の順で積み重ねられ、後から適用される層が行単位で勝ちを取る順序関係にある。

最後に、よくある誤解を一つ正しておく:一部の読者は bundle と profile が「包含関係」にある、つまり profile が bundle を含むので bundle も profile の一種である、と考えている。そうではない。profile の bundles リスト内で bundle を参照することは参照関係であり、身分の継承ではない。ある bundle が複数の profile から参照されても、それ自体は依然としてただの bundle である;ある profile がいくつかの bundle を参照しても、それ自体は依然としてただの profile である。素材の「何ものも同時に両方ではありえない」というのは、まさにこの身分の混同を断ち切っている。この点をしっかり覚えておけば、後で bundles 配列の順序や上書き関係を論じるときに、「誰が誰を含むか」と「誰が誰を上書きするか」を混同せずに済む。

dsh.bundle が宣言するもの:プラグイン行を挿入または上書きする patch ファイル

前節では bundle が「このパッケージが何を提供するか」に答えると述べたが、本節ではその「何」を具体化する:bundle の核心的な成果物は patch ファイルである。その役割はプラグイン行を挿入または上書きすることだ。これは dsh のコンポジションパッケージ機構において情報密度が最も高い一文であり、一語ずつ分解して見る価値がある。

まず「patch ファイル」を見る。dsh の設定階層体系において、patch は「層ごとの合成」に参加する基本単位である。有効な設定は一枚のハードコードされた yml ではなく、空のルートから始まり、固定された順序で複数の patch を順に重ね合わせた結果である。bundle が携えるこの patch ファイルこそ、それが一層として合成に参加するための根拠である。ユーザーが cordis.yml や profile で目にする多くのプラグイン行は、実際にはある bundle の patch が適用された産物である。

次に「プラグイン行を挿入または上書きする」を見る。これは patch の二つの基本動作を明らかにしている:

  • 挿入:元の設定にこのプラグイン行が存在しない場合、bundle の patch がそれを追加する。これは「システムに新しい能力を追加する」シナリオに対応する。例えば hello-plugin をインストールすると、その patch が hello 関連のプラグイン行を設定に挿入する。
  • 上書き:元の設定にすでにこのプラグイン行が存在する場合、bundle の patch がその設定を置き換える。これは「既存の動作をカスタマイズする」シナリオに対応する。例えばある bundle が基礎パッケージ内のあるプラグインのパラメータを調整したい場合などだ。

ここで一つの重要な機構を引き合いに出す必要がある。素材は読み込み順序を説明する際に明確な結論を示している:patch は対象行の config 値全体を置き換えるのであり、各キーを深くマージするのではない。この点は bundle の挙動を理解する上で極めて重要である。基礎設定のあるプラグイン行が { greeting: 'Hello', maxRetries: 3 } であり、あなたの bundle patch が { greeting: '您好' } しか書いていないと仮定しよう。すると上書き後のこの行の config は { greeting: '您好' } となり、maxRetries は基礎設定から「継承」されない——より高い優先度の層で再び補われるか、そのプラグイン自身の schema のデフォルト値で補われる場合を除いて。これは非常に頻度の高い落とし穴である:多くの人は patch を「パッチを当てるように一つのキーだけを変更するもの」と考えるが、実際は行全体の置き換えである。解決策は通常二つある:patch にこの行のすべてのキーを書き切るか、省略されたキーが schema のデフォルト値によって正しい意味論を補えることを確認するかである。

なぜ bundle の成果物は他の形式ではなく patch ファイルなのか?それは dsh の階層モデルが「積み重ね可能・順序付け可能・行単位で勝敗が決まる」設定単位を必要とするからである。patch ファイルはまさにこれを満たす:積み重ね可能(複数の bundle の patch が順に適用される)、順序付け可能(bundles 配列の順序がそのまま適用順序である)、行単位で勝敗が決まる(後に適用された層が先に適用された層を上書きする)。もし bundle が直接「最終設定」を産出するなら、階層的に積み重ねる能力を失い、複数の bundle 間で調整することもできなくなる。

配布の観点から見ると、素材は bundle がプラグイン作者によって書かれ、パッケージと共に配布されると明確に述べている。これは明確な責務の線引きである:

  1. プラグイン作者がプラグインコードを書く(Config インターフェースと同名の schema をエクスポートする)。
  2. プラグイン作者が patch ファイルを書き、このパッケージがどのプラグイン行を挿入または上書きするかを宣言する。
  3. プラグイン作者が package.json の dsh キーの下で dsh.bundle を宣言し、この patch を指し示す。
  4. パッケージが公開されるか、ローカルで checkout される。
  5. ユーザーが dsh plugin を通じて、それをある profile にインストールする。

この5つのステップのうち、最初の4つは bundle 側の事柄であり、5番目で profile 側に入る。bundle 作者は、ユーザーがどの profile で起動するかを気にする必要はなく、自分がどう組み合わされるかを仮定すべきでもない。それはただ「自分が何を貢献するか」に答えるだけである。この境界感覚がエコシステムのコンポーザビリティの基盤である:bundle が越境しないからこそ、同じ bundle が任意の数の profile で再利用できるのだ。

さらに、上級の読者が追问するであろう点がある:bundle の patch とユーザー自身の patch(例えば profile レベルの cordis.patch.yml、home レベルの patch、コマンドラインの --patch overlay)は、仕組み上同じものなのか?答えは、それらはすべて「層」であり、「後から適用される層が行単位で勝ち残る」というルールに従う。違いは位置と優先度だけである。bundle の patch は dsh.profile.bundles リストを通じて体系に入り、階層順序の第一档に位置する。ユーザーの patch はその後に並ぶため、ユーザーは常に bundle の設定を上書きできる。この「bundle がデフォルトを提供し、ユーザーが最終的な発言権を持つ」という設計は、dsh の階層体系における非常に実用的な選択である。

$DSH_HOME/profiles/<name>:profile がインストールディレクトリの外に置かれる理由

次に視点を profile に切り替える。素材は非常に正確な位置づけを与えている:profile は $DSH_HOME/profiles/<name> の下にあり、起動可能な組み合わせを記述するディレクトリである。そして同様に正確なパスの説明:profile はインストールディレクトリの外にあり、パステンプレートは $DSH_HOME/profiles/<name> である。この「インストールディレクトリの外」は恣意的な選択ではなく、profile の位置づけの直接的な帰結である。

まず profile とは何かを理解しよう。それは起動可能な組み合わせを記述する——つまり、profile 自体は実行可能なコードではなく、インストール産物でもなく、一份の「起動レシピ」である:どの bundle が必要か、どの順序で、どのユーザー patch を重ねるか。その manifest は dsh.profile を宣言し、核心的な内容は dsh.profile.bundles 配列である。ユーザーが dsh --profile <name> で起動すると、dsh はこのパスへ行き profile 記述を読み、それに基づいて有効な設定を組み立て、そして起動する。

なぜこのような「レシピ」をインストールディレクトリの外に置くのか。いくつかの観点から理解できる:

  • インストールディレクトリは交換可能で、アップグレード可能である。dsh 本体とその依存関係は再インストール、アップグレード、バージョン変更される可能性がある。もし profile をインストールディレクトリ内に書いていたら、一度のアップグレードでユーザーの起動レシピが消えたり上書きされたりするかもしれない。profile を $DSH_HOME の下に置けば、インストールディレクトリがどう変わろうと、ユーザーの設定資産は影響を受けない。
  • profile はユーザー側、マシンローカルの資産であり、配布物ではない。bundle はプラグイン作者が配布するもので、profile はユーザー/マシン自身のものである。ユーザーは異なるマシンで異なる profile を作るかもしれず、これらは配布物にパッケージされるべきではなく、パッケージに追随すべきでもない。
  • 1 つの profile は複数の来源からの bundle を参照できる。profile の bundles リストには @deepseek-ai/dsh-base のような公式ベースパッケージもあれば、ローカルで checkout したサードパーティパッケージもある。これには profile が「中立的で、特定のパッケージに独占されない」位置にあることが求められ、$DSH_HOME/profiles はまさにそのような中立なパスを提供する。
  • 複数 profile の並存の必要性。パステンプレート内の <name> は、複数の profile(demo、prod、debug……)が同一マシン上に並存し、それぞれが異なる起動構成を記述し、同一のインストールを共有できることを意味する。これは profile がインストールディレクトリから独立している場合にのみ容易に行える。

profile を $DSH_HOME に置くことにはもう一つ暗黙の利点がある:それはマシンローカルの選好と共有設定の自然な落としどころである。素材は読み込み順序を説明する際に $DSH_HOME/cordis.patch.yml を home レベルの層として挙げており、$DSH_HOME が単なる profile の容器ではなく、「各 profile が共有するマシンローカルの選好」の保管場所でもあることを示している。profile が $DSH_HOME/profiles/<name> に位置することは、まさにこの体系の自然な延長である。

ここで「profile をインストールディレクトリ内に置く」場合と「インストールディレクトリ外に置く」場合の差異を比較し、判断の助けとすることができる:

次元インストールディレクトリ内に置く$DSH_HOME/profiles/<name> に置く
dsh アップグレード時上書きまたは消失の可能性影響を受けず、資産は保持される
複数 profile の並存追加の分離が必要で、互いに干渉しやすい<name> により天然に分離される
配布パッケージとの関係誤って配布物にパッケージされやすい明確にユーザー側に属し、パッケージに追随しない
home レベル patch との関係共有層を構築しにくい同じ $DSH_HOME にあり、選好の共有が自然に成立する
起動時の bundle 参照パスがインストール位置に結合する参照が中立で、来源は多様であり得る

よくある混同点を明確にしておきます。profile のディレクトリ名 <name> は、起動時の dsh --profile <name> にある <name> そのものです。したがって、demo という profile のパスは $DSH_HOME/profiles/demo であり、起動は dsh --profile demo になります。名前は一意の識別子であり、dsh はそれによって $DSH_HOME/profiles 配下のディレクトリを特定します。これが、資料で profile が「dsh plugin によって自動的に作成・維持される」と述べられている理由でもあります。ある profile を初めて使うとき、dsh plugin は $DSH_HOME/profiles/<name> 配下でそれを初期化し、ユーザーは通常、手動でディレクトリを作成したり package.json を書いたりする必要はありません。

もう一点、後続のインストール手順に直接関係する事柄があります。資料は「初回使用時に profile が初期化される」と述べ、さらに非常に具体的な詳細を示しています——@deepseek-ai/dsh-base がその最初の組み合わせパッケージになる。この一文は情報量が非常に大きいです。つまり、まったく新しい profile の dsh.profile.bundles リストでは、最初の項目は常に @deepseek-ai/dsh-base になります。これは階層順序の第一段であり、ユーザーがインストールしたすべての bundle はその後に並びます。これにより、後からインストールした bundle が基礎パッケージの動作を上書きできる理由も説明できます——順序が勝敗を決めるのです。profile はインストールディレクトリの外にありますが、それが参照する最初の bundle は公式の基礎パッケージに由来します。この「位置は分離、参照は結合」という構造こそが、階層体系の設計意図なのです。

dsh.profile.bundles リスト:この設定がどの組み合わせパッケージによって、どのような順序で構成されているか

profile の中核となる内容はまさに dsh.profile.bundles リストであり、それが答える問題は資料に明確に書かれています:この設定がどの組み合わせパッケージによって、どのような順序で構成されているか。ここには二つの要素があることに注意してください——どれか(メンバー)とどのような順序か(並び順)です。この両者が共同で最終的に有効になる設定を決定し、どちらも欠かせません。

まずメンバーの次元を見ます。bundles 配列の各項目は、一つの組み合わせパッケージ名です。資料はインストール手順の中で実際のリストを示しています:まず dsh-base、次にインストール済みの各組み合わせパッケージがその追加順に従って並ぶ。つまり、初期化後の profile では、bundles はおおよそ ['@deepseek-ai/dsh-base', '...ユーザーがインストールしたパッケージ...'] の形をとります。公式の基礎パッケージは常に先頭に立ち、ユーザーがインストールしたパッケージが順にその後ろに続きます。

次に順序の次元を見ます。順序がなぜ重要なのでしょうか。それは階層体系が後から適用された層が行単位で勝つという規則に従うからです。bundles 配列の順序は patch の適用順序です。配列内で前にある bundle が先に適用され、後ろにあるものは後から適用されます。二つの bundle が同じ行のプラグイン設定に触れる場合、後から適用されたものが勝ちます。したがって、bundles 配列は単なる「リスト」ではなく、優先度のシーケンスです。ある bundle をリストのより後ろの位置に置くことは、それにより高い上書き優先度を与えることと同じです。これは profile レベルで最も実用的な調整手段の一つです。

先に説明した「patch は config を深くマージするのではなく行全体を置換する」を踏まえると、具体的なシナリオを推論できます。ベースパッケージ dsh-base の patch がとあるプラグインの行を挿入し、設定が { greeting: 'Hello', maxRetries: 3, verbose: false } であるとします。その後ユーザーがインストールした hello-plugin の patch もこの行を設定しようとして、{ greeting: '您好' } だけを書いたとします。hello-plugin は bundles 内で dsh-base より後に並んでいるため、その patch は後から適用され、この行は最終的に { greeting: '您好' } になります——maxRetries と verbose はベースパッケージの値を保持しないことに注意してください。それらはプラグイン schema のデフォルト値で補われるか(3 と false)、あるいは欠落するかのどちらかです。この例は bundles の順序、行単位での勝敗、行全体の置換という 3 つのメカニズムを結びつけており、profile を理解するための重要な推論です。

素材にはある profile の package.json の実例が示されており、一字一句分析する価値があります:

{
  "name": "dsh-profile-demo",
  "private": true,
  "dependencies": {
    "dsh-hello-plugin": "link:/path/to/hello-plugin"
  },
  "dsh": {
    "profile": {
      "bundles": [
        "@deepseek-ai/dsh-base",
        "dsh-hello-plugin"
      ]
    }
  }
}

この profile の package.json には注意すべき細部がいくつかあります:

  • name は dsh-profile-demo で、profile 名 demo に対応しており、命名スタイルは dsh-profile-<name> です。
  • private: true は、これがプライベートで公開されないパッケージであることを示します——そもそもユーザー側の設定ディレクトリであり、公開されるべきではありません。
  • dependencies には link:/path/to/hello-plugin があり、この hello-plugin がローカルの checkout 由来で、link 方式で接続されていることを示します。これはまた、素材が hello-plugin を含むディレクトリでインストールコマンドを実行することを推奨している理由も説明します——ローカルパスのリンクは npm に公開する必要がありません。
  • dsh.profile.bundles 配列の最初の項目は @deepseek-ai/dsh-base、2 番目は dsh-hello-plugin で、先の推論と完全に一致しています。

もう一度「dsh plugin によって自動的に作成・維持される」という責務を強調しておく。ユーザーがこの bundles 配列を手動で編集するわけではない——少なくとも通常の経路では。ユーザーが dsh plugin --profile <name> add <パッケージ> を実行すると、dsh は profile ディレクトリ内で pnpm に転送して依存関係のインストールを完了させ、同時にそのパッケージが dsh.bundle を宣言しているため、それを dsh.profile.bundles に追加する。つまり、bundles 配列はインストール行為の副産物であり、順序は「追加された順序」によって決まる。これには実用的な推論が伴う。ある bundle により高い上書き優先度を持たせたい場合、それを再インストールしても必ずしもリストの末尾に移動するとは限らない(dsh の実装詳細による)。より確実な方法は、順序のセマンティクスを理解した上で、必要に応じてメカニズムに従って調整することである。いずれにせよ、通常の使用においては dsh plugin の自動維持を信頼すべきである。

bundles リストを階層順序全体の中に置いて見ると、その位置はより明確になる。素材が示す完全な読み込み順序は次のとおりである:

  1. profile の dsh.profile.bundles リスト——各組み合わせパッケージの patch をリスト順に、まず dsh-base、次にインストール済みの各組み合わせパッケージをその追加順に;
  2. profile 自身の cordis.patch.yml——ユーザー profile レベルの patch 層;
  3. home レベルの $DSH_HOME/cordis.patch.yml——各 profile が共有するマシンローカルの設定;
  4. 各 --patch <path> overlay——argv 順に。

見てのとおり、bundles リストは階層順序の第 1 段階に位置し、すべてのユーザー patch より前の基礎層である。これはつまり、bundle が基礎となる組み合わせを提供し、profile レベルの patch がそれを上書きでき、home レベルの patch がさらに上書きでき、コマンドライン overlay が最終的な発言権を持つということである。4 つの層の「誰が書くか、共有範囲、優先度」を表で明確に比較することは、以降の内容を理解する最良の下準備となる:

順序誰が書くか / 共有範囲相対優先度
1dsh.profile.bundles リストプラグイン作者が patch を提供し、dsh plugin がリストを維持;単一 profile 内最低(最初に適用)
2profile 自身の cordis.patch.ymlユーザーがその profile 用に記述;単一 profile 内bundles より高い
3$DSH_HOME/cordis.patch.ymlユーザーがマシン全体用に記述;各 profile が共有profile レベルより高い
4--patch <path> overlayコマンドラインで argv により渡される;単一の起動時最高(最後に適用)

この表はまた、混同しやすい2つのポイントを明確にしている。第一に、home レベルの patch は profile レベルの patch より優先される——profile のほうがより「専用」に見えるかもしれないが、ロード順では home レベルのほうが後になり、行優先のルールに従えば home レベルが勝つ。設計意図は、マシンローカルの設定が個々の profile の設定を統一的に上書きできるようにすることである。第二に、アプリケーション引数は別の patch レイヤーではない。コマンドラインのアプリケーション引数は通常のアプリケーション固有のサービスによって解決され、この4層の重ね合わせには関与しない。実際に重ね合わせに関与するコマンドライン入力は --patch overlay であり、これは argv の順序に従い、最後に配置され、最優先となる。この2点を区別できれば、「どのレイヤーが最終的に勝つのか」という問いに対して正確な答えを出せるようになる。

mkdir -p hello-plugin:組み合わせパッケージ作成の最小の第一歩

理論の説明は終わったので、実際の作業に移る。公式チュートリアルで示される最初のステップは非常にシンプルである:

mkdir -p hello-plugin

これはパッケージディレクトリを作成するだけである。この操作自体に技術的な難しさはないが、この一連の説明における役割は述べておく価値がある。なぜなら、これが後続のすべてのインストールとレイヤリングの議論における物理的な出発点となるからである。

なぜディレクトリ作成から始めるのか?なぜなら、組み合わせパッケージはまず npm パッケージであり、npm パッケージの最小形態はディレクトリと package.json ひとつだからである。このディレクトリには後にプラグインコード、patch ファイル、そして package.json(その中で dsh.bundle を宣言する)が置かれ、それを実際にインストール可能な bundle へと変える。hello-plugin はチュートリアルにおける一貫したサンプルとしての役割を持つ:これは profile にインストールされ、dsh.profile.bundles リストの2番目のメンバーとなり、「どちらがどちらを上書きするか」というレイヤリングの推論に関与する。したがって、今作成するディレクトリは、この完全なチェーンの最初の環である。

この操作を正しい順序の中に位置づけて見てみよう:

  1. パッケージディレクトリを作成する(本節の mkdir -p hello-plugin)。
  2. ディレクトリ内にプラグインコードを書き、Config インターフェースと同名の schema をエクスポートする(本記事の前の節に対応)。
  3. patch ファイルを作成し、このパッケージがどのプラグイン行を挿入または上書きするかを宣言する。
  4. package.json の dsh キーの下で dsh.bundle を宣言する。
  5. hello-plugin を含むディレクトリで dsh plugin --profile demo add ./hello-plugin を実行し、それを profile にインストールする。
  6. dsh が profile を初期化し(初回使用時に @deepseek-ai/dsh-base が最初の組み合わせパッケージとなる)、pnpm がその checkout をリンクし、dsh はこのパッケージが dsh.bundle を宣言しているため、それを dsh.profile.bundles に追加する。

第 5 ステップのコマンド形式に注目してください:dsh plugin --profile <name> <args...>。資料には、これが profile ディレクトリ内で pnpm に転送されると明記されているため、すべての pnpm サブコマンドが利用可能です。つまり、add、remove、install、update といった pnpm 操作はすべて dsh plugin を入口として、指定した profile のコンテキスト内で実行できます。これは dsh が「profile 管理」と「依存関係管理」を一つに統合した実用的な設計です。

さらに、上級者にとって注目すべき細部があります。資料には「hello-plugin を含むディレクトリで、そのパッケージの checkout をインストールする」とあり、使用するコマンドは add ./hello-plugin です。この相対パスは pnpm によって link 依存として記録されます(profile の package.json では link:/path/to/hello-plugin として表現されます)。すなわち、bundle の開発は完全にローカルで完結でき、npm に公開しなくても profile から参照できるということです。これは組み合わせパッケージのデバッグや patch の反復に非常に有利です。コードと patch を修正すれば、profile は link を通じて直接反映されます。これを理解すれば、自分のデプロイ環境でも同じ方法で bundle の挙動を検証でき、公開プロセスを経由する必要はありません。

mkdir -p の -p も言及に値します。これは親ディレクトリが存在しない場合にエラーを出さず、既に存在する場合もエラーを出さないことを保証します。些細なことに見えますが、スクリプト化のベストプラクティスに合致しています——インストールスクリプトや Makefile 内で安心して繰り返し実行できます。デプロイと運用を対象としたチュートリアルでは、このような「繰り返し実行可能」という細部が、しばしば構文そのものよりも重要です。

ここまでで、「設定はどこから来るのか」の前半が説明し終わりました:cordis.yml から apply へのフィールドマッピング、Schemastery の同名 schema におけるデフォルト値の帰属、設定とコードの分離を判断する基準、bundle と profile という二つの manifest の役割分担、bundle の patch 成果物、profile を $DSH_HOME に置く理由、bundles リストの順序の意味、そして hello-plugin の初期動作です。後半では引き続き、インストールコマンドの完全な実行詳細、ロード順序の四層がどのように段階的に組み合わされるか、patch の行全体置換がもたらす具体的な落とし穴と回避策、そして dsh が bundles を自動保守する完全な挙動を説明し、「有効な設定は結局どこから来るのか」という手がかりを完全に収束させます。

前段では、プラグイン側の Config インターフェースと同名の Schemastery schema を詳しく説明し、bundle と profile という二つの manifest の役割分担も明らかにしました。ここから一歩進めます:hello-plugin を実際に profile にインストールし、dsh の有効な設定が一体どの層から現れるのかをはっきりさせましょう。

dsh plugin --profile <name> <args...>:profile ディレクトリ内で pnpm に転送する仕組み

多くの読者は dsh plugin --profile demo add ./hello-plugin というコマンドを初めて見たとき、dsh が独自のパッケージマネージャを実装していると誤解するかもしれません。実際はまったく逆です。dsh はここでは単なる運び屋にすぎません——--profile 以降の残りの引数をそのまま pnpm に転送し、しかもその転送は profile ディレクトリ内で実行されます。この点を理解すれば、プラグインインストールチェーン全体を一度に把握できるようになります。

まずコマンドを分解してみましょう。コマンドは次の形式です:

dsh plugin --profile <name> <args...>

ここで --profile <name> は dsh 自身の引数であり、どの profile を操作するかを特定するために使われます。一方、<args...> は丸ごと pnpm に透過的に渡される引数、つまり pnpm のコマンドラインです。これは、普段使い慣れている pnpm のサブコマンドがここではほぼすべて使えることを意味します:add で依存関係をインストール、remove で依存関係を削除、install で lockfile に従って復元、update でバージョンアップ、list でインストール済み内容を確認、why で依存元を追跡、link でローカルディレクトリをリンク、run でスクリプトを実行。転送はそのまま行われるため、pnpm で学んだ引数の書き方、--save-dev のようなスイッチ、さらには --filter のような使い方まで、dsh 側で追加サポートする必要はありません。dsh は構文の翻訳を行わず、作業ディレクトリを切り替えて制御を pnpm に委ねるだけです。

なぜ profile ディレクトリ内で実行する必要があるのでしょうか?なぜなら profile 自体が $DSH_HOME/profiles/<name> に位置するディレクトリであり、独自の package.json と依存関係ツリーを持っているからです。インストールする各バンドルは、最終的にこのディレクトリの node_modules に配置され、このディレクトリの package.json に書き込まれます。もし dsh が現在の作業ディレクトリで pnpm を呼び出した場合、インストールされたものは間違った場所に配置されます——依存関係はプロジェクトルートに落ち、profile からは読み取れません。あるいはさらに悪いことに、2 つの profile 間で共有されるべきでない依存関係が共有されてしまいます。したがって「profile ディレクトリ内で転送する」ことは実装の詳細ではなく、インストールセマンティクスの一部です:profile の依存関係クロージャは profile 自身のディレクトリに帰属します

まず初回使用について。まだ存在しない profile 名に対して最初に dsh plugin --profile demo ... を実行すると、dsh は「profile が存在しない」とエラーを出して終了するのではなく、この profile を初期化します。$DSH_HOME/profiles/demo ディレクトリを作成し、最小限の profile スケルトン(package.json を含む)を生成し、さらに @deepseek-ai/dsh-base をその最初のバンドルとして登録します。つまり、この初期化という動作には自然と「ベースライン」が伴います。この設計は非常に重要です。profile は誕生した瞬間から空の設定ではなく、dsh-base レイヤーによるフォールバックを持ち、サードパーティの Plugin を一つもインストールしていなくても、profile は起動可能なバンドルのままであることが保証されます。

エンジニアリング上、ここには踏みやすいポイントが二つあります。第一に、profile 内の node_modules を手動で変更したり、依存関係を手書きしたりしないでください。profile の依存関係は dsh plugin という経路で保守すべきです。そうすることで、dsh は pnpm のインストール完了後に自身の登録処理(後述)を続けて行えます。第二に、profile 名はパスの一部になりますdemoprodstaging のような小文字英字を使うと安心です。大文字小文字やプラットフォーム差異によるパスの問題を避けられます。第三に、CI で一度限りのインストールを行う場合、まず $DSH_HOME が書き込み可能で、想定された永続化ボリューム上にあることを確認してください。profile は実行時の状態であり、一時ディレクトリに置くと次のビルドで bundles リストが失われます。

もう一つ、ここで押さえておくべき概念があります。--profile は「どの設定セットを組み立てるか」を選択するものであり、Plugin 自体とは無関係で、patch とも無関係です。同じ Plugin パッケージは、demo と prod の両方の profile に同時にインストールできます。二つの profile はそれぞれ自身の dsh.profile.bundles リストを維持し、互いに干渉しません。これは後の「環境ごとの差異化設定」への伏線となります。

dsh plugin --profile demo add ./hello-plugin:一度のインストールが引き起こす三重の変化

では、前段の hello-plugin を実際にインストールします。コマンドは次のとおりです:

dsh plugin --profile demo add ./hello-plugin

このコマンドは一見地味に見えますが、実際には demo という profile の中で同時に3つのことが起きています。この3つの順序と責務の境界を理解すれば、インストール異常が発生したときにどの層で問題が起きているかを素早く特定できるようになります。

第一の変化:pnpm がこの checkout をリンクしました。 引数 ./hello-plugin は npm 上のパッケージ名ではなくローカルディレクトリであるため、pnpm はこれをローカルパス依存として扱い、profile の node_modules 配下にそのディレクトリを指すリンクを作成し、この依存を profile の package.jsondependencies"dsh-hello-plugin": "link:/path/to/hello-plugin" のような形式で書き込みます。link セマンティクスの意味に注意してください:ソースディレクトリでコードを変更すると、インストール先の内容が即座に追随して変わります。再 add は不要です。これはプラグイン開発期には極めて便利ですが、リリース前には注意が必要です——link 依存は「インストール済みのバージョン」と「ソースディレクトリの状態」を結びつけてしまうため、ソースディレクトリがクリーンアップされたりブランチが切り替わったりすると、profile もそれに追随して変わってしまいます。バージョンを固定したい場合は、registry に公開した上でバージョン番号を使ってインストールすべきです。

第二の変化:dsh が dsh.bundle 宣言に基づいて、これを dsh.profile.bundles に追加します。 これがインストールフロー全体の中で dsh 独自のステップです。pnpm が依存をインストールし終えた後、dsh はこのパッケージの package.json を読み、その dsh キーの下に dsh.bundle が宣言されているかを確認します。宣言されていれば、dsh はこのパッケージを profile の dsh.profile.bundles 配列に追加します。このステップが「インストール」と「単なる依存」を分ける鍵です:あるパッケージは pnpm によって profile にインストールされても、dsh.bundle を宣言していなければ組合せパッケージにはならず、設定層を提供することもありません。bundle の本質は設定層を伴う npm パッケージであり、その設定層とはそれが提供する patch ファイルのことです。dsh.bundle という manifest キーが答える問いはまさに「このパッケージは何を提供するのか」です。したがってインストール動作はこう覚えるとよいでしょう:pnpm はパッケージを「存在させる」役割を担い、dsh はパッケージを「有効化する」役割を担います。

第三の変化:@deepseek-ai/dsh-base が最初の組合せパッケージになります。 demo は初回利用で初期化された profile であるため、boss の位置は常に dsh-base に譲られます——それは bundles リストの先頭であり、基礎層でもあります。以降、インストール済みの組合せパッケージが追加されるたびに、「追加された順序」に従ってその後に並びます。この順序は無意味ではありません:その後のロード時の重ね合わせ順序を直接決定し、dsh-base が最初に下地を敷き、サードパーティのパッケージがその上に重なります。だからこそ、上書きの問題を調査する際に最初に見るべきは bundles 配列の並びなのです。

三重の変化を実行順に繋げると:pnpm が checkout をリンク → 依存が package.json に入る → dsh が manifest を読み、パッケージを bundles に追加 → dsh-base が先頭に固定される。いずれかのステップで問題が起きると、それぞれ異なる症状として現れます。もし pnpm のステップで失敗した場合(例えばディレクトリに有効な package.json がない場合)、パッケージマネージャのエラーが表示されます。もし dsh.bundle のステップが効いていない場合(例えばパッケージ名とディレクトリ名が一致しない、manifest の記述位置が間違っているなど)、依存はインストールされたのに設定がまったく変わらない、という状態になります——これは最も見つけにくい種類の故障です。なぜならインストールが「成功したように見える」からです。このような場合は、まず profile の dsh.profile.bundles にそのパッケージ名が実際にあるかどうかを確認してください。

以下に、そのまま貼り付けて実行できるサンプルを示します。パッケージ作成からインストール、そして検証までの完全な流れをカバーしています。サンプル内のパッケージ名とディレクトリ名は、ご自身の実際のプロジェクトに合わせて置き換えてください。

# 1) 任意の作業ディレクトリに最小構成の組み合わせパッケージのソースを用意する
mkdir -p hello-plugin/src
cat > hello-plugin/package.json <<'EOF'
{
  "name": "dsh-hello-plugin",
  "version": "0.0.1",
  "private": true,
  "main": "src/index.js",
  "dsh": {
    "bundle": {
      "patch": "cordis.patch.yml"
    }
  }
}
EOF

# 2) bundle が必ず提供しなければならない patch ファイル
cat > hello-plugin/cordis.patch.yml <<'EOF'
# この patch は、本組み合わせパッケージが設定に挿入/上書きするプラグイン行を宣言する
# 具体的な行の内容は、ご自身のプラグイン id と config に合わせて記述してください
EOF

# 3) demo profile にインストールする(初回は profile が自動初期化される)
dsh plugin --profile demo add ./hello-plugin

# 4) 検証:bundles リストに該当パッケージが現れるか確認する
cat "$DSH_HOME/profiles/demo/package.json"

# 5) ついでに pnpm 側で何がインストールされたか確認する
dsh plugin --profile demo list --depth 0

このサンプルには、読者が置き換える必要のある箇所が 2 つあります:dsh.bundle 配下の patch ファイルの実際のパス、そして patch ファイル内で実際に挿入または上書きするプラグイン行です。紙幅の都合で patch 行のフォーマットについては展開しませんが、その役割だけはしっかり覚えておいてください:bundle の manifest は「このパッケージが何を提供するか」に答えるものであり、その答えは1 つの patch ファイルです——それがプラグイン行を挿入または上書きします。

生成後の profile package.json はどんな形になるか:dependencies と dsh.profile.bundles の対照

インストール完了後、生成される profile の package.json はおおよそ次のようになります(形状は素材に基づくもので、パスは実際の checkout に置き換えてください):

{
  "name": "dsh-profile-demo",
  "private": true,
  "dependencies": {
    "dsh-hello-plugin": "link:/path/to/hello-plugin"
  },
  "dsh": {
    "profile": {
      "bundles": [
        "@deepseek-ai/dsh-base",
        "dsh-hello-plugin"
      ]
    }
  }
}

フィールドごとに読んでいくと、このファイルは実のところ「誰が何をインストールし、誰が何を提供しているか」を示す二列表のようなものです。

  • name: dsh-profile-demo。これは profile 自身のパッケージ名であり、あなたがインストールしたプラグインのパッケージ名とは別物である点に注意してください。profile は独立したディレクトリであり、pnpm ワークスペースやログの中で識別できるよう、自身の識別子が必要です。これは通常、初期化時に dsh が profile 名に基づいて生成するもので、手動で保守する必要はありません。
  • private: true。profile は決して registry に公開されるべきではありません。これは「このマシン上における、この起動可能な組み合わせ」を記述するものであり、実行時のアセンブリ情報であって、配布可能な成果物ではありません。private というマークは同時に、profile がインストールディレクトリの外に位置していることも思い出させてくれます。パスのテンプレートは $DSH_HOME/profiles/<name> であり、それは環境に属するもので、コードリポジトリには属しません。
  • dependencies: { "dsh-hello-plugin": "link:/path/to/hello-plugin" }。これは pnpm の領域であり、「この profile の依存ツリーにどのパッケージがあり、どこから来ているか」を記録します。link プレフィックスは、それがローカルの checkout を指していることを示します。このセクションが答えるのは「パッケージが存在するかどうか」です。
  • dsh.profile.bundles。これは dsh の領域であり、「この構成がどの組合せパッケージによって、どのような順序で構成されているか」を記録します。配列の先頭にある @deepseek-ai/dsh-base は初期化時に自動的に追加される基礎レイヤーであり、以降は追加された順に並びます。このセクションが答えるのは「パッケージが有効になるかどうか、どの順序で有効になるか」です。

二つのフィールドを並べて見ると、このセクションで最も重要なメンタルモデルが得られる:dependencies は「インストールされたかどうか」を決め、bundles は「使うかどうか、どう重ねるか」を決める。両者は自動的に同期するわけではない——pnpm は dsh.bundle を宣言していないパッケージもインストールできる(dependencies には入るが bundles には入らない)。そして実務においては、bundles の順序こそが上書き関係を決定する要因である。したがって、あるプラグインが「効いていない」と疑ったときの調査チェーンは次のとおり:まず dependencies でパッケージがインストールされていることを確認し、次に bundles でパッケージが登録されていることを確認し、最後にようやく patch の内容と読み込み順序を疑うべきである。

ここにはさらに、bundle と profile の位置づけの違いに由来する、覚えておく価値のある二つの抽象的な判別問題がある:bundle はあなたが作成し配布するものであり、profile はユーザーが dsh --profile <name> で起動するものである。両者はどちらも一つの package.json で記述されるが、携える manifest の種類が異なる。両方であるものは存在しない——あるパッケージは dsh.bundle を宣言するか(それは一つの patch ファイルを提供する)、あるいは profile として存在するか(それはどの bundle がどの順序で構成されるかを記述する)のどちらかであり、組み合わせパッケージでありかつ profile であるパッケージは存在しない。この排除法をしっかり覚えておけば、多くの概念の混同は自然と解消される。また、profile の manifest は通常 dsh plugin によって自動的に作成・維持されるため、手書きする必要はほとんどない。一方、bundle の manifest はプラグイン作者自身が書くものであり、あなたが責任を負う部分である。以下の表は、これら二種類の manifest の主要な差異を並べて示したもので、早見表として便利である。

次元bundle(組み合わせパッケージ)profile
manifest キーdsh.bundledsh.profile
答える問いこのパッケージは何を提供するか(一つの patch ファイル)この設定はどの bundle がどの順序で構成されるか
誰が書くかプラグイン作者が書き、パッケージとともに配布されるdsh plugin によって自動的に作成・維持される
誰が使うか依存として profile にインストールされるユーザーが dsh --profile <name> で起動する
所在位置npm パッケージとともに配布され、profile の依存ツリー内に置かれるインストールディレクトリの外、パステンプレートは $DSH_HOME/profiles/<name>
典型的な内容プラグイン行を挿入または上書きする一つの patch ファイル重ね合わせの順序を決める bundles リスト
手動で変更できるかできる。あなたのコードベースに属するdsh plugin に維持を任せることを推奨
同時にもう一方になれるかできない。両方であるものは存在しない。

有効な設定の4層ロード:空のルートから段階的に組み合わせて argv overlay へ

プラグインはインストールされ、bundles も登録されたが、「最終的に有効な設定」はまだ形になっていない。dsh のやり方はこうだ:空のルートの上に、決まった順序で層ごとに組み合わせ、後から適用された層が行単位で勝つ。素材が示す完全な順序は以下の通り:

  1. profile の dsh.profile.bundles リスト。各バンドルの patch はリスト順に適用される——まず dsh-base、次にインストール済みの各バンドルが追加された順に。これが設定の主要な来源である。
  2. profile 自身の cordis.patch.yml。profile ディレクトリ内にあるユーザー profile レベルの patch 層で、この環境の個別調整に使う。
  3. home レベルの $DSH_HOME/cordis.patch.yml。DSH_HOME 層に位置し、各 profile で共有されるマシンローカルの嗜好であり、このマシン上のすべての profile に影響する。
  4. 各 --patch <path> overlayargv の順序で順に適用され、起動時の一時的な重ね合わせに使う。

この順序図はデスクに貼っておく価値がある。なぜなら、それは同時に二つのことを決めるからだ:誰が誰を上書きできるか、そして変更をどこに置くべきか。層の粒度は「パッケージ」から「profile」へ、「マシン」へ、「今回のプロセス」へと、広いものから狭いものへ、共有から一時へと階段状になっている。ある環境の設定を変えたいなら第1・2層に置く。このマシン上のすべての profile の嗜好を変えたいなら第3層に置く。今回の起動だけで実験したいなら第4層を使う。

ここに極めて重要でありながら、最も誤読されやすい結論が一つある。素材は一文でそれを明示している:アプリケーション引数は別の patch 層ではない。多くの人は無意識にコマンドライン上の各種引数を「第五層」とみなしてしまうが、それは patch の階層体系には含まれず、「行単位で勝つ」という上書き比較には参加しない。ある重要な設定が「コマンドライン引数が設定ファイルを上書きする」と期待しているのに、v1 ではそれが実はサービス解決を通る通常のアプリケーション引数である場合、期待に反する有効値が得られ、「第五層」がどこにあるのか全く見つけられなくなる。この種の問題を調査するときは、まずアプリケーション引数を patch 階層のメンタルモデルから外しておこう。

もう一つ見落とされがちな細部は「空のルート」である。レイヤリングはデフォルト設定から始まるのではなく、空のルートから始まり、一層ずつ積み上げられていく。つまり最終的な設定にどのプラグイン行が存在し、どの config が存在するかは、これら四つの層によって完全に決定されるのであり、隠された「暗黙のデフォルト」は一切存在しない。これはむしろトラブルシューティングに有利である——各行は必ずいずれかの層に遡ることができ、層の読み方さえ知っていればよい。

ついでによく尋ねられる特定の問題を一つ挙げておく。profile の bundles リストは「追加された順序」で並んでおり、この追加順序は dsh plugin add の呼び出し履歴によって決まる。同じパッケージ群を繰り返し add / remove していると、順序がコードレビュー時に見たディレクトリ順序と一致しないことがある。順序はすなわち意味であるため、オーバーライド関係を調査する際には、bundles の並びが README の推奨順序と等しいと仮定せず、実際に profile の package.json を読むべきである。

後から適用される層は行単位で勝つ:patch は config 行全体を置換し、深いマージは行わない

ここから本節で最もつまずきやすい点、そして全文を通じて最も暗記すべきセマンティクスに入る:後から適用される層は行単位で勝ち、しかも patch は対象行の config 値全体を置換するのであって、各キーを深くマージするのではない

「行単位で勝つ」は粗い粒度である。比較の単位はプラグイン行であり、config 内の個々のキーではない。下位層であるプラグイン行が有効になっており、上位層に同じプラグイン行が現れた場合、上位層のその行が勝ち、下位層のその行は全体として関与しなくなる。「下位層が greeting を提供し、上位層は maxRetries だけを補う」といったキー単位の継ぎ合わせの暗黙の了解は発生しない。

「config 値全体を置換する」はこのセマンティクスの中で最も鋭い刃である。下位層のあるプラグイン行が完全な設定を伴っていると仮定しよう:

  • greeting: "Hello"
  • maxRetries: 3
  • verbose: false

そして上位層では一つのフィールドだけを変更したい、たとえば maxRetries を 5 にしたいと考え、そのキーだけを含む patch 行を書いたとする。結果は「残りのキーは保持され、maxRetries が 5 になる」ではなく、config 全体がこの新しい値で置換される:新しい値に maxRetries しか含まれていなければ、greeting と verbose は来源を失い、最終的な設定におけるこれら二つのキーの値は直感に反するものとなる(そのプラグインの schema がデフォルト値を提供するかどうか、および欠落時の挙動に依存する)。これは実際に本番環境で発生する落とし穴であり、症状はたいてい「一つのパラメータを変えただけなのに、なぜ別のパラメータまで変わったのか」というもので、しかも置換セマンティクスが原因だとはなかなか思い至らない。

対策はきわめて明確である:上位層に勝たせつつ残りのキーも保持したいなら、上位層のその patch に config 全体を完全に書かなければならない。「行全体の置換」を契約とみなすのだ:勝つ行を提供した者が、その行のすべてのキーに対して責任を負う。メンテナンスコストを下げるため、下位層の config キーをできるだけ少なくし、変わりやすい項目を上位層に集中させるというのが一般的な手法である。あるいは各環境の完全な config をそのまま完全なブロックとして書き、可読性と引き換えに確実性を得るという手もある。

もう一つ、「行単位での優先」に直接関係する詳細は前段から来ています。プラグイン側の Config schema では、デフォルト値は schema フィールドに書かれ(例:Schema.string().default('Hello')Schema.number().default(3)Schema.boolean().default(false))、さらに apply の第2引数は検証済みの設定です。したがって、上位層が特定のキーを置き換え、そのキーが欠落した場合、最終的な config にそのキーの値が残るかどうかは、schema がデフォルト値を与えているか、および欠省時の検証の挙動に依存します。これにより「schema 設計」と「階層的オーバーライド」の2つが結びつきます。オプショナルキー(TypeScript の verbose?: boolean など)とデフォルト値付きのキーでは、置換セマンティクス下での振る舞いが異なります。patch を書く際にすべてを書かされるのを避けたいなら、schema のデフォルト値に頼るのが一つの方法ですが、そのデフォルト値がその環境の期待に合致しているかをまず確認すべきであり、望まない方向へ値を黙って「引き戻す」ことのないようにしてください。

次に第3層のセマンティクス上の帰結です。home レベルの $DSH_HOME/cordis.patch.yml は profile 自身の patch 層のに位置するため、すべての profile の profile レベル設定を上書きします。これは「マシンローカルの嗜好」(例えばローカルのパス、プロキシ、ログスイッチ)という位置づけに非常によく合致しますが、誤って影響を与えやすい層でもあります。共有マシンで home レベルの patch を書くと、すべての profile の意図を覆してしまいます。「ある profile の設定がなぜこのマシンで効かないのか」を調査する際は、まず $DSH_HOME/cordis.patch.yml に同名のプラグイン行がないかを確認してください。同様に、argv overlay は最後にあり、それより前のすべての層を上書きするため、一時的な診断には非常に適していますが、重要な差異を長期的に起動コマンドに埋め込まないでください。それはディスクに保存されず、レビューもできず、さらに「適用パラメータは別の patch 層ではない」というルールの下では、あなたが想定しているオーバーライド経路がそもそも通らない可能性があります。

以下の例は、典型的な行全体置換のシナリオを示しています。上位層は一つのキーだけを変更したいのに、置換セマンティクスによって下位層の残りのキーも一緒に持っていかれてしまいます。例では2つの擬似 patch で階層関係を表現し、自分のプロジェクトで再現・検証できるようにしています。

# 低層:profile の dsh.profile.bundles 中某组合包贡献的 patch(示意)
- id: my-plugin
  config:
    greeting: "Hello"
    maxRetries: 3
    verbose: false

# 高层:profile 自己的 cordis.patch.yml,只想把 maxRetries 调到 5
- id: my-plugin
  config:
    maxRetries: 5

# 结果(按行胜出 + 整行替换,而不是逐键 merge):
#   greeting   —— 不再来自低层那一行,取值改由该层 config 与 schema 默认值决定
#   maxRetries —— 5
#   verbose    —— 同理,不再来自低层那一行
# 正确做法:高层要把整份 config 写全
- id: my-plugin
  config:
    greeting: "Hello"
    maxRetries: 5
    verbose: false

この種のオーバーライドが期待通りに発生しているかを検証する際に最も有用なのは、推測することではなく層ごとに patch を追加して最終的な設定の変化を観察することです。まず patch が一切ない状態でベースラインを記録します。次に profile レベルの patch を追加し、変更されたキーを観察します。さらに home レベルの patch を追加し、どのキーが再び上書きされるかを観察します。最後に起動コマンドに --patch overlay を追加し、それが意図通りに先行するすべての層を覆い隠すかを観察します。一度に一層ずつ追加することが、オーバーライドのセマンティクスを調査する唯一信頼できる方法です。

表層コンポジションパッケージが自前サービスを解決するパス:通常のアプリケーションが依存を取得する仕組み

階層の説明が終わったところで、もう一つ必ず答えるべき問題があります。プラグインが幾重にも patch された後、それはどのように他のサービスにアクセスするのでしょうか。素材には非常に簡潔でありながら情報量の多い一文があります——表層コンポジションパッケージは通常のアプリケーション自前のサービスを通じてそれらを解決できる

これをエンジニアリングの言葉に翻訳すると、コンポジションパッケージは「設定層」によって組み立てられてはいますが、設定によってのみ書き換え可能な静的な宣言ではありません。コードを伴う npm パッケージとして、それは依然として通常の cordis アプリケーション部品であり、実行時には通常のサービス解決メカニズムを通じて自身が依存するサービスを取得できます。つまり、「patch によって管理されること」と「サービスを解決できること」は別の事柄です。前者はその行の config がどこから来るかを決め、後者は実行時にどこから能力を取得するかを決めます。

この区別はよくある概念的な不安を解消します。「自分のプラグインは patch によって挿入されているのだから、コンテキストを取得できず、他のプラグインに依存できないのではないか」と心配する人がいます。答えは否です。コンポジションパッケージは cordis のアプリケーション体系の中で生きており、サービス解決はアプリケーション自前の経路を通ります。bundle を使うかどうか、どの層の patch で変更されるかとは無関係です。config はオーバーライド可能な入力であり、サービスは実行時の依存解決です。

実践上、これには二つの有用な推論が伴います。第一に、config に何を置くべきか、サービスから何を取得すべきかを区別する必要があります。運用が環境に応じて調整することを許容するもの(スイッチ、閾値、パス、挨拶文など)は Config schema に入れて patch 層にオーバーライドさせます。コードレベルの能力依存(ログ、ストレージ、他のプラグインが提供するサービス)はサービス解決を通し、本来依存のレベルで解決すべき問題を config で「オンオフ」しようとしてはなりません。第二に、「プラグインが依存を取得できない」を調査する際は、patch の階層を探してはなりません。階層は設定値の出所を説明するだけであり、サービス解決の失敗は通常、依存のインストール順序、サービスの登録タイミング、またはコンテキスト取得方法の問題です。両者は異なる問題領域です。この二つの問題領域を分けることで、誤った方向への探索に費やす大量の時間を節約できます。

2026 年 9 月の実践:dsh 階層設定の調査チェックリスト

これまでのメカニズムを調査の現場に落とし込みます。上級読者向けに、以下のチェックリストは dsh 階層設定問題の取っ掛かりとしてそのまま使えます。「まず階層、次にセマンティクス」の順で進めることで、「設定が反映されない/反映されたが望むものではない」という類の障害のほとんどをカバーできます。

  1. まず profile の選択が正しいことを確認する。 起動コマンド内の --profile <name> が、想定している profile を指しているかチェックする。profile は $DSH_HOME/profiles/<name> にあり、profile ごとにそれぞれの package.json と bundles を持つ。選び間違えれば、設定ツリー全体を選び間違えたことになる。
  2. bundles の順序を推測せず読む。 profile の package.json を開き、dsh.profile.bundles を確認する:dsh-base が先頭にあるか、サードパーティパッケージの並びが想定と一致しているか、インストールしたつもりなのに実際にはリストにないパッケージがないか(それは dsh.bundle を宣言していないか、インストール手順が完了していないことを意味する)。
  3. 2 種類の patch 層の位置を特定する。 profile ディレクトリ内の cordis.patch.yml(profile レベル)と $DSH_HOME/cordis.patch.yml(home レベル)を見る。特に home レベルは各 profile で共有されており、profile レベルの後に適用され、profile レベルを上書きすることに注意する。「本機のある profile の設定が効かない」の答えは、しばしばこの層に隠れている。
  4. overlay の argv 順序を照合する。--patch <path> overlay は argv 順で適用され、最後のものがそれ以前のすべてを上書きする。起動スクリプトで複数の overlay を連結している場合、あるいは環境変数で動的に生成している場合、順序が上書き結果を直接決定する要因となる。順序が不安定なら、挙動も不安定になる。
  5. キーごとの merge ではなく、行全体の置換セマンティクスを適用する。「1 つのキーを変更したら、他のキーも一緒に変わった」という場合、まず置換セマンティクスに該当すると仮定する:上位の patch の config はすべて書かれているか?下位の残りのキーも一緒に持ってくる必要があるか?上位で省略されたキーが下位から継承されることを期待してはいけない。
  6. 適用パラメータを階層モデルから切り離す。適用パラメータは別の patch 層ではないことを覚えておく。ある設定が「行単位で勝ち抜く」ことに参加することを期待していても、それが適用パラメータとして渡されているなら、その比較経路を通らない。期待している上書きが、そもそもこの階層に含まれていないのではないか確認する。
  7. config の問題とサービスの問題を分離する。「プラグインが何も取得できない」のが config の値が正しくないためか、サービス解決の失敗なのかを確認する。前者は patch 階層の中で探し、後者は依存関係とサービス登録の方向で探す。混同してはいけない。
  8. 単層インクリメンタル法で再現する。 patch なしのベースラインを記録 → profile レベルを追加 → home レベルを追加 → overlay を追加、と毎回 1 層だけ追加して最終設定を diff する。これは一度に 3 層を疑うよりはるかに速く、上書き順序を真に確認できる唯一の方法である。

このチェックリストに一言で哲学を与えるなら、こうなる:まず「この行はどこから来たのか」を問い、次に「なぜこの値なのか」を問う。階層モデルが答えるのは最初の問いであり、置換セマンティクスが答えるのは二番目の問いであり、そしてアプリケーションパラメータとサービス解決は、すべてがこの階層に収まっているわけではないことを思い出させてくれる。

まとめとベストプラクティス

全文を実行可能なチェックリストに凝縮する。次に「有効な設定は結局どこから来るのか」に直面したとき、ゼロから推測するのではなく、これに沿って一つずつ辿っていけることを願っている。

  • プラグイン側でまず契約を立てるConfig インターフェースと同名の Schemastery schema をエクスポートし、デフォルト値は schema のフィールドに書く(例:Schema.string().default('Hello')Schema.number().default(3)Schema.boolean().default(false))。apply の第二引数が検証済みの設定である。これが「設定とコードの分離」の出発点だ。
  • 二つの manifest を区別するdsh.bundle は「このパッケージが何を提供するか」に答える——プラグイン行を挿入または上書きする patch ファイルである;dsh.profile は「この設定がどの bundle によってどのような順序で構成されるか」に答える。bundle はプラグイン作者が作成・配布するものであり、profile はユーザーが dsh --profile <name> で起動するものである。両方であるものは存在しない
  • dsh plugin の本質を理解するdsh plugin --profile <name> <args...> は profile ディレクトリ内で引数を pnpm に転送するため、すべての pnpm サブコマンドが利用可能である;初回使用時はprofile を初期化し、@deepseek-ai/dsh-base をその最初の組み合わせパッケージにする。
  • 一度のインストールで三つの変化:pnpm が checkout をリンク → dsh が dsh.bundle 宣言に従ってパッケージを dsh.profile.bundles に追加 → dsh-base が先頭に固定される。dependency は「インストールされたかどうか」を決め、bundles は「使うかどうか、どう重ねるか」を決めると覚えておけ。
  • 四層のロード順を暗記する:① profile の dsh.profile.bundles(dsh-base と各インストール済み組み合わせパッケージを含み、追加順)→ ② profile 自身の cordis.patch.yml → ③ $DSH_HOME/cordis.patch.yml(各 profile が共有するマシンローカルの設定)→ ④ 各 --patch <path> overlay(argv 順)。アプリケーションパラメータは別の patch 層ではない
  • 上書きセマンティクスはただ一つ:後に適用される層が行単位で勝ち、patch は対象行の config 値全体を置換し、各キーを深くマージするのではない。一つのキーを変えつつ残りを保持したいなら、上位層で config 全体を書き切ること;下位層からキーを継承することを期待してはいけない。
  • サービス解決と設定階層は直交する:表層の組み合わせパッケージは通常のアプリケーションとして自前のサービスを解決できる。config は patch で上書き可能な入力を受け持ち、サービスは実行時の能力依存を解決するために使う。二つの事柄を一つの問題に混ぜてはいけない。
  • エンジニアリング規律:手書きの依存と手書きの bundles で dsh plugin を迂回させない;開発期は link 依存が便利だが、リリース前には固定バージョンを使う;profile は環境に属し、永続化可能で書き込み可能な $DSH_HOME の下に置く;起動コマンド内の overlay は一時的な診断手段と見なし、長期的な設定差異の隠れ家としない。
  • トラブルシューティングの口诀:まず profile が正しく選ばれているか確認 → bundles の順序を読む → 二種類の patch 層を特定 → overlay の argv 順を照合 → 行全体置換セマンティクスを適用 → アプリケーションパラメータを階層から切り出す → config とサービスの問題を分離 → 単層増分法で再現する。まず「この行はどこから来たのか」を問い、次に「なぜこの値なのか」を問う。

この階層化と manifest の仕組みを完全に理解すれば、あなたの手元には三つのものが揃う:patch で上書き可能な設定契約(Config と schema)、インストール可能で再利用可能な配布単位(bundle)、そして予測可能なロードツリー(四層の順序と行単位の優先)である。これらを組み合わせることで、dsh の有効な設定がどこから来るのかという問いに対する完全な答えが得られる。