mcpField
C
cli-architecture-pattern

cli-architecture-pattern

github.com/loxosceles-dev開発128 回インストール
リポジトリ

cli-architecture-pattern について

インフラ操作を CLI ツールとして構成するためのリファレンススキルです。プロジェクトの複雑さに応じた1〜3層の設計を、JavaScript/TypeScript と Python の例とともに示します。

主な機能

  1. 011〜3層の CLI パターン
  2. 02プロジェクト規模に応じた設計選択
  3. 03引数解析とロジックの分離
  4. 04小さなコアモジュールの組み合わせ
  5. 05JavaScript/TypeScript と Python の例

活用シーン

  1. 01分散したインフラコマンドの整理
  2. 02プロジェクトの CLI 構成選択
  3. 03共有ロジックとエントリーポイントの分離

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/loxosceles-dev/dev-skills --skill cli-architecture-pattern

インストール後、Claude Code で/cli-architecture-patternを入力して読み込まれたか確認します。

詳細

CLI アーキテクチャパターン

これは参考パターンです。 このアプローチから学び、自分のコンテキストに合わせて応用してください。そのままコピーしないでください。

問題:インフラ運用がシェルスクリプト、手動コマンド、デプロイパイプラインに分散すると、保守が難しくなり、一貫性も失われます。

解決策:統一されたエントリーポイントを備えた、構造化された CLI ツールです。


このパターンを採用する理由

利点:

  • 一貫性:同じコマンドをローカル、CI/CD、デプロイスクリプトで使えます
  • 可読性:pnpm deploy:dev は、AWS CLI の生のコマンドより明確です
  • テスト容易性:CLI コマンドを個別にテストできます
  • 再利用性:異なるエントリーポイント間でロジックを共有できます
  • ドキュメント:コマンドは --help で自己説明的になります

統合ポイント:

  • ルートの package.json スクリプト(JS/TS プロジェクト)
  • pyproject.toml スクリプト(Python プロジェクト)
  • GitHub Actions ワークフロー
  • デプロイスクリプト(bash など)
  • ローカル開発

統合例:

// package.json
{
  "scripts": {
    "deploy:dev": "ts-node cli/bin/deploy.ts --env=dev",
    "export-params:dev": "ts-node cli/bin/ssm-params.ts export dev frontend"
  }
}
# GitHub Actions
- name: Deploy infrastructure
  run: pnpm deploy:dev

- name: Export parameters
  run: pnpm export-params:dev

1 層アーキテクチャ(シンプルなプロジェクト向け)

構造:

lib/
  infra.ts               # Single entry point: arg parsing + business logic
  core/
    environment-manager.ts  # Bootstrap config loader (shared utility)

単一ファイル - すべてのコマンドを 1 つの switch にまとめます:

// lib/infra.ts
import { EnvironmentManager } from './core/environment-manager';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { CloudFormationClient, DescribeStacksCommand } from '@aws-sdk/client-cloudformation';
import { CloudFrontClient, CreateInvalidationCommand } from '@aws-sdk/client-cloudfront';
import { spawn } from 'child_process';

const [command] = process.argv.slice(2);

async function main() {
  switch (command) {
    case 'deploy':
      await deploy();
      break;
    case 'publish':
      await publish();
      break;
    case 'synth':
      await run('npx', ['cdk', 'synth']);
      break;
    default:
      console.log('Commands: deploy | publish | synth');
      process.exit(1);
  }
}

async function deploy() {
  console.log('📦 Provisioning infrastructure...');
  await run('npx', ['cdk', 'deploy', '--all', '--require-approval', 'never']);
  console.log('🏗️ Publishing frontend...');
  await publish();
}

async function publish() {
  const stage = getStage();
  const stackName = `myproject-web-${stage}`;
  const region = new EnvironmentManager(stage).load().cdkDefaultRegion;

  await run('pnpm', ['--filter', 'frontend', 'run', 'build']);

  const bucketName = await getStackOutput(stackName, 'WebBucketName', region);
  await syncToS3('frontend/out', bucketName, region);

  const distId = await getStackOutput(stackName, 'CloudfrontDistributionId', region);
  await invalidate(distId, region);

  console.log('✅ Published');
}

