# DSH バンドル / Cordis プラグイン オーサリング仕様(公式実物からの抽出) 出典: 読み取り専用調査 / 対象 @deepseek-ai/dsh 0.2.0-rc.2 / 作成日 2026-10-18 - 調査方法: `$HOME/.local/lib/node_modules/@deepseek-ai/dsh/` 配下の**インストール済み実物**のソース・型定義・ `cordis.patch.yml` を読み、必要箇所は `node` で**読み取り専用の実測**(ローダ解決の検証、サンドボックス境界の検証)を行った。 DSH インストール側への書き込みは一切していない。 - 表記: `$AI` = `$HOME/.local/lib/node_modules/@deepseek-ai/dsh/node_modules/@deepseek-ai` (= 205 パッケージが入っているディレクトリ)。`$DSH` = `$HOME/.local/lib/node_modules/@deepseek-ai/dsh`(ランチャ本体)。 - 断り: 本文中の「実測」は本環境で実際に実行して確認した事実、「推測」は実装からの読みによる推定、 「不明」は確認できなかったものを指す。 --- ## 1. `cordis.patch.yml` の正確な構文 ### 1.1 ファイルの正体 `cordis.patch.yml` は「ローダのパッチ層」であり、**トップレベルの YAML 配列**でなければならない。 パースは `js-yaml` + 独自スキーマ `entryListSchema = yaml.JSON_SCHEMA.extend(JsExpr)` で行われる (`$AI/dsh-app-boot/lib/index.js:20-34`、配列でなければ例外: 同 `:3566`)。 `PatchOptions` の実体は `EntryOptions[]` に対する差分で、`insert` の有無で 2 形態に分かれる。 パッチ適用の唯一の実装は `applyEntryPatches`(`$AI/dsh-app-boot/lib/index.js:61-110`): ```js for (const patch of patches) { const { id, insert, name, ...overrides } = patch; if (insert) { // ---- 挿入 ---- if (id) { /* 既存の group エントリの config 配列へ push */ } else data.push(...insert); // id 無し → ルート配列の末尾へ buildMap(insert); // 同一パッチリスト内の後続パッチから見える continue; } if (!id) { warn("patch: id is required for non-insert patches"); continue; } const target = entryMap.get(id); if (!target) { warn("patch: entry %C not found", id); continue; } if (name && name !== target.name) { warn("patch: name mismatch …"); continue; } for (const [key, value] of Object.entries(overrides)) target[key] = value; // ---- 上書き ---- } ``` 重要な性質(すべて同関数からの帰結): | 性質 | 根拠 | |---|---| | 入力 `data` は破壊されない(`structuredClone`) | `index.js:63` | | **`config` はマージされず「行の config 全体を置換」する** | `index.js:104-107`(`target[key] = value`) | | 挿入は `data.push` = **ルート配列の末尾に追加**(`insert` に `id` を書けば「その group の子」になる) | `index.js:87` / `:74-86` | | 同一パッチリスト内で、先に挿入した行を後のパッチが id で狙える | `index.js:88` | | マッチしないパッチは**警告してスキップ**(起動は続行) | `index.js:97-99` | | 行の並び順にロード意味は無い(サービス可用性駆動) | `$AI/dsh-base/cordis.patch.yml:12-13` | | `name` を上書き行に書くと**一致検査**に使われる(不一致ならスキップ。リネームではない) | `index.js:100-103` | ### 1.2 「空ルートに行を挿す」書き方 プロファイルのルート `cordis.yml` は文字通り `[]`(`$DSH/lib/profile-boot-BZ2ZjNWi.js:121-125`)。 `dsh-base` はそれを **`id` 無しの `insert` 1 発**で丸ごと組み立てている: ```yaml # $AI/dsh-base/cordis.patch.yml:15-16 - insert: - id: tool-plugin-manager name: '@deepseek-ai/dsh-plugin-manager/tools' disabled: true ``` `dsh-headless` も同じ(`$AI/dsh-headless/cordis.patch.yml:20-23`): ```yaml - insert: - id: headless-startup name: '@deepseek-ai/dsh-headless/startup' ``` → **自作バンドルが「空ルートに行を挿す」ときはこの形をそのまま使う。** ### 1.3 各フィールドの意味 `EntryOptions` の定義(`$AI/cordis-plugin-loader/src/config/entry.ts:9-23`): ```ts export interface EntryOptions { id: string // 包含する entry tree 内で安定な id name: string // entry tree が import するモジュール指定子 config?: any // プラグインへ渡す config group?: boolean|null // ネストした group を意味する disabled?: boolean|null // この行と子孫の実行を止める inject?: Inject|null // 要求サービス、またはサービス intercept 設定 } ``` | フィールド | 意味 | 根拠 | |---|---|---| | `id` | 行の安定キー。**すべての後続層は id でしか行を狙えない**。省略時は `Math.random()` の 8 桁 hex を自動採番 | `entry.ts:12`; `$AI/cordis-plugin-loader/src/config/tree.ts:51-58` | | `name` | import するモジュール指定子(解決規則は §3) | `entry.ts:13` | | `config` | 当該プラグインの `Config` スキーマで検証されて `apply(ctx, config)` に渡る | `entry.ts:15`; `$AI/cordis/lib/types/registry.d.ts` 相当 `src/registry.ts:104,181-186` | | `disabled` | `true` で行と子孫を停止。**`!!js` 式を書ける**(§1.4) | `entry.ts:19`; `entry.ts:73-93` | | `inject` | 要求サービス名の配列、または `名前 → intercept config` のマップ。**entry 行の inject は、プラグインがコード側で持つ `inject` にマージされる** | `entry.ts:21`; `$AI/cordis-plugin-loader/src/index.ts:129-135`(`Inject.resolve(fiber.entry.options.inject, fiber.inject)`) | | `group` | `true` で `config` が子行の配列になる(キャリア行) | `entry.ts:17`; `group.ts:75-89` | **`id` を指定した override の書き方**(実例 2 つ): ```yaml # 実例1: $AI/dsh-headless/cordis.patch.yml:9-13(config 全体を置換) - id: system-prompt config: personaSuffix: Your working directory is {{cwd}}. personaPrefix: >- You are a coding agent powered by the {{model}} model. ``` ```yaml # 実例2: $AI/dsh-headless/cordis.patch.yml:33-34(無効化のみ) - id: hmr disabled: true ``` `name` を併記する override もある(`$HOME/.dsh/profiles/headless/cordis.patch.yml:5-7` など)。 `name` は上書き対象ではなく**一致検査**に使われる点に注意(§1.1 表)。 ### 1.4 `!!js` 式 定義(`$AI/dsh-app-boot/lib/index.js:20-26`): ```js const JsExpr = new yaml.Type("tag:yaml.org,2002:js", { kind: "scalar", resolve: (data) => typeof data === "string", construct: (data) => ({ __jsExpr: data }), predicate: isJsExpr, represent: (data) => data["__jsExpr"] }); ``` 評価器(`$AI/cordis-plugin-loader/src/config/utils.ts:5-9`): ```js export const evaluate = new Function('ctx', 'expr', ` with (ctx) { return eval(expr) } `) ``` **スコープ**: `with (ctx)` なので、自由識別子はそのエントリの Cordis `Context` のプロパティとして解決される。 `ctx` 自身も名前として使える。boot 時に `ctx.provide("dshHomePath", dshHomePath)` が入るので `dshHomePath('sessions')` が書ける(`$AI/dsh-app-boot/lib/index.js:4071`、実例 `$AI/dsh-base/cordis.patch.yml:133`)。 **使い方の実例(3 種)**: ```yaml # (a) 真偽値ゲート — $AI/dsh-base/cordis.patch.yml:22 disabled: !!js "!ctx.get('profileContext')" # (b) 環境変数フォールバック — $AI/dsh-base/cordis.patch.yml:207,211 mode: !!js process.env.DSH_TELEMETRY_MODE || 'FEEDBACK_ONLY' url: !!js process.env.DSH_TELEMETRY_OTLP_URL ?? 'https://…/v1/logs' # (c) 値そのものを式で組み立てる(複文は IIFE) — dsh-tui バンドル実物 config: !!js >- (() => { …; return { personaPrefix: persona } })() ``` **制約(実装から確定できるもの)**: 1. **単一式しか書けない**(`eval(expr)` の戻り値が値になる)。文が必要なら IIFE にする。→ 実例 (c)。 2. `interpolate` は**配列・オブジェクトを再帰的に**走査するので、`config` の入れ子のどこにでも置ける (`utils.ts:12-22`)。 3. ただし **`group` / `include` キャリア行の `config` は「リテラルのまま」** で、子行の `!!js` は **その子行自身の fiber** で評価される(`$AI/cordis-plugin-loader/src/index.ts:104-113` の分岐)。 4. `disabled` は評価結果が options に書き戻されず、**元の `__jsExpr` ノードのまま保持**される (`entry.ts:85-93`)。だから HMR / write-back しても式が保存される。 5. `disabled` 式が throw した場合は「無効化」ではなく**エントリの失敗**として扱われる (`$AI/dsh-app-boot/lib/index.js:3999-4001`)。 6. スキーマは `JSON_SCHEMA` なので YAML 1.1 の `yes/no/on/off` は**真偽値にならない**。 `true/false` のみが boolean。`openAt: never`(`$AI/dsh-base/cordis.patch.yml:153`)が文字列のまま 通っているのはこのため(`index.js:33-34`)。 ### 1.5 パッチ層の適用順(プロファイル起動時) `readProfilePatches`(`$AI/dsh-app-boot/lib/index.js:1023-1034`)と `composeProfile`(`$DSH/lib/profile-boot-BZ2ZjNWi.js:205-216`)より: ``` 1. 各バンドルの cordis.patch.yml (dsh.profile.bundles の順) 2. プロファイル自身の cordis.patch.yml ← ホットリロード対象 3. $DSH_HOME/cordis.patch.yml (全プロファイル共通のユーザー層) 4. --patch オーバーレイ(argv 順、複数可) 5. テレメトリ無効化パッチ(DSH_TELEMETRY_DISABLED が非空のときのみ生成) ``` 最後に書いた層が同一 id に対して勝つ(`dsh-base/cordis.patch.yml:1-4` のコメントも同旨)。 --- ## 2. 自作プラグインモジュールの契約 ### 2.1 モジュール解決後に何が起きるか `Entry._init()`(`$AI/cordis-plugin-loader/src/config/entry.ts:221-235`): ```js exports = await this.parent.tree.import(this.options.name, this.getOuterStack) const plugin = this.loader.unwrapExports(exports) this.fiber = this.ctx.registry.plugin(plugin, this.options.config, this.getOuterStack).ctx.fiber ``` `unwrapExports`(`$AI/cordis-plugin-loader/src/index.ts:200-208`): ```js unwrapExports(exports) { if (isNullable(exports)) return exports exports = exports.default ?? exports // default export があればそれ if (!exports.__esModule) return exports // ESM namespace はここで確定 return exports.default ?? exports } ``` → **`export default` でも名前付き export 群でもよい。** ESM のモジュール名前空間オブジェクトは `__esModule` を持たないので、そのままプラグインオブジェクトとして渡る。 (ESM 名前空間は frozen だが、`Plugin.Base` の読み取りしかしないため問題ない。) ### 2.2 プラグインとして認められる 3 形態 `$AI/cordis/src/registry.ts:92-133`: ```ts export type Plugin = Plugin.Function | Plugin.Constructor | Plugin.Object export namespace Plugin { export interface Base { name?: string // 表示名(fiber 診断・logger 名) Config?: StandardSchemaV1 // config 検証器 inject?: Inject // 要求サービス provide?: string | string[] // 提供サービス名 intercept?: Dict // intercept config を消費するサービス } export interface Function extends Base { (ctx: Context, config: T): any } export interface Constructor extends Base { new (ctx: Context, config: T): any } export interface Object extends Base { apply(ctx: Context, config: T): any } } ``` **`reusable` は存在しない。** `$AI/cordis` 4.0.4 の `Plugin.Base` に該当フィールドは無く、 `src/registry.ts` 全体にも `reusable` は 1 箇所も無い(grep 実測)。 他のパッケージの `lib/index.js` に `reusable` の語が出るのは**無関係な英単語**(例: `$AI/dsh-skill-badge/lib/index.js:18` のスキル説明文 "…or a reusable dsh badge asset…")。 → **我々のプラグインで `reusable` を export しても無視される。** ### 2.3 `apply` のシグネチャ(実物のミニマル例) 実在する最小級プラグイン `$AI/dsh-skill-badge/lib/index.js:44-53`(全文 53 行): ```js /** Cordis plugin name. */ const name = "skill-badge"; /** Service required by the bundled provider. */ const inject = ["skills"]; /** Register the bundled `dsh-badge` provider on `ctx.skills`. */ function apply(ctx) { ctx.skills.registerProvider(() => provider); } export { apply, inject, name }; ``` 同じ形の型定義 `$AI/dsh-skill-badge/lib/types/index.d.ts`: ```ts export declare const name = "skill-badge"; export declare const inject: string[]; export declare function apply(ctx: Context): void; ``` `config` を使う場合(`$AI/dsh-agent-instructions`): - `lib/index.js:25` … `const Config = z.object({ … })`(`@deepseek-ai/schemastery`) - `lib/index.js:765` … `const name = "agent-instructions"` - `lib/index.js:1073` … `const inject = ["sessionProjections"]` - `lib/index.js:1099` … `function apply(ctx, config)` - `lib/index.js:1312` … `export { Config, apply, discoverBaselineInstructionFiles, inject, loadBaselineInstructions, name, renderAgentInstructions }` - 型: `export declare function apply(ctx: Context, config: Config): void;`(`lib/types/index.d.ts` 末尾) **実際の export 形の分布(実測)**: `$AI/dsh-*/lib/index.js` の `export { … }` 行を数えると `name` / `inject` / `apply` / `Config` の 4 点セットが標準で、`Config` を含むものが 69 件。 他に `default` で Service クラスを出すもの(`$AI/dsh-session-title/lib/index.js:647`)もある。 ### 2.4 `inject` の書き方 `Inject.resolve`(`$AI/cordis/src/registry.ts:65-88`)が受け付ける形: - **配列**: `inject: ["skills"]`(`$AI/dsh-skill-badge/lib/index.js:47`) - **オブジェクト**: `名前 → intercept config`(`null`/`undefined` は「config 無し」) - **クラスの static `inject`** はプロトタイプチェーンを辿って合成される(`registry.ts:40-44`) entry 行の `inject`(パッチ側)はコード側の `inject` に**加算マージ**される (`$AI/cordis-plugin-loader/src/index.ts:129-135`)。dsh-tui バンドルはこれを使って 「行側でしか表現できない順序保証」を足している(dsh-tui の `cordis.patch.yml` の `dsh-tui` 行の `inject: [workspaceRegistry, agents, …]` と、その直後の長いコメント)。 ### 2.5 `Config`(schemastery)の使い方 - 型は `StandardSchemaV1`(`registry.ts:104`)。DSH 内の実装は `@deepseek-ai/schemastery` (`$AI/dsh-agent-instructions/package.json:41` に `"@deepseek-ai/schemastery": "~3.18.4"`)。 - `import z from "@deepseek-ai/schemastery"` して `z.object({...})` を作り `Config` として export。 - 検証は `ctx.registry.plugin(plugin, config)` の中で fiber 生成前に行われる (`registry.ts:316-330`)。`entry.ts:164-195` の `fiber.config` が検証済みの値。 - `z.object({ … .default(…) })` を使うと、パッチで一部キーだけ書いても残りは default で埋まる (`$AI/dsh-agent-instructions/lib/index.js:25-34` が実例)。 - スキーマは `dsh --dump-config-schema` が集めて JSON Schema として出力する (`$DSH/lib/dump-config-schema-DhhNOaro.js:29-33`、`$AI/dsh-app-boot/lib/index.js:3182-3186`)。 --- ## 3. ローダが `name` をどう解決するか ### 3.1 解決の実装 `EntryTree.import`(`$AI/cordis-plugin-loader/src/config/tree.ts:111-129`): ```js import(name, getOuterStack) { if (name.startsWith('cordis:')) return this.ctx.loader.builtins[name.slice(7)] return composeError(async (info) => { info.offset += 3 if (this.ctx.loader.internal) { return await this.ctx.loader.internal.import(name, this.ctx.baseUrl, {}) // ★通常はこれ } else if (name.startsWith('.')) { return await import(new URL(name, this.ctx.baseUrl).href) } else { return await import(name) } }, getOuterStack) } ``` **`this.ctx.loader.internal` は実在する**(本環境で実測)。`ModuleLoader.fromInternal()` (`$AI/cordis-plugin-loader/src/internal.ts:131-143`)が Node 内部 ESM ローダを取り出し、 `node-addon-require-builtin`(`$DSH/node_modules/node-addon-require-builtin` に実在)経由で `internal/modules/esm/loader` を取得する。実測結果: ``` $ node /tmp/dsh_probe_loader.mjs node: v22.22.1 internalLoader: v1 file-url -> OK PROBE_TARGET_LOADED abs-path -> OK PROBE_TARGET_LOADED relative-vs-base-> OK ``` → 本環境では常に **Node の内部 ESM ローダ** が使われる。したがって `name` は **Node の ESM 解決規則そのもの**で解決される。 ### 3.2 `name` に書ける値と、解決される先 | `name:` の形 | 解決 | 実測/根拠 | |---|---|---| | パッケージ名 `@scope/pkg` | Node の node_modules 探索。親 URL = **ルート include のディレクトリ(=プロファイルディレクトリ)** | `tree.ts:122` の `this.ctx.baseUrl` | | サブパス `@scope/pkg/sub` | パッケージの `exports["./sub"]`(無ければ失敗) | 例 `$AI/dsh-headless/cordis.patch.yml:22` の `@deepseek-ai/dsh-headless/startup` と `$AI/dsh-headless/package.json:21-24` の `"./startup"` | | **相対パス** `./x.js` / `../x.js` | baseUrl(プロファイルディレクトリ)基準。**ただしパッチ層経由なら自動で絶対化される(§3.3)** | `tree.ts:123-124` | | **絶対パス** `/abs/x.js` | Node は `/` 始まりを「base 相対 URL」として `file:///abs/x.js` に解決する → **動く(実測)** | 実測 `abs-path -> OK` | | **`file://` URL** | Node がそのまま URL として解決 → **動く(実測)** | 実測 `file-url -> OK` | | `cordis:group` / `cordis:include` | ローダのビルトイン | `tree.ts:113-115`; `$AI/dsh-app-boot/lib/index.js:3692-3703` | **実測はプロダクションと同一経路**(`internal.import(spec, base, {})`)で行った。 ### 3.3 相対/絶対パスを書いたときの自動変換(重要) `parsePatchList`(`$AI/dsh-app-boot/lib/index.js:3559-3571`)は末尾で `anchorInsertedPluginNames` を必ず呼ぶ(`:3570`): ```js // $AI/dsh-app-boot/lib/index.js:3536-3545 function anchorInsertedPluginNames(patches, file) { const base = dirname(resolve(file)); const visit = (entry) => { if (typeof entry.name === "string" && (isAbsolute(entry.name) || entry.name.startsWith("./") || entry.name.startsWith("../"))) entry.name = pathToFileURL(resolve(base, entry.name)).href; if (entry.group && Array.isArray(entry.config)) entry.config.forEach(visit); }; for (const patch of patches) patch.insert?.forEach(visit); return patches; } ``` → **バンドルの `cordis.patch.yml` / プロファイルの `cordis.patch.yml` / `--patch` オーバーレイ**に `insert` で書いた `name: './plugin.js'` は、**そのパッチファイルのディレクトリ基準の `file://` URL** に 変換されてからローダに渡る。`cordis.yml` に直接書いた場合はこの変換が無く、プロファイルディレクトリ基準になる。 注意: この変換は `insert` 行だけを辿る。非 insert(override)行には適用されない。 ### 3.4 ビルド成果物を `lib/` に置く必要があるか - **ローダは `package.json` の `exports` / `main` に従うだけ**なので、`lib/` という名前自体に意味は無い。 ただし実在する全バンドルは `"type": "module"` + `"main": "lib/index.js"` + `"exports": { ".": { "types": …, "default": "./lib/index.js" } }` を宣言している (`$AI/dsh-base/package.json:13-24`、`$AI/dsh-headless/package.json:13-28`)。 - **必要になるのは「Node が直接実行できる JS であること」**。`internal.import` は Node の素の ESM パイプラインなので、TypeScript のままでは読めない。本環境の Node は v22.22.1 で 型ストリッピング用の Amaro が無い(= TS 実行不可、既知の制約)。**必ず JS へビルドしてから載せる。** - `files` フィールドに `lib/index.js` と `cordis.patch.yml` を入れる(`$AI/dsh-headless/package.json:29-35`)。 - 相対パスで直接 JS ファイルを指す場合(§5.3 の回避策)は `exports` すら不要。 --- ## 4. プロファイル側の用意 ### 4.1 プロファイルとは `$AI/dsh-app-boot/lib/index.js:466-481`(モジュールコメント): > A profile is a directory under `$DSH_HOME/profiles/` holding a `package.json` > (out-of-tree plugin dependencies plus the profile manifest `dsh.profile` with its ordered > `bundles` list) and a `cordis.patch.yml` (the user's own patch layer, applied after every > bundle layer). `$DSH_HOME` は `DSH_HOME` 環境変数 → `~/.dsh` の順で解決される (`$AI/dsh-home-paths/lib/index.js:11-15, 73-76`)。本環境では実際に `DSH_HOME=/home/mugi/.dsh` が 設定済みで、それが使われている。 ### 4.2 ディレクトリに必要なファイル `initProfile`(`$AI/dsh-app-boot/lib/index.js:576-592`)が作るもの=正解の集合: ```js // 1) package.json { "name": `dsh-profile-${basename(dir)}`, "private": true, "dependencies": {}, "dsh": { "profile": { "bundles": [...bundles] } } } // 2) cordis.patch.yml ← PROFILE_PATCH_TEMPLATE($AI/dsh-app-boot/lib/index.js:558-562) # Your patch layer for this dsh profile, applied after every bundle layer: # a top-level YAML array of loader patch entries (id-targeted config # overrides, disables, and insert lists; `!!js` expressions allowed). [] // 3) pnpm-workspace.yaml ← PROFILE_PNPM_WORKSPACE($AI/dsh-app-boot/lib/index.js:563-568) packages: - . nodeLinker: hoisted autoInstallPeers: false ``` さらに `prepareProfile`(`$DSH/lib/profile-boot-BZ2ZjNWi.js:185-191`)が **毎回** `cordis.yml` を書き込む: ```js // $DSH/lib/profile-boot-BZ2ZjNWi.js:189 writeFileSync(join(profile.dir, PROFILE_ROOT_FILENAME), PROFILE_ROOT_CONFIG); // PROFILE_ROOT_FILENAME = "cordis.yml" ``` 中身は `$DSH/lib/profile-boot-BZ2ZjNWi.js:121-125` のとおり**空配列 `[]` + 説明コメント**。 `--dump-config` / `--dump-config-schema` も同じ `prepareProfile` を通るので**必ず書き込まれる**。 実在するプロファイル実例(`$HOME/.dsh/profiles/headless/`): `cordis.patch.yml` / `cordis.yml` / `package.json` / `pnpm-workspace.yaml` の 4 ファイル (+ 外部依存を入れたプロファイルは `pnpm-lock.yaml` と `node_modules/`)。 ### 4.3 `dsh.profile.bundles` `DshProfileManifest`(`$AI/dsh-package-manifest/lib/types/types.d.ts:70-74`): ```ts export interface DshProfileManifest { /** Ordered bundle layer list, using installed package names. */ bundles?: string[]; } ``` 読込は `loadProfileDirectory`(`$AI/dsh-app-boot/lib/index.js:920-956`): ```js const bundles = readProfileManifest(binName, dir).dsh?.profile?.bundles ?? []; for (const packageName of bundles) try { const packageDir = resolveBundleDir(binName, packageName, installAnchor, dir); const bundleManifest = readProfileManifest(binName, packageDir); const bundle = bundleManifest.dsh?.bundle; if (bundle === void 0) throw new Error(`… declares no dsh.bundle in its package.json`); const issue = evaluatePluginCompatibility(bundleManifest, exemptions); if (issue !== void 0 && !issue.exempted) throw new Error(pluginCompatibilityWarning(issue)); const patchPaths = bundlePatchPaths(packageDir, bundle); const patches = patchPaths.flatMap((patchPath) => loadOverlayPatches(binName, patchPath)); layers.push({ packageName, packageDir, patchPaths, patches }); } catch (error) { skippedBundles.push({ packageName, reason: String(error) }); } ``` → 解決できない/`dsh.bundle` が無い/互換性 NG のバンドルは **エラーにならずスキップされ**、 `profile.skippedBundles` に積まれて `reportSkippedBundles` が stderr に 1 行出す (`:515-517`)。**「バンドルを書いたのに何も起きない」ときはまず stderr を見る。** ### 4.4 新規プロファイル作成の正しい手順 | 手段 | 挙動 | 根拠 | |---|---|---| | 手で作る | `initProfile` と同じ 3 ファイルを置けばよい。**`dsh.profile.bundles` を自由に書ける** | `$AI/dsh-app-boot/lib/index.js:576-592` | | `dsh --from-default-profile