CLI を 2 本持つ

node 版はそのままに、C# 版を並べる。定義は 1 か所から配る

📅 作成: 2026-09-02 / 更新: 2026-09-07

課題 i260901-06 の計画である。csc で aichat.exe を作り、node 版 (src/client/chat.mjs)はそのまま残して両方を保守する。

目次

  1. なぜ 2 本持つのか
  2. 決めたこと
  3. 作ったもの
  4. 食い違いを防ぐ仕組み
  5. 速さは狙いにならなかった

なぜ 2 本持つのか

始まりはコマンドラインの長さである。node 版は node ...\src\client\chat.mjs と書かせる。この形はパスの区切りで 2 回落ちた。Bash に渡ると \ がエスケープとして消え、Cannot find module になる。

観点いまの node 版exe にしたら
起動 node ...\chat.mjs と書く。パスの区切りで 2 回落ちた aichat だけ。パスを書かせない
Node への依存 要る (v22.5 以降。node:sqlite のため) 要らない。ただし CLI は DB を触らないので、そもそも sqlite は不要
保守 1 本 2 本になる。オプションを増やすたび両方を直す

2 本を保守すると、必ず食い違う。検討を始めた日にも usage() と USAGE の一覧が食い違っていた。食い違いを検出する手立てを、作る前に決める。

先に決めた 6 つの論点

#論点中身
1どこまで作るか全コマンドか、待受けとよく使う分 (wait say recent)だけか
2食い違いをどう防ぐか同じ振る舞いのテストを両方に当てる。オプションの表を 2 本の共通の元にできるか
3置き場納品するものは src/。ビルドは tools/20_build/。exe の置き場も決める
4ビルドの前提csc はどこにあるか。.NET SDK か、Windows 同梱のものか。まず実測する
5exe を Git に入れるかビルド済みを配るのか、各自でビルドするのか
6どちらを正とするか資料に書くコマンドはどちらにするか。両方書くと読み手が迷う

決めたこと

項目中身
どこまで作るか全コマンド。「一部だけ」だと、どちらを使うか毎回考えることになる
食い違いの防ぎ方src/client/options.mjs を唯一の出どころにし、JSON を埋め込む
ビルドcsc 単ファイル。SDK が要らない
名前と置き場aichat.exe を root に置く。Git 管理外 (作り直せる)
既定値接続先と ID は持たない。node 版と同じ。黙って本番に書く事故を防ぐ
ソースの置き場src/cli-cs/。他プロジェクトが使うものなので納品側
node 版の呼び方aichat-node.cmd を root に置き、PATH から呼べるようにした

node 版にも「パスを書かせない」利点を持たせた。aichat-node.cmd の中身は node 版を呼ぶだけである。

おかげでプロセスを数える式が 1 つで済む。どちらで張っても、コマンドラインに aichat が残る。

作ったもの

2026-09-02 に作った。8 ファイルと、ビルド・書き出し・テストである。

aichat.exe                              root。57.5 KB。Git 管理外
src/cli-cs/Program.cs                   入口・使い方の表示
src/cli-cs/Commands.cs                  各コマンド
src/cli-cs/Args.cs                      引数の解析
src/cli-cs/Client.cs                    HTTP (繋ぎ直し・503・終了コード 3)
src/cli-cs/Json.cs                      小さな JSON の読み書き
src/cli-cs/Definition.cs                埋め込んだ定義を読む
src/cli-cs/WaitLog.cs                   待受けの記録
src/cli-cs/Waiters.cs                   走っている待受けを数える
tools/20_build/export-options.mjs       options.mjs から JSON を書き出す
aichat-node.cmd                         node 版を呼ぶだけの包み。root。PATH から呼べる
tools/20_build/build-aichat.ps1 / .cmd  ビルド
tools/40_test/measure-cli-startup.ps1   起動の速さを測る (20 回 × 2 巡)
tests/cli-cs.test.mjs                   node 版と出力が揃うかを見る (11 件)

exe を作り直すときは待受けを止めない。走っている待受けが aichat.exe を掴んでおり、そこには他プロジェクトの分も混ざる。build-aichat.ps1 は掴まれているときだけ tmp/ へ退避する。

食い違いを防ぐ仕組み

定義は 1 か所にしかない。src/client/options.mjs から JSON を書き出し、ビルド時に exe へ埋め込む (csc /resource:)。手で写していないので、オプションを増やしても 2 本がずれない。

埋め込んだ JSON には schema を持たせた。形が変わったら C# 側が起動時に気づいて止まる。

そのうえで tests/cli-cs.test.mjs が両方を実際に動かして出力を比べる。時刻や番号は揺れるので形だけを見る。

見ていること中身
--helpコマンドとオプションの並びが一字一句同じか
join1 行目の形。参加者の一覧は増減するので数は見ない
相互に読めるかC# 版で say したものを node 版の recent で読む
廃止したオプション両方が同じ文で、終了コード 2 で止まるか
接続先なし両方が終了コード 2 で止まるか
定義の出どころ書き出した JSON と options.mjs が一致するか

1 度だけ食い違いが出て、テストが見つけた。説明を始める列が node 版 40・C# 版 37 でずれていた。中身は同じで表示だけが違う。

目で読んで気づける差ではない。見つけたいのはまさにこの類である。

速さは狙いにならなかった

20 回 × 2 巡 (順序を入れ替え)、ウォームアップ 3 回を捨てて最小値で比べた。測るのは tools/40_test/measure-cli-startup.cmd。

Node 自体の起動の方が、C# (.NET Framework)より速かった。CLR の初期化は V8 より重い。C# が勝っているのは、node が options.mjs などを読み込む分だけである。その差も、書き換える理由になる大きさではない。

はじめの測定は粗かった。5 回の平均で、出力を | Out-Null に流していた。パイプラインの処理が乗り、プロセスの起動時間が埋もれる。ウォームアップも捨てていない。

数字は結果として近かったが、根拠としては弱かった。測り方を決めてから測り直した。

残った利点

残る利点は「パスを書かせない」ことだけである。それでも価値はある。パスの区切りで 2 回落ちたのは、コマンドラインが長いことが原因だった。aichat と打てるなら、その手の事故は起きない。

観点node 版C# 版
書き方aichat-node wait :id: -p 8787aichat wait :id: -p 8787
Node への依存要る要らない。ただしサーバーは Node で動くので、同じ PC では意味が薄い
大きさ—42 KB。SDK も実行環境も要らない
起動の速さ差はあるが、書き換える理由になる大きさではない