npm version npm downloads license
fsss — "File Structure, Single Schema" — bun + TypeScript + Zod
ファイル構造がそのままコマンド構造になり、スキーマを一回書けば CLI フラグ・環境変数・設定ファイルのどこから値が来ても同じように型付きで受け取れる CLI フレームワーク
A CLI framework where your file structure becomes your command structure, and a single schema gives you typed values whether they come from flags, env vars, or config files.
commands/
serve.ts → my-app serve
config/
set.ts → my-app config set
get.ts → my-app config get
remote/
[name]/
push.ts → my-app remote origin push
ファイルを置くだけでコマンドが生える。ディレクトリのネストがサブコマンドの階層になる。[name] は動的セグメントで、params.name として値を受け取れる。
// commands/serve.ts export default defineCommand({ description: "サーバーを起動する", args: { port: { type: z.coerce.number().min(1).max(65535), description: "ポート番号", alias: "p", default: 3000, }, host: { type: z.string(), description: "ホスト名", default: "localhost", }, }, run({ args }) { // args.port: number, args.host: string — 型推論される console.log(`${args.host}:${args.port}`); }, });
この1つの定義だけで、CLI フラグ・環境変数・設定ファイル・デフォルト値のすべてが統合される。
CLI flag my-app serve --port 8080 ← 最優先
env MYAPP_SERVE_PORT=5000 ← prefix + コマンドパス + arg 名で自動導出
config file { "serve": { "port": 4000 } } ← コマンドツリーと同じ構造
default 3000
どのソースから来た値も、最終的に同じ Zod スキーマでバリデーションされる。
autoEnv を指定すると、コマンドパス + arg 名から環境変数名を自動導出する。
const cli = createCLI({ name: "my-app", autoEnv: { prefix: "MYAPP" }, });
| コマンド | arg | 自動導出される env 名 |
|---|---|---|
serve |
port |
MYAPP_SERVE_PORT |
remote push |
force |
MYAPP_REMOTE_PUSH_FORCE |
config ファイルの JSON 構造はコマンドツリーと一致する。
{
"serve": { "port": 5000, "host": "0.0.0.0" },
"remote": { "push": { "force": true } }
}my-app --config app.json serve
$ my-app serve --help
サーバーを起動する
Usage: my-app serve [options]
Options:
-p, --port <port> ポート番号 (env: MYAPP_SERVE_PORT, default: 3000)
--host <host> ホスト名 (env: MYAPP_SERVE_HOST, default: localhost)
-h, --help ヘルプを表示する
commands/
_plugins/
logger.ts ← 全コマンドに適用
serve.ts
remote/
_plugins/
auth.ts ← remote 配下にのみ適用
[name]/
push.ts
commands/ ツリーの任意の階層に _plugins/ を配置するだけで、その階層以下のコマンドにプラグインが自動適用される。
// commands/_plugins/logger.ts export default definePlugin(({ cliName }) => ({ provide: { logger: { info: (msg: string) => console.log(`[${cliName}] ${msg}`), }, }, middleware: async (_ctx, next) => { const start = performance.now(); await next(); console.log(`${(performance.now() - start).toFixed(0)}ms`); }, }));
provideで注入した値はコマンドのrun()でextensionsとして型付きで受け取れるmiddlewareはコマンドの実行を包む onion model(root 側が外側、leaf 側が内側)- Extensions の型は
fsss-codegenで自動生成される
run({ extensions }) { extensions.logger.info("hello"); }
- ファイルベースルーティング — コマンドツリー、動的セグメント、params と args の分離
- スキーマと値の解決 — defineCommand、args 定義、値の優先順位、ヘルプ生成
- 設定ファイルと環境変数 — autoEnv、config ファイル階層、
--configフラグ - プラグインシステム —
_plugins/規約、ミドルウェア、Extensions の型生成 - 内部アーキテクチャ — パイプライン設計、各モジュールの責務、処理トレース
- 既存ツールとの比較 — commander / oclif / Pastel / Gud CLI / gunshi / convict との比較