AI に「最小の TODO アプリ」を作らせ、GitHub に公開する―実戦編のはじめかた

ここまでの 7 回で、Git の地図はひととおり見てきました。コミットの鎖、3 つの領域、ブランチとマージ、取り消しとやり直し、チームでの協働、そして worktree。仕組みは分かった。でも、読んで分かったことと、自分の手で動かせることの間には、まだ距離があります。

その距離を埋めるのが、この実戦編です。小さな TODO アプリを 1 つ、ゼロから育てながら、これまで学んだ操作を全部使い切ります。しかも、コードは 1 行も自分で書きません。

AI に「こう作って」とひと言頼む。私たちの仕事は、技術者になることではなく、技術の分かるマネージャーとして AI に的確に頼み、結果を Git で守ることです。初回の今日は、アプリをゼロから作り、最初の記録を残し、GitHub で世界に公開するところまでを一気に通します。

TL;DR

  • 実戦編は、1 つの TODO アプリを育てながら本編で学んだ Git 操作を全部使います。初回はゼロから公開まで一本道で通します。
  • アプリは AI にひと言で作らせます。素の HTML・CSS・JavaScript だけなので、インストールもビルドも要りません。
  • プロジェクトは 最初に .gitignore を置く のが習慣です。そのあと git initaddcommit で最初の記録を残し、gh repo create 一発で公開します。
  • AI に頼むと、結果は「こちらが指定した値」と「AI が勝手に決めた値」に分かれます。この線引きを毎回はっきりさせるのが、実戦編の主役です。

この連載で作るもの

これから育てるのは、ブラウザだけで動くシンプルな TODO アプリ です。最終的には、タスクの追加・完了・削除、残り件数の表示、ブラウザを閉じても消えない保存、絞り込みフィルタ、ダークモード、並び替えまでを備え、GitHub Pages で公開し、push するだけで自動デプロイされる状態まで持っていきます。

大事なのは、機能を 1 つ足すたびに、本編で見た Git 操作とちょうど 1 対 1 で対応させていくことです。

  • 今回 (第 8 回): ゼロからアプリを作り、git init →最初のコミット→ gh repo create で公開
  • 第9回: 完了チェックと削除を 別のブランチ で足し、わざとコンフリクトを起こして解決する
  • 第10回: 保存機能を入れながら revert / reset / reflog で取り消しとやり直しを体験する
  • 第11回: 絞り込みフィルタを プルリクエスト で取り込む
  • 第12回: ダークモードと並び替えを worktree で並行開発する
  • 第13回: GitHub Pages で公開し、v1.0 のタグを打つ
  • 第14回: GitHub Actions で push したら自動でデプロイされる仕組みを作る

本編が「Git の仕組みを内側から理解する」回だったとすれば、実戦編は「その仕組みを実際の開発で使う」回です。仕組みの説明は本編に譲り、手を動かすことに集中します。

今日のゴールまでの流れです。

flowchart TD
    L["手元のフォルダ<br/>index.html を作る"] -->|"git init → add → commit"| G["Git の履歴<br/>最初の記録を残す"]
    G -->|"gh repo create --push"| H["GitHub<br/>世界に公開する"]

手元でファイルを作り、Git で記録し、GitHub へ送る。この左から右への一本道を、今日のうちに最後まで通します。

まず AI に「最小の TODO」を作らせる

何から始めればいいか。まずは動くものを 1 つ、AI に作らせます。完璧を目指さず、「テキストを入れて追加するとリストに増える」だけの最小形でかまいません。

私が実際に AI へ渡したのは、次のひと言です。

ブラウザで開くだけで動く、最小のTODOアプリを作ってほしい。テキストを入力して
『追加』を押すと下のリストに増える、それだけでいい。あとで GitHub Pages にそのまま
載せたいので、インストールやビルドが要らないように、素の HTML・CSS・JavaScript だけで
作って。外部ライブラリやフレームワークは使わないで。