// ... helper functions (run, getStackOutput, syncToS3, invalidate)

main().catch((err) => {
  console.error(err.message);
  process.exit(1);
});

統合:

// package.json
{
  "scripts": {
    "deploy:dev": "ENVIRONMENT=dev ts-node infrastructure/lib/infra.ts deploy",
    "deploy:prod": "ENVIRONMENT=prod ts-node infrastructure/lib/infra.ts deploy",
    "publish:dev": "ENVIRONMENT=dev ts-node infrastructure/lib/infra.ts publish",
    "synth": "ENVIRONMENT=dev ts-node infrastructure/lib/infra.ts synth"
  }
}

使用する状況:

  • シンプルなプロジェクト(静的フロントエンド、単一スタック)
  • コマンドが 5 個以下
  • 抽象化する価値のある、コマンド間で共有されるロジックがない
  • 迅速な実装を優先する

2 層への移行を検討する状況:

  • コマンドの合計が約 200 行を超える
  • --verbose、--dry-run などの共有フラグが必要になる
  • 複数のコマンドで大幅なロジックを共有する

2 層アーキテクチャ(ほとんどのプロジェクトに推奨)

構造:

cli/
  bin/           # Tier 1: Argument parsing
  commands/      # Tier 2: Business logic + SDK calls
shared/
  env-loader.ts  # Simple utilities
  configs.ts

第 1 層:CLI バイナリ - エントリーポイントのみ

// bin/ssm-params.ts
import { Command } from 'commander';
import { getParameter } from '../commands/ssm-params';

const program = new Command();

program
  .command('get <name>')
  .action(async (name: string) => {
    const value = await getParameter(name);
    console.log(value);
  });

program.parse();

第 2 層:コマンド - SDK を直接使用

// commands/ssm-params.ts
import { SSMClient, GetParameterCommand } from '@aws-sdk/client-ssm';
import { loadEnv } from '../shared/env-loader';

loadEnv();

const ssmClient = new SSMClient({ 
  region: process.env.CDK_DEFAULT_REGION 
});

export async function getParameter(name: string): Promise<string> {
  const command = new GetParameterCommand({ Name: name });
  const response = await ssmClient.send(command);
  return response.Parameter?.Value ?? '';
}

使用する状況:

  • 中規模のプロジェクト
  • CLI ツールが 2~5 個
  • 直感的な操作
  • 迅速な開発を優先する

3 層アーキテクチャ(複雑なプロジェクト向け)

継承ではなくコンポジションを使用します。小さく、役割を絞ったモジュールをコマンドで組み合わせます。

構造:

cli/
  bin/           # Tier 1: Argument parsing + error handling ONLY
  commands/      # Tier 2: Business logic, composes core modules
core/
  logger.ts          # Tier 3: Standalone utilities
  env.ts
  aws/
    ssm.ts
    s3.ts

第 1 層:CLI バイナリ — エントリーポイントのみ(ビジネスロジックは含めない)

// bin/ssm-params.ts
import { Command } from 'commander';
import { getParameter, exportParams } from '../commands/ssm-params';

const program = new Command();

program
  .command('get <name>')
  .option('-v, --verbose', 'verbose output')
  .action(async (name, opts) => {
    const value = await getParameter(name, { verbose: opts.verbose });
    console.log(value);
  });

program
  .command('export <stage> <service>')
  .action(async (stage, service) => {
    await exportParams(stage, service);
  });

program.parse();

第 3 層:コアモジュール — 小さく、役割を絞り、継承を使わない

各モジュールは 1 つの処理を行い、通常の関数またはシンプルなファクトリー関数をエクスポートします:

// core/logger.ts
export function createLogger(verbose: boolean) {
  return {
    log: (msg: string) => { if (verbose) console.log(msg); },
    error: (msg: string) => console.error(msg),
  };
}
// core/env.ts
export function loadEnv(stage: string): Record<string, string> {
  const envPath = `.env.${stage}`;
  // Load from file, fall back to process.env
  return { ...parseEnvFile(envPath), ...process.env };
}

