diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..af6c7d4 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,81 @@ +FROM node:20 + +ARG TZ +ENV TZ="$TZ" + +# Install basic development tools and iptables/ipset +RUN apt update && apt install -y less \ + git \ + procps \ + sudo \ + fzf \ + zsh \ + man-db \ + unzip \ + gnupg2 \ + gh \ + iptables \ + ipset \ + iproute2 \ + dnsutils \ + aggregate \ + jq \ + vim + +# Ensure default node user has access to /usr/local/share +RUN mkdir -p /usr/local/share/npm-global && \ + chown -R node:node /usr/local/share + +ARG USERNAME=node + +RUN echo "node ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers + +# Persist bash history. +RUN SNIPPET="export PROMPT_COMMAND='history -a' && export HISTFILE=/commandhistory/.bash_history" \ + && mkdir /commandhistory \ + && touch /commandhistory/.bash_history \ + && chown -R $USERNAME /commandhistory + +# Set `DEVCONTAINER` environment variable to help with orientation +ENV DEVCONTAINER=true + +# Create workspace and config directories and set permissions +RUN mkdir -p /workspace /home/node/.claude && \ + chown -R node:node /workspace /home/node/.claude + +WORKDIR /workspace + +RUN ARCH=$(dpkg --print-architecture) && \ + wget "https://github.com/dandavison/delta/releases/download/0.18.2/git-delta_0.18.2_${ARCH}.deb" && \ + sudo dpkg -i "git-delta_0.18.2_${ARCH}.deb" && \ + rm "git-delta_0.18.2_${ARCH}.deb" + +# Set up non-root user +USER node + +# Rust and similarity-ts install +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y +RUN ~/.cargo/bin/cargo install similarity-ts + +# Install global packages +ENV NPM_CONFIG_PREFIX=/usr/local/share/npm-global +ENV PATH=$PATH:/usr/local/share/npm-global/bin + +# Set the default shell to zsh rather than sh +ENV SHELL=/bin/zsh + +# Default powerline10k theme +RUN sh -c "$(wget -O- https://github.com/deluan/zsh-in-docker/releases/download/v1.2.0/zsh-in-docker.sh)" -- \ + -p git \ + -p fzf \ + -a "source /usr/share/doc/fzf/examples/key-bindings.zsh" \ + -a "source /usr/share/doc/fzf/examples/completion.zsh" \ + -a "export PROMPT_COMMAND='history -a' && export HISTFILE=/commandhistory/.bash_history" \ + -x + +### Customize ### + +# Install mise +RUN curl https://mise.run | sh +RUN echo "eval \"\$(/home/node/.local/bin/mise activate zsh)\"" >> "/home/node/.zshrc" +RUN echo "alias ccinstall=\"npm install -g @anthropic-ai/claude-code\"" >> "/home/node/.zshrc" diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..8043394 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,48 @@ +{ + "name": "Claude Code Sandbox", + "build": { + "dockerfile": "Dockerfile", + "args": { + "TZ": "${localEnv:TZ:America/Los_Angeles}" + } + }, + "runArgs": ["--cap-add=NET_ADMIN", "--cap-add=NET_RAW"], + "customizations": { + "vscode": { + "extensions": [ + "eamodio.gitlens", + "biomejs.biome" + ], + "settings": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "biomejs.biome", + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + }, + "terminal.integrated.defaultProfile.linux": "zsh", + "terminal.integrated.profiles.linux": { + "bash": { + "path": "bash", + "icon": "terminal-bash" + }, + "zsh": { + "path": "zsh" + } + } + } + } + }, + "remoteUser": "node", + "mounts": [ + "source=claude-code-bashhistory-${devcontainerId},target=/commandhistory,type=volume", + "source=claude-code-config-${devcontainerId},target=/home/node/.claude,type=volume" + ], + "remoteEnv": { + "NODE_OPTIONS": "--max-old-space-size=4096", + "CLAUDE_CONFIG_DIR": "/home/node/.claude", + "POWERLEVEL9K_DISABLE_GITSTATUS": "true", + "GITHUB_TOKEN": "${localEnv:DEVCONTAINER_GITHUB_TOKEN}" + }, + "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=delegated", + "workspaceFolder": "/workspace" +} diff --git a/.gitignore b/.gitignore index 58346f6..1248700 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,14 @@ .Trashes ehthumbs.db Thumbs.db + +# Dependencies +node_modules/ +package-lock.json +yarn.lock +pnpm-lock.yaml + +# Build output +dist/ +*.tsbuildinfo +assets/ diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..cffe8cd --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +save-exact=true diff --git a/.secretlintrc.json b/.secretlintrc.json new file mode 100644 index 0000000..7a1a5df --- /dev/null +++ b/.secretlintrc.json @@ -0,0 +1,7 @@ +{ + "rules": [ + { + "id": "@secretlint/secretlint-rule-preset-recommend" + } + ] +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..3d823a8 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,67 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## 概要 + +TsumikiはAI駆動開発フレームワークのコマンドテンプレートを提供するCLIツールです。このプロジェクトはTypeScript + ReactをInkで構成されたCLIアプリケーションで、Claude Code用のコマンドテンプレートをユーザーの`.claude/commands/`ディレクトリにインストールします。 + +## 開発コマンド + +```bash +# 開発環境 +pnpm install # 依存関係のインストール + +# ビルド +pnpm build # プロジェクトをビルドし、commandsディレクトリをdist/にコピー +pnpm build:run # ビルド後、CLI実行(テスト用) + +# コード品質 +pnpm check # Biomeでコードチェック +pnpm fix # Biomeで自動修正 +pnpm typecheck # TypeScriptの型チェック(tsgoを使用) +pnpm secretlint # シークレット情報の検査 + +# pre-commitフック +pnpm prepare # simple-git-hooksのセットアップ +``` + +## プロジェクト構造 + +- **`src/cli.ts`**: CLIエントリーポイント、commanderを使用してコマンド定義 +- **`src/commands/install.tsx`**: React + Inkを使用したインストールコマンドのUI実装 +- **`commands/`**: TsumikiのAI開発フレームワーク用Claude Codeコマンドテンプレート(`.md`と`.sh`ファイル) +- **`dist/`**: ビルド出力、`dist/commands/`にテンプレートがコピーされる + +## 技術スタック + +- **CLI Framework**: Commander.js +- **UI Framework**: React + Ink(CLIでのReactレンダリング) +- **Build Tool**: tsup(TypeScript + ESBuildベース) +- **Code Quality**: Biome(リンタ・フォーマッタ) +- **TypeScript**: tsgo(高速型チェック) +- **Package Manager**: pnpm + +## ビルドプロセス + +ビルド時(`pnpm build`)は以下の処理が実行されます: +1. `dist`ディレクトリをクリーンアップ +2. `dist/commands`ディレクトリを作成 +3. `commands/`内の`.md`と`.sh`ファイルを`dist/commands/`にコピー +4. tsupでTypeScriptコードをESMとCJSの両形式でビルド + +## インストール動作 + +`tsumiki install`コマンドは以下を実行します: +1. 現在のディレクトリに`.claude/commands/`ディレクトリを作成 +2. ビルド済みの`dist/commands/`から全ての`.md`と`.sh`ファイルをコピー +3. React + Inkでプログレス表示とファイル一覧を表示 + +## 品質管理 + +Pre-commitフックで以下が自動実行されます: +- `pnpm secretlint`: 機密情報のチェック +- `pnpm typecheck`: 型チェック +- `pnpm fix`: コードの自動修正 + +コード修正時は必ず`pnpm check`と`pnpm typecheck`を実行してからコミットしてください。 \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..1a925f1 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,195 @@ +# コントリビューションガイド + +Tsumikiプロジェクトへのコントリビューションをありがとうございます!このガイドでは、プロジェクトに貢献する方法について説明します。 + +## 開発環境のセットアップ + +### 必要な環境 + +- Node.js 18.0.0以上 +- pnpm 10.13.1以上 + +### セットアップ手順 + +1. リポジトリをフォークしてクローンします: + +```bash +git clone https://github.com/YOUR_USERNAME/tsumiki.git +cd tsumiki +``` + +2. 依存関係をインストールします: + +```bash +pnpm install +``` + +3. pre-commitフックをセットアップします: + +```bash +pnpm prepare +``` + +## 開発ワークフロー + +### ブランチ戦略 + +- `main`ブランチ:安定版のコード +- 機能開発:`feature/機能名` +- バグ修正:`bugfix/バグ名` +- ホットフィックス:`hotfix/修正内容` + +### 開発手順 + +1. 新しいブランチを作成します: + +```bash +git checkout -b feature/your-feature-name +``` + +2. コードを変更します + +3. コード品質チェックを実行します: + +```bash +# 型チェック +pnpm typecheck + +# コードチェック +pnpm check + +# 自動修正 +pnpm fix + +# 機密情報チェック +pnpm secretlint +``` + +4. ビルドテストを実行します: + +```bash +pnpm build:run +``` + +5. 変更をコミットします: + +```bash +git add . +git commit -m "feat: 新機能の追加" +``` + +## コミットメッセージ規約 + +[Conventional Commits](https://www.conventionalcommits.org/)形式を使用してください: + +- `feat:` 新機能 +- `fix:` バグ修正 +- `docs:` ドキュメント変更 +- `style:` コードスタイル変更(機能に影響しない) +- `refactor:` リファクタリング +- `test:` テスト追加・修正 +- `chore:` ビルドプロセスやツール変更 + +例: +``` +feat: add new install command for .sh files +fix: resolve path handling issue in install command +docs: update README with new command examples +``` + +## コード品質基準 + +### 自動チェック + +Pre-commitフックで以下が自動実行されます: + +- **secretlint**: 機密情報(APIキー、パスワードなど)の混入チェック +- **typecheck**: TypeScriptの型チェック +- **fix**: Biomeによるコードの自動修正 + +### 手動チェック + +変更前に以下のコマンドを実行してください: + +```bash +# 全てのチェックを実行 +pnpm typecheck && pnpm check && pnpm secretlint + +# コードの自動修正 +pnpm fix +``` + +## プロジェクト構造 + +``` +tsumiki/ +├── src/ +│ ├── cli.ts # CLIエントリーポイント +│ └── commands/ +│ └── install.tsx # インストールコマンドのUI実装 +├── commands/ # コマンドテンプレート(.md, .sh) +├── dist/ # ビルド出力 +├── package.json +├── CLAUDE.md # プロジェクト指示書 +└── README.md +``` + +## プルリクエスト + +### プルリクエストの作成 + +1. 変更をプッシュします: + +```bash +git push origin feature/your-feature-name +``` + +2. GitHubでプルリクエストを作成します + +3. プルリクエストテンプレートに従って説明を記載します + +### プルリクエストの要件 + +- [ ] 変更内容が明確に説明されている +- [ ] 関連するIssueがリンクされている(該当する場合) +- [ ] コード品質チェックが通っている +- [ ] ビルドが成功している +- [ ] 機密情報が含まれていない + +## Issue報告 + +バグ報告や機能要望は[Issues](https://github.com/classmethod/tsumiki/issues)で受け付けています。 + +### バグ報告 + +以下の情報を含めてください: + +- 再現手順 +- 期待される動作 +- 実際の動作 +- 環境情報(OS、Node.jsバージョンなど) +- エラーメッセージ(該当する場合) + +### 機能要望 + +以下の情報を含めてください: + +- 提案する機能の説明 +- ユースケース +- 期待される利益 +- 実装案(あれば) + +## セキュリティ + +セキュリティに関する問題を発見した場合は、公開のIssueではなく、プライベートに報告してください。 + +## ライセンス + +このプロジェクトはMITライセンスの下で公開されています。コントリビューションする際は、このライセンスに同意したものとみなされます。 + +## 質問・サポート + +- [Issues](https://github.com/classmethod/tsumiki/issues) - バグ報告、機能要望 +- [Discussions](https://github.com/classmethod/tsumiki/discussions) - 質問、議論 + +コントリビューションをお待ちしています! \ No newline at end of file diff --git a/README.md b/README.md index fddfbe5..b02dd2a 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,16 @@ TsumikiはAI駆動開発のためのフレームワークです。要件定義から実装まで、AIを活用した効率的な開発プロセスを提供します。 +## インストール + +Tsumikiを使用するには、次のnpxコマンドでインストールしてください: + +```bash +npx tsumiki install +``` + +このコマンドを実行すると、`.claude/commands/` にTsumikiのClaude Codeスラッシュコマンドがインストールされます。 + ## 概要 Tsumikiは以下の2つのコマンドで構成されています: @@ -85,6 +95,13 @@ Kairoは要件定義から実装までの開発プロセスを自動化・支援 /rev-requirements ``` +### 開発環境のクリーンアップ + +```bash +# 開発環境をクリーンアップ +/clear +``` + ## 詳細なマニュアル 使用方法の詳細、ディレクトリ構造、ワークフロー例、トラブルシューティングについては [MANUAL.md](./MANUAL.md) を参照してください。 diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..4c4e966 --- /dev/null +++ b/biome.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.1.3/schema.json", + "vcs": { + "enabled": false, + "clientKind": "git", + "useIgnoreFile": false + }, + "files": { + "ignoreUnknown": false + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2 + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + }, + "assist": { + "enabled": true, + "actions": { + "source": { + "organizeImports": "on" + } + } + } +} diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000..40ca167 --- /dev/null +++ b/mise.toml @@ -0,0 +1,3 @@ +[tools] +node = "24" +pnpm = "latest" diff --git a/package.json b/package.json new file mode 100644 index 0000000..bcdcba3 --- /dev/null +++ b/package.json @@ -0,0 +1,78 @@ +{ + "name": "tsumiki", + "private": false, + "version": "0.0.6", + "description": "A CLI tool for install tsumiki commands", + "keywords": [ + "cli", + "tsumiki", + "claude", + "claudecode", + "ai" + ], + "homepage": "https://github.com/classmethod/tsumiki#readme", + "bugs": { + "url": "https://github.com/classmethod/tsumiki/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/classmethod/tsumiki.git" + }, + "license": "MIT", + "author": { + "name": "classmethod" + }, + "type": "module", + "exports": { + ".": { + "types": "./dist/cli.d.ts", + "import": "./dist/cli.js", + "require": "./dist/cli.cjs" + } + }, + "main": "./dist/cli.cjs", + "module": "./dist/cli.js", + "types": "./dist/cli.d.ts", + "bin": { + "tsumiki": "./dist/cli.js" + }, + "files": [ + "dist" + ], + "scripts": { + "build": "rm -rf dist && mkdir -p dist/commands && cp ./commands/*.md ./commands/*.sh dist/commands/ 2>/dev/null || true && tsup", + "build:run": "pnpm build && node dist/cli.js", + "check": "biome check src", + "fix": "biome check src --write", + "prepare": "simple-git-hooks", + "secretlint": "secretlint --secretlintignore .gitignore **/*", + "typecheck": "tsgo --noEmit" + }, + "simple-git-hooks": { + "pre-commit": "pnpm secretlint && pnpm typecheck && pnpm fix" + }, + "dependencies": { + "commander": "14.0.0", + "fs-extra": "11.3.0", + "ink": "6.1.0", + "react": "19.1.1" + }, + "devDependencies": { + "@biomejs/biome": "2.1.3", + "@secretlint/secretlint-rule-preset-recommend": "10.2.1", + "@tsconfig/node24": "24.0.1", + "@types/fs-extra": "11.0.4", + "@types/node": "24.1.0", + "@types/react": "19.1.9", + "@typescript/native-preview": "7.0.0-dev.20250729.2", + "secretlint": "10.2.1", + "simple-git-hooks": "2.13.0", + "tsup": "8.5.0", + "tsx": "4.20.3", + "typescript": "5.8.3" + }, + "packageManager": "pnpm@10.13.1", + "engines": { + "node": ">=18.0.0" + } +} diff --git a/src/cli.ts b/src/cli.ts new file mode 100644 index 0000000..e3653fb --- /dev/null +++ b/src/cli.ts @@ -0,0 +1,30 @@ +#!/usr/bin/env node + +import { Command } from "commander"; +import { gitignoreCommand } from "./commands/gitignore.js"; +import { installCommand } from "./commands/install.js"; +import { uninstallCommand } from "./commands/uninstall.js"; + +const program = new Command(); + +program + .name("tsumiki") + .description("CLI tool for installing Claude Code command templates") + .version("1.0.0"); + +program + .command("install") + .description("Install Claude Code command templates to .claude/commands/") + .action(installCommand); + +program + .command("uninstall") + .description("Uninstall Claude Code command templates from .claude/commands/") + .action(uninstallCommand); + +program + .command("gitignore") + .description("Add commands/*.{md,sh} to .gitignore file") + .action(gitignoreCommand); + +program.parse(); diff --git a/src/commands/gitignore.tsx b/src/commands/gitignore.tsx new file mode 100644 index 0000000..3ddf50b --- /dev/null +++ b/src/commands/gitignore.tsx @@ -0,0 +1,212 @@ +import * as path from "node:path"; +import { fileURLToPath } from "node:url"; +import fs from "fs-extra"; +import { Box, Newline, render, Text } from "ink"; +import React, { useEffect, useState } from "react"; + +type GitignoreStatus = + | "starting" + | "checking" + | "updating" + | "completed" + | "skipped" + | "error"; + +const GitignoreComponent: React.FC = () => { + const [status, setStatus] = useState("starting"); + const [addedRules, setAddedRules] = useState([]); + const [skippedRules, setSkippedRules] = useState([]); + const [error, setError] = useState(null); + + useEffect(() => { + const performGitignoreUpdate = async (): Promise => { + try { + setStatus("checking"); + + const currentDir = process.cwd(); + const gitignorePath = path.join(currentDir, ".gitignore"); + + // tsumikiのcommandsディレクトリを取得 + const __filename = fileURLToPath(import.meta.url); + const __dirname = path.dirname(__filename); + // ビルド後はdist/commandsを参照(cli.jsがdist/にあるため) + const tsumikiDir = path.join(__dirname, "commands"); + + // commandsディレクトリ内のすべての.mdファイルと.shファイルを取得 + const files = await fs.readdir(tsumikiDir); + const targetFiles = files.filter( + (file) => file.endsWith(".md") || file.endsWith(".sh"), + ); + + // 具体的なファイルパスをルールとして作成 + const rulesToAdd = targetFiles.map( + (file) => `.claude/commands/${file}`, + ); + + let gitignoreContent = ""; + let gitignoreExists = false; + + try { + gitignoreContent = await fs.readFile(gitignorePath, "utf-8"); + gitignoreExists = true; + } catch { + gitignoreExists = false; + } + + const existingLines = gitignoreContent + .split("\n") + .map((line) => line.trim()); + const rulesToActuallyAdd: string[] = []; + const rulesAlreadyExist: string[] = []; + + for (const rule of rulesToAdd) { + if (existingLines.includes(rule)) { + rulesAlreadyExist.push(rule); + } else { + rulesToActuallyAdd.push(rule); + } + } + + if (rulesToActuallyAdd.length === 0) { + setSkippedRules(rulesAlreadyExist); + setStatus("skipped"); + setTimeout(() => { + process.exit(0); + }, 2000); + return; + } + + setStatus("updating"); + + let newContent = gitignoreContent; + if ( + gitignoreExists && + gitignoreContent.length > 0 && + !gitignoreContent.endsWith("\n") + ) { + newContent += "\n"; + } + + if (gitignoreExists && gitignoreContent.length > 0) { + newContent += "\n# Tsumiki command templates\n"; + } else { + newContent = "# Tsumiki command templates\n"; + } + + for (const rule of rulesToActuallyAdd) { + newContent += `${rule}\n`; + } + + await fs.writeFile(gitignorePath, newContent); + + setAddedRules(rulesToActuallyAdd); + setSkippedRules(rulesAlreadyExist); + setStatus("completed"); + + setTimeout(() => { + process.exit(0); + }, 2000); + } catch (err) { + const errorMessage = + err instanceof Error ? err.message : "Unknown error occurred"; + setError(errorMessage); + setStatus("error"); + + setTimeout(() => { + process.exit(1); + }, 3000); + } + }; + + performGitignoreUpdate(); + }, []); + + if (status === "starting") { + return ( + + 🚀 .gitignore の更新を開始します... + + ); + } + + if (status === "checking") { + return ( + + 📋 .gitignore ファイルをチェック中... + + ); + } + + if (status === "updating") { + return ( + + ✏️ .gitignore を更新中... + + ); + } + + if (status === "error") { + return ( + + ❌ エラーが発生しました: + {error} + + ); + } + + if (status === "skipped") { + return ( + + ⏭️ すべてのルールが既に存在します + + 既存のルール: + {skippedRules.map((rule) => ( + + • {rule} + + ))} + + .gitignore の更新は不要でした + + ); + } + + if (status === "completed") { + return ( + + ✅ .gitignore の更新が完了しました! + + {addedRules.length > 0 && ( + <> + 追加されたルール ({addedRules.length}個): + {addedRules.map((rule) => ( + + • {rule} + + ))} + + )} + {skippedRules.length > 0 && ( + <> + 既存のルール ({skippedRules.length}個): + {skippedRules.map((rule) => ( + + • {rule} + + ))} + + )} + + + Tsumiki のコマンドファイルが Git から無視されるようになりました + + + ); + } + + return null; +}; + +export const gitignoreCommand = (): void => { + render(React.createElement(GitignoreComponent)); +}; diff --git a/src/commands/install.tsx b/src/commands/install.tsx new file mode 100644 index 0000000..ecc78a8 --- /dev/null +++ b/src/commands/install.tsx @@ -0,0 +1,138 @@ +import * as path from "node:path"; +import { fileURLToPath } from "node:url"; +import fs from "fs-extra"; +import { Box, Newline, render, Text } from "ink"; +import React, { useEffect, useState } from "react"; + +type InstallStatus = + | "starting" + | "checking" + | "copying" + | "completed" + | "error"; + +const InstallComponent: React.FC = () => { + const [status, setStatus] = useState("starting"); + const [copiedFiles, setCopiedFiles] = useState([]); + const [error, setError] = useState(null); + + useEffect(() => { + const performInstall = async (): Promise => { + try { + setStatus("checking"); + + // 現在のディレクトリを取得 + const currentDir = process.cwd(); + const targetDir = path.join(currentDir, ".claude", "commands"); + + // tsumikiのcommandsディレクトリを取得 + const __filename = fileURLToPath(import.meta.url); + const __dirname = path.dirname(__filename); + // ビルド後はdist/commandsを参照(cli.jsがdist/にあるため) + const tsumikiDir = path.join(__dirname, "commands"); + + // .claude/commandsディレクトリが存在しない場合は作成 + await fs.ensureDir(targetDir); + + setStatus("copying"); + + // commandsディレクトリ内のすべての.mdファイルと.shファイルを取得 + const files = await fs.readdir(tsumikiDir); + const targetFiles = files.filter( + (file) => file.endsWith(".md") || file.endsWith(".sh"), + ); + + const copiedFilesList: string[] = []; + + for (const file of targetFiles) { + const sourcePath = path.join(tsumikiDir, file); + const targetPath = path.join(targetDir, file); + + await fs.copy(sourcePath, targetPath); + copiedFilesList.push(file); + } + + setCopiedFiles(copiedFilesList); + setStatus("completed"); + + // 2秒後に終了 + setTimeout(() => { + process.exit(0); + }, 2000); + } catch (err) { + const errorMessage = + err instanceof Error ? err.message : "Unknown error occurred"; + setError(errorMessage); + setStatus("error"); + + setTimeout(() => { + process.exit(1); + }, 3000); + } + }; + + performInstall(); + }, []); + + if (status === "starting") { + return ( + + 🚀 Tsumiki インストールを開始します... + + ); + } + + if (status === "checking") { + return ( + + 📋 環境をチェック中... + + ); + } + + if (status === "copying") { + return ( + + 📝 コマンドテンプレートをコピー中... + + ); + } + + if (status === "error") { + return ( + + ❌ エラーが発生しました: + {error} + + ); + } + + if (status === "completed") { + return ( + + ✅ インストールが完了しました! + + コピーされたファイル ({copiedFiles.length}個): + {copiedFiles.map((file) => ( + + {" "} + • {file} + + ))} + + + Claude Codeで以下のようにコマンドを使用できます: + + /tdd-requirements + /kairo-design + ... + + ); + } + + return null; +}; + +export const installCommand = (): void => { + render(React.createElement(InstallComponent)); +}; diff --git a/src/commands/uninstall.tsx b/src/commands/uninstall.tsx new file mode 100644 index 0000000..d287c4d --- /dev/null +++ b/src/commands/uninstall.tsx @@ -0,0 +1,180 @@ +import * as path from "node:path"; +import { fileURLToPath } from "node:url"; +import fs from "fs-extra"; +import { Box, Newline, render, Text } from "ink"; +import React, { useEffect, useState } from "react"; + +type UninstallStatus = + | "starting" + | "checking" + | "removing" + | "completed" + | "error" + | "not_found"; + +const UninstallComponent: React.FC = () => { + const [status, setStatus] = useState("starting"); + const [removedFiles, setRemovedFiles] = useState([]); + const [error, setError] = useState(null); + + useEffect(() => { + const performUninstall = async (): Promise => { + try { + setStatus("checking"); + + // 現在のディレクトリを取得 + const currentDir = process.cwd(); + const targetDir = path.join(currentDir, ".claude", "commands"); + + // .claude/commandsディレクトリが存在するかチェック + const dirExists = await fs.pathExists(targetDir); + if (!dirExists) { + setStatus("not_found"); + setTimeout(() => { + process.exit(0); + }, 2000); + return; + } + + // tsumikiのcommandsディレクトリを取得 + const __filename = fileURLToPath(import.meta.url); + const __dirname = path.dirname(__filename); + // ビルド後はdist/commandsを参照(cli.jsがdist/にあるため) + const tsumikiDir = path.join(__dirname, "commands"); + + // tsumikiのファイル一覧を取得 + const tsumikiFiles = await fs.readdir(tsumikiDir); + const tsumikiTargetFiles = tsumikiFiles.filter( + (file) => file.endsWith(".md") || file.endsWith(".sh"), + ); + + setStatus("removing"); + + // .claude/commands内のファイルをチェックして、tsumiki由来のファイルのみ削除 + const installedFiles = await fs.readdir(targetDir); + const removedFilesList: string[] = []; + + for (const file of installedFiles) { + if (tsumikiTargetFiles.includes(file)) { + const filePath = path.join(targetDir, file); + await fs.remove(filePath); + removedFilesList.push(file); + } + } + + // 削除後に.claude/commandsディレクトリが空になったかチェック + const remainingFiles = await fs.readdir(targetDir); + if (remainingFiles.length === 0) { + // 空のディレクトリを削除 + await fs.rmdir(targetDir); + // .claudeディレクトリも空の場合は削除 + const claudeDir = path.dirname(targetDir); + const claudeFiles = await fs.readdir(claudeDir); + if (claudeFiles.length === 0) { + await fs.rmdir(claudeDir); + } + } + + setRemovedFiles(removedFilesList); + setStatus("completed"); + + // 2秒後に終了 + setTimeout(() => { + process.exit(0); + }, 2000); + } catch (err) { + const errorMessage = + err instanceof Error ? err.message : "Unknown error occurred"; + setError(errorMessage); + setStatus("error"); + + setTimeout(() => { + process.exit(1); + }, 3000); + } + }; + + performUninstall(); + }, []); + + if (status === "starting") { + return ( + + 🗑️ Tsumiki アンインストールを開始します... + + ); + } + + if (status === "checking") { + return ( + + 📋 インストール状況をチェック中... + + ); + } + + if (status === "removing") { + return ( + + 🗑️ コマンドテンプレートを削除中... + + ); + } + + if (status === "not_found") { + return ( + + + ⚠️ .claude/commands ディレクトリが見つかりません + + Tsumikiはインストールされていないようです。 + + ); + } + + if (status === "error") { + return ( + + ❌ エラーが発生しました: + {error} + + ); + } + + if (status === "completed") { + if (removedFiles.length === 0) { + return ( + + ⚠️ 削除対象のファイルが見つかりませんでした + + Tsumikiのコマンドはインストールされていないようです。 + + + ); + } + + return ( + + ✅ アンインストールが完了しました! + + 削除されたファイル ({removedFiles.length}個): + {removedFiles.map((file) => ( + + {" "} + • {file} + + ))} + + + TsumikiのClaude Codeコマンドテンプレートが削除されました。 + + + ); + } + + return null; +}; + +export const uninstallCommand = (): void => { + render(React.createElement(UninstallComponent)); +}; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..7510be6 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "@tsconfig/node24/tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "jsx": "react" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100644 index 0000000..abbfc66 --- /dev/null +++ b/tsup.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: ['src/cli.ts'], + format: ['esm', 'cjs'], + dts: true, + outDir: 'dist', + clean: false, + target: 'es2022', + tsconfig: 'tsconfig.json', + esbuildOptions: (options) => { + options.jsx = 'automatic'; + }, +});