ポイントは 2 つだけ伝えました。「最小の TODO であること」と「素の HTML・CSS・JavaScript だけで作ること」です。あとで GitHub Pages にそのまま載せたいので、ビルドや外部ライブラリが要らない形に縛っておきます。これだけで、AI(今回は Claude) は次の index.html を 1 ファイルで返してきました。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>TODO</title>
  <style>
    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
    }

    body {
      font-family: sans-serif;
      background: #f5f5f5;
      display: flex;
      justify-content: center;
      padding: 40px 16px;
    }

    .container {
      background: #fff;
      border-radius: 8px;
      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
      padding: 32px;
      width: 100%;
      max-width: 480px;
    }

    h1 {
      font-size: 1.4rem;
      margin-bottom: 24px;
      color: #222;
    }

    .input-row {
      display: flex;
      gap: 8px;
      margin-bottom: 24px;
    }

    .input-row input {
      flex: 1;
      padding: 8px 12px;
      font-size: 1rem;
      border: 1px solid #ccc;
      border-radius: 4px;
      outline: none;
    }

    .input-row input:focus {
      border-color: #4a90e2;
    }

    .input-row button {
      padding: 8px 16px;
      font-size: 1rem;
      background: #4a90e2;
      color: #fff;
      border: none;
      border-radius: 4px;
      cursor: pointer;
    }

    .input-row button:hover {
      background: #357abd;
    }

    ul {
      list-style: none;
    }

    li {
      padding: 10px 12px;
      border-bottom: 1px solid #eee;
      color: #333;
      font-size: 1rem;
    }

    li:last-child {
      border-bottom: none;
    }
  </style>
</head>
<body>
  <div class="container">
    <h1>TODO</h1>
    <div class="input-row">
      <input type="text" id="todo-input" placeholder="やることを入力..." />
      <button id="add-btn">追加</button>
    </div>
    <ul id="todo-list"></ul>
  </div>

  <script>
    const input = document.getElementById('todo-input');
    const addBtn = document.getElementById('add-btn');
    const list = document.getElementById('todo-list');

    function addTodo() {
      const text = input.value.trim();
      if (!text) return;

      const li = document.createElement('li');
      li.textContent = text;
      list.appendChild(li);

      input.value = '';
      input.focus();
    }

    addBtn.addEventListener('click', addTodo);

    input.addEventListener('keydown', function (e) {
      if (e.key === 'Enter') addTodo();
    });
  </script>
</body>
</html>

このファイルをブラウザで開くと、白いカードの中に入力欄と「追加」ボタンが並び、入力して押すとリストに増えます。注文どおりの最小形です。