export function requireEnv(key: string): string {
  const value = process.env[key];
  if (!value) throw new Error(`${key} is required`);
  return value;
}
// core/aws/ssm.ts
import { SSMClient, GetParameterCommand, GetParametersByPathCommand } from '@aws-sdk/client-ssm';

export function createSSMClient(region: string) {
  const client = new SSMClient({ region });

  return {
    async getParameter(name: string): Promise<string> {
      const res = await client.send(new GetParameterCommand({ Name: name }));
      return res.Parameter?.Value ?? '';
    },

    async getParametersByPath(path: string): Promise<Record<string, string>> {
      const res = await client.send(new GetParametersByPathCommand({ Path: path }));
      const params: Record<string, string> = {};
      for (const p of res.Parameters ?? []) {
        if (p.Name && p.Value) params[p.Name] = p.Value;
      }
      return params;
    },
  };
}

第 2 層:コマンド — コアモジュールを組み合わせ、すべてのビジネスロジックを含みます

// commands/ssm-params.ts
import { createLogger } from '../core/logger';
import { requireEnv } from '../core/env';
import { createSSMClient } from '../core/aws/ssm';

export async function getParameter(name: string, opts: { verbose?: boolean } = {}) {
  const log = createLogger(opts.verbose ?? false);
  const region = requireEnv('AWS_REGION');
  const ssm = createSSMClient(region);

  log.log(`Fetching parameter: ${name}`);
  return ssm.getParameter(name);
}

export async function exportParams(stage: string, service: string) {
  const region = requireEnv('AWS_REGION');
  const projectId = requireEnv('PROJECT_ID');
  const ssm = createSSMClient(region);

  const params = await ssm.getParametersByPath(`/${projectId}/${stage}/${service}/`);
  for (const [key, value] of Object.entries(params)) {
    const envKey = key.split('/').pop();
    console.log(`${envKey}=${value}`);
  }
}

継承よりコンポジションを選ぶ理由:

  • 各モジュールを個別にテストできます。モックする基底クラスは不要です
  • 親クラスによる隠れた動作がありません
  • 実装を簡単に入れ替えられます(例:異なるクラウドプロバイダー)
  • コマンドが必要なものを明示的に宣言します。基底クラスから暗黙的に this.log() を呼び出すことはありません

使用する状況:

  • CLI ツールが 5 個以上ある大規模プロジェクト
  • 複数のコマンドでコアモジュール(SSM、S3、ロギング)を共有する
  • 一貫したパターンが必要なチーム環境

選択ガイド

要素1 層2 層3 層
プロジェクト規模シンプル中規模大規模
CLI コマンド5 個以下2~5 個5 個以上
チーム規模1 人1~3 人3 人以上
複雑さ最小限中程度複雑
開発速度最速速い初期は遅い
再利用性なし低い高い
共有ロジック最小限一部大幅

静的フロントエンドや単一スタックのプロジェクトでは、1 層から始めてください。 コマンドが約 200 行を超える場合や、大幅なロジックを共有する場合は、2 層に移行してください。 次の場合は、3 層に移行してください:

  • CLI ツールが 5 個以上ある
  • コマンド間でロジックが重複している
  • チームで一貫したパターンが必要
  • 抽象化によって明確な価値が得られる

言語固有の統合

JavaScript/TypeScript - package.json:

{
  "scripts": {
    "deploy:dev": "ts-node cli/bin/deploy.ts --env=dev",
    "params:get": "ts-node cli/bin/ssm-params.ts get"
  }
}

Python - pyproject.toml:

[tool.poetry.scripts]
deploy-dev = "cli.bin.deploy:main"
params-get = "cli.bin.ssm_params:get_parameter"

Makefile - 汎用:

deploy-dev:
	python cli/bin/deploy.py --env=dev

params-get:
	python cli/bin/ssm_params.py get $(NAME)

関連パターン


段階的な改善

開発者がこのスキルで防ぐべきだった動作を修正した場合は、今後同じ修正が必要にならないよう、このスキルに加える具体的な変更を提案してください。

コメント