ここで一度立ち止まって、何を指定して、何を指定しなかったか を見てみます。私が伝えたのは「最小の TODO」「素の HTML・CSS・JavaScript」の 2 点だけでした。残りは AI が勝手に埋めています。

  • 空欄のまま追加を押しても増えない (trim() で空白を落として弾く)
  • Enter キーでも追加できる
  • タスクの保存はしない(ブラウザを再読み込みすると消える)
  • 各タスクに id を振らず、画面に直接足している
  • 配色は青 (#4a90e2)、カード幅は最大 480px、入力欄の案内文は「やることを入力…」

どれも私は頼んでいません。AI が「最小の TODO ならこうだろう」と補った部分です。保存やタスクごとの id は、第10回以降で必要になったときに足していきます。最初の段階で全部を求めない。これも AI に頼むときのコツです。

コミットの前に、.gitignore を最初に置く

動くものができました。記録に残す前に、もうひと手間かけます。.gitignore を最初に置く ことです。

なぜ最初なのか。プロジェクトを作ると、自分では作った覚えのないファイルが紛れ込んできます。代表が macOS の .DS_Store(フォルダの表示設定を OS が勝手に覚える隠しファイル) や、エディタが作る .vscode/.idea/ です。どれも他の人には関係のないゴミで、最初の記録に巻き込みたくありません。入口で「これは入れない」と宣言しておきます。

.gitignore はゼロから手書きしなくてかまいません。GitHub が github/gitignore という定番テンプレート集を公開しています。これをベースに用意する作業も、AI に任せられます。

私が AI に渡したのは、このひと言です。

このプロジェクトに合う .gitignore を、GitHub が配布している定番のテンプレートをベースに用意して。

このひと言で、AI は github/gitignore を実際に見にいき、テンプレートを取得して .gitignore を組み立てました。おもしろいのは、ここで AI が下した判断です。

素の HTML/CSS/JavaScript のプロジェクトには、ぴたりと合うテンプレートがありません。一番近いのは Node.gitignore ですが、中身は node_modules/dist/ など、npm を使う前提のものばかり。このアプリにそれらは存在しないので、AI は「名前を借りただけで意味がない」と見送りました。代わりに選んだのが、Global/(OS やエディタなど、言語によらず共通のもの) にある macOS・Windows・Linux・VS Code・JetBrains のテンプレートです。ビルドの生成物が出ない素の HTML では、リポジトリを汚す原因がもっぱら OS とエディタのゴミだからです。

出来上がった .gitignore は、何をベースにしたかをヘッダに書き込んだうえで、OS・エディタごとに区切られていました。全部で 140 行ほどあるので、ヘッダと代表的な行だけ抜き出します。

# =============================================================
# .gitignore for plain HTML/CSS/JavaScript project
# Base: github/gitignore Global templates
#   - Global/macOS.gitignore
#   - Global/VisualStudioCode.gitignore
#   - Global/JetBrains.gitignore
#   - Global/Windows.gitignore
#   - Global/Linux.gitignore
# =============================================================

# macOS
.DS_Store
._*
.Spotlight-V100
.Trashes

# (中略:Windows / Linux / VS Code / JetBrains のセクションが続く)

# このプロジェクト用の追加
*.log
.env
.env.local
.sass-cache/
*.css.map
*.tmp

.DS_Store を 1 行書くだけでも始められます。でも「定番テンプレートをベースに」とひと言頼むだけで、OS やエディタをまたいだ抜け漏れのない無視リストが、出典コメントつきで一度に手に入ります。.gitignore に何を入れるかという考え方(秘密情報・生成物・環境固有のゴミの 3 種類)は、第6回でひととおり扱いました。

ひとつだけ、順番にまつわる落とし穴に触れておきます。.gitignore が効くのは、まだ Git に追跡されていないファイルだけ です。もし先にコミットしてしまって、あとから .gitignore に書いても、すでに追跡中のファイルは無視されません。だから「最初に置く」のが一番ラクなのです。置き忘れて追跡されてしまった場合は、git rm --cached で追跡だけを外す必要があります。この対処は第6回で詳しく扱ったので、ここでは「最初に置けば、この手間がそもそも要らない」とだけ覚えておけば十分です。

Git で「最初の記録」を残す

index.html.gitignore がそろいました。ここで Git の出番です。git init でこのフォルダを履歴管理の対象にし、git add で記録するものを選び、git commit で最初のスナップショットを撮ります。

コミットが「変更の差分」ではなく「その瞬間の全体を撮った 1 枚の写真 (スナップショット)」だという話は、第1回第3回で確かめたとおりです。今から撮るのは、この小さなアプリの最初の 1 枚です。

私が AI に渡したのは、このひと言です。

このフォルダを、これから変更履歴を残せるようにして。今の状態を『最初の記録』として残して。

コマンドを 1 つも指定していません。それでも AI は、次の 3 つを順にこなしました。

git init
git add index.html
git commit -m "Initial commit: add todo app"

ここでも「指定した値」と「AI が勝手に決めた値」を見ておきます。私が伝えたのは「変更履歴を残せるようにして」「今の状態を最初の記録に」だけです。次の点は AI が補いました。

  • 最初のブランチの名前を main にした
  • コミットメッセージを Initial commit: add todo app と英語で命名した
  • git add . のような全体指定ではなく、git add index.html とファイルを名指しした

コミットメッセージは未来の自分への手紙です。AI が付けた Initial commit: add todo app(最初のコミット。todo アプリを追加) は、最初の 1 枚として過不足ありません。気に入らなければ「最初のコミットメッセージは日本語で『TODO アプリの最初の記録』にして」と頼めば、その文言で残してくれます。

flowchart TD
    W["index.html / .gitignore<br/>手元のファイル"] -->|"git add"| S["ステージング<br/>記録する物を選ぶ"]
    S -->|"git commit"| R["最初のスナップショット<br/>main の先頭に残る"]

作業ツリー(手元の編集場所) から、ステージング (買い物カゴ) を経て、リポジトリ (レジ) へ。第4回で見た 3 つの領域を、今まさに最初の 1 往復として通りました。

GitHub に公開する

手元に最初の記録が残りました。最後に、これを GitHub に公開します。

AI に渡したのは、このひと言だけです。

これを GitHub の公開リポジトリ todo-app-git-deep-dive に上げたい。

新規のプロジェクトなら、この一本道で公開まで終わります。

gh repo create todo-app-git-deep-dive --public --source=. --push

この 1 行が、3 つの仕事をまとめてやってくれます。GitHub 上に todo-app-git-deep-dive という公開リポジトリを作り、手元のフォルダをその送り先 (リモート) として登録し、コミットを push する。第3回第4回では git remote addgit push を分けて打ちましたが、gh repo create --source=. --push は「作る・つなぐ・送る」を一度に済ませます。

公開でも、線引きは同じです。私が指定したのは「リポジトリ名 (todo-app-git-deep-dive)」と「公開 (--public)」の 2 点です。残りは AI が補いました。

  • 送り先 (リモート) の名前を origin にした
  • 手元の main と GitHub 側の main を結びつけ、次回から git push だけで送れるようにした
  • README を付けるかどうかを判断した (今回は付けず、index.html だけを上げた)

実行後、表示された URL を開くと、リポジトリが公開されているのが確認できます。

https://github.com/ikuma-hiroyuki/todo-app-git-deep-dive

公開ページの index.html をブラウザで開いて、入力欄に 2 件入れれば 2 件並び、空欄のまま追加を押しても増えず、Enter でも追加できる。注文どおりに動いています。手元のフォルダから始まった一本道が、これで世界に開かれた 1 ページまでつながりました。

AI に頼むときの言い方

今日の作業を振り返ると、毎回「こちらが指定した値」と「指定しなかったので AI が勝手に決めた値」に分かれていました。これを整理すると、AI への頼み方のコツが見えてきます。

種別こちらが指定した値指定しないと AI が勝手に決める値
アプリ最小の TODO・素の HTML/CSS/JS・ライブラリなし空欄の弾き方・Enter 対応・保存の有無・配色・案内文
.gitignore「定番テンプレートをベースに」どのテンプレを選ぶか (Global の 5 本)・Node を流用しない判断・独自に足すルール
記録「最初の記録を残して」ブランチ名 main・コミットメッセージ・add する対象
公開リポジトリ名・公開 (public)リモート名 origin・追従設定・README の有無

忘れてはいけないのは、AI が勝手に決めた値が悪いわけではない、という点です。最小形を作る段階では、むしろ任せたほうが速い。問題は、どこを自分で決めるべきか を分かったうえで任せているか、です。たとえば「保存の有無」は、今は任せていい。でも第10回で保存機能を入れるときは、私が「ブラウザを閉じても残るように」と伝えます。任せる場所と、握る場所。この線引きができると、AI への指示はぐっと的確になります。

まとめ

実戦編の初回として、小さな TODO アプリをゼロから作り、GitHub に公開するまでを一本道で通しました。

  • アプリは AI にひと言で作らせた。指定したのは「最小の TODO」「素の HTML/CSS/JS」の 2 点だけ。
  • 記録の前に、GitHub の定番テンプレート集を元にした .gitignore を AI に用意させ、入口に置いた。素の HTML には専用テンプレがなく、AI は OS・エディタ用の Global テンプレを選んだ。
  • git initaddcommit で最初のスナップショットを撮り、gh repo create --public --source=. --push で公開した。
  • AI に頼むと、結果は「指定した値」と「AI が勝手に決めた値」に分かれる。この線引きが、これから毎回の主役になる。

難しそうだった「世界に公開」が、コマンド 1 つで済む。この手応えが、実戦編を進める燃料になります。次回は、完成したこのアプリに「完了チェック」と「削除」を足します。しかも、いきなり main を触らず別のブランチで作り、わざとコンフリクトを起こして解決します。第4回で仕組みを見たブランチとマージを、今度は本物のアプリで動かします。

なお、この記事で AI に渡したひと言は、すべて Claude(Sonnet) に実際に渡し、書いてあるとおりに動くことを確かめたものです。モデルやその日の状態によって、AI が勝手に決める値 (配色やコミットメッセージなど) は変わります。指定しなかった部分は変わりうる、と思って読んでください。