{"id":51051164,"url":"https://github.com/shinji-san/git-workshop","last_synced_at":"2026-06-22T17:01:35.928Z","repository":{"id":364587157,"uuid":"1259552245","full_name":"shinji-san/git-workshop","owner":"shinji-san","description":"Interactive desktop trainer that stands in for a Git instructor in hands-on workshops — a real sandboxed terminal, auto-graded step-by-step  exercises, and a live commit graph. Offline \u0026 cross-platform.","archived":false,"fork":false,"pushed_at":"2026-06-13T16:30:27.000Z","size":191,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"develop","last_synced_at":"2026-06-13T17:23:38.673Z","etag":null,"topics":["clean-architecture","d3","education","electron","git","git-workshop","interactive-learning","offline","teaching-tool","typescript","xterm-js"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/shinji-san.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-04T16:10:21.000Z","updated_at":"2026-06-13T16:30:30.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/shinji-san/git-workshop","commit_stats":null,"previous_names":["shinji-san/git-workshop"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/shinji-san/git-workshop","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shinji-san%2Fgit-workshop","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shinji-san%2Fgit-workshop/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shinji-san%2Fgit-workshop/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shinji-san%2Fgit-workshop/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/shinji-san","download_url":"https://codeload.github.com/shinji-san/git-workshop/tar.gz/refs/heads/develop","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shinji-san%2Fgit-workshop/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34657902,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-22T02:00:06.391Z","response_time":106,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["clean-architecture","d3","education","electron","git","git-workshop","interactive-learning","offline","teaching-tool","typescript","xterm-js"],"created_at":"2026-06-22T17:01:32.625Z","updated_at":"2026-06-22T17:01:35.917Z","avatar_url":"https://github.com/shinji-san.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Git-Workshop\n\nInteractive desktop trainer for hands-on Git workshops: a **real Git console in a sandbox**, a\n**task panel** that validates the entered commands step by step, and a second window with a\n**live commit graph** (including orphaned/disconnected commits). Runs **offline** (air-gapped) and\n**cross-platform** (Linux / Windows / macOS).\n\n\u003e Built when the workshop grew from ~8 to 76 simultaneous participants and one-on-one help during\n\u003e the hands-on part was no longer possible: this app takes over the trainer's role for the exercise\n\u003e part.\n\n\u003e **Maintaining the code?** See [`CLAUDE.md`](./CLAUDE.md) for the architecture, the design\n\u003e rationale (the *why*), the annotated source tree, and the project conventions.\n\n---\n\n## 📑 Contents\n\n1. [What it does](#-what-it-does)\n2. [Requirements](#-requirements)\n3. [Quickstart](#-quickstart)\n4. [Build, test, run](#-build-test-run)\n5. [Dependencies](#-dependencies)\n6. [Writing exercises (file format)](#-writing-exercises-file-format)\n7. [Deployment (packaging)](#-deployment-packaging)\n8. [Architecture \u0026 project layout](#-architecture--project-layout)\n\n---\n\n## ✨ What it does\n\n- **Real console** in an isolated sandbox (a true PTY, real tools such as `vim`, `nano`, `cat`,\n  `ls`, `grep` operating on the `.git` folder). With a **context menu** (right-click:\n  Copy/Paste/Select all) and the shortcuts **Ctrl+Shift+C / Ctrl+Shift+V**.\n- **Exercises** as a sequence of steps with declarative goals.\n- **Validation**: after every change the repo state is read and checked against the current step's\n  goals; a checklist shows satisfied/open sub-goals.\n- **Trainer substitute**: an escalating ladder of on-demand hints, affirmatively detected \"pitfalls\"\n  (e.g. detached HEAD, merge in the wrong direction), and automatic help when the learner is stuck.\n- **Concept framing**: each exercise can show an `intro` (the *why/what* before doing) above the\n  goals and a `debrief` (consolidation) on completion.\n- **Commit graph** in a second window: nodes = commits, edge child→parent, including orphaned\n  commits and disconnected components, animated (d3). **Zoom/pan with Ctrl+wheel**, **exportable as\n  PNG** via the menu. **Hovering a node shows its SHA** (first 7 chars highlighted, rest in full),\n  **clicking opens a popup** with author, committer, both dates (original time zone) and the full\n  commit message, and **right-clicking offers copy actions** (SHA, message, author, committer, all).\n- **Export console output** (File menu): plain-text transcript of the current attempt as `.txt`\n  (ANSI sequences stripped).\n- **Per-participant progress** (persisted in `~/.git-workshop/`): an overview with status\n  (✓ completed, ⏸ paused, ℹ new, ☐ not started). Exercises can be **paused and resumed exactly**\n  (sandbox snapshot); completed ones restart via 🔄.\n- **Code-gated exercises** (optional): an exercise can be **locked** until the learner enters a code\n  the trainer announces — controls the pace. Only a salted hash is stored, so codes can't be looked\n  up in the (public) repo. See [Writing exercises](#writing-exercises-file-format).\n- **Exercise search** in the overview: live filter over chapter + title, umlaut-tolerant (e.g.\n  \"uberfuhren\" matches \"überführen\"). Appears once there is more than one exercise.\n- **Per-exercise time budget** (optional): a countdown in the status bar, red blinking in the final\n  minute, metronome ticks and a beep at expiry.\n\n---\n\n## 📋 Requirements\n\n- **Node.js ≥ 22.12** and npm (required by Electron 42; pinned in `engines`).\n- A **C/C++ toolchain** for `node-pty` (built natively on install):\n  - Linux: `build-essential`, `python3`.\n  - macOS: Xcode Command Line Tools.\n  - Windows: Visual Studio Build Tools (Desktop C++).\n- **Git** must be available at runtime. For **air-gapped deployment** a **bundled toolchain** is\n  shipped (on Windows *MinGit* is not enough — it lacks `bash`, coreutils and `vim`; bundle **Git\n  for Windows Portable** instead), wired in via the env vars `TOOLCHAIN_BIN` / `GIT_BIN` (see\n  `src/electron/main/composition.ts`).\n\n---\n\n## 🚀 Quickstart\n\n```bash\ngit clone \u003crepo\u003e \u0026\u0026 cd git-workshop\nnpm install          # pulls deps; postinstall builds the exercise bundles + icon\nnpm test             # unit tests of the pure core (no Electron needed)\nnpm run dev          # launch the app (needs a desktop environment)\n```\n\n\u003e **Troubleshooting:** if `npm run dev`/`start` reports `Electron uninstall`, the Electron binary\n\u003e wasn't downloaded during install — run `node node_modules/electron/install.js` (or\n\u003e `npm rebuild electron`) to fetch it.\n\n---\n\n## 🔧 Build, test, run\n\n| Command                        | Effect                                                                                                                                                    |\n|--------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------|\n| `npm test`                     | **Unit tests** (Vitest) of the pure core — runs without Electron/Git.                                                                                     |\n| `npm run test:watch`           | Tests in watch mode.                                                                                                                                      |\n| `npm run typecheck`            | Type-check the **whole** project (incl. Electron; needs all deps).                                                                                        |\n| `npm run typecheck:core`       | Type-check **without** the Electron shell (`tsconfig.check.json`).                                                                                        |\n| `npm run schema:gen`           | Generate `exercises/**/exercise.schema.json` from the Zod schema.                                                                                         |\n| `npm run bundles:gen`          | Build the exercise bundles reproducibly (also runs as `postinstall`).                                                                                     |\n| `npm run icon:gen`             | Rasterize `build/icon.svg` → `build/icon.png` (for the `dist:*` builds).                                                                                  |\n| `npm run unlock:gen -- \"CODE\"` | Print the `unlock:` block (salted hash) for a gated exercise.                                                                                             |\n| `npm run dev`                  | Start the app via electron-vite (hot reload).                                                                                                             |\n| `npm run build`                | Build main/preload/renderer into `out/`.                                                                                                                  |\n| `npm run start` / `preview`    | Start the built artifacts.                                                                                                                                |\n| `npm run clean`                | Remove build outputs (`out/`, `dist/`) and tool caches (`node_modules/.vite`, `.vitest`). Generated input assets (`*.bundle`, `build/icon.png`) are kept. |\n| `npm run clean:deep`           | Like `clean`, also removes `node_modules/` → follow with `npm ci`.                                                                                        |\n| `npm run verify`               | Fresh run: `clean` → `typecheck` → `test` → `build`.                                                                                                      |\n\n\u003e **Note:** `npm test`, `npm run typecheck:core` and `npm run schema:gen` run in any environment\n\u003e (CI, container). `npm run dev/build/start` and `npm run typecheck` need a **desktop environment**\n\u003e and the installed native modules (Electron, node-pty). The pure core is decoupled from that and\n\u003e fully tested.\n\n---\n\n## 📦 Dependencies\n\n**Runtime (app):**\n\n| Package                            | Purpose                                                               |\n|------------------------------------|-----------------------------------------------------------------------|\n| `electron`                         | Desktop shell, own Chromium → identical rendering across all machines |\n| `node-pty`                         | real pseudo-terminal (native, compiled on install)                    |\n| `@xterm/xterm`, `@xterm/addon-fit` | terminal emulator in the renderer                                     |\n| `chokidar`                         | filesystem watcher on the sandbox (polling mode, see CLAUDE.md)       |\n| `d3`                               | animated commit graph                                                 |\n| `zod`                              | validation of the exercise file format + type inference               |\n| `js-yaml`                          | parse the YAML exercises                                              |\n\n**Build/dev:** `electron-vite`, `vite`, `typescript`, `vitest`, `tsx`, `zod-to-json-schema`,\n`@resvg/resvg-js` (icon rasterization), `electron-builder`, `@types/*`.\n\n---\n\n## 📝 Writing exercises (file format)\n\nAn exercise package is a folder under `exercises/\u003cslug\u003e/` (the folder name is a readable slug,\n**not** the id):\n\n```\nexercises/feature-branch-merge/\n  exercise.yaml          # the exercise (see template) – versioned\n  exercise.schema.json   # generated: npm run schema:gen – versioned\n  bundle.yaml            # declarative starting history of the repo – versioned\n  feature-branch-merge.bundle   # generated from bundle.yaml: npm run bundles:gen – NOT versioned (.gitignore)\n```\n\n`exercises/feature-branch-merge/exercise.yaml` is a complete, runnable template. Its first line\nbinds the JSON schema so VS Code (with the `redhat.vscode-yaml` extension) validates as you type.\n\n### Identity \u0026 order\n\nEvery exercise has a required `id` (**UUID**, the stable identity for\nprogress — survives renaming/retitling) and `chapter` (a dotted chapter number such as `\"1.0\"`,\n`\"1.1\"`, `\"2.0\"`, which is also the overview's sort key). The **folder name** stays a readable slug;\nthe repository resolves the UUID to the folder on load. Generate a UUID with\n`node -e \"console.log(crypto.randomUUID())\"`.\n\n### Concept text (`intro` / `debrief` / `task`)\n\nAll three run through the same **minimal, safe**\nrich-text formatter (no Markdown library → offline/CSP-safe; HTML is escaped). Supported: paragraphs\n(blank line), **bullet lists** (`- ` / `* `), `**bold**` and `` `code` ``. Within a paragraph single\nline breaks are **reflowed** (Markdown soft-wrap), so literal blocks (`task: |`) wrap to the panel\nwidth instead of at the YAML line ends. `intro` is shown (foldable) above the goals, `debrief` on\ncompletion; both are optional.\n\n### Available assertions\n\nDiscriminator `type`:\n`branchExists`, `headOnBranch`, `headDetached`, `repoInitialized`, `headUnborn` (optional `branch`),\n`commitExists` (+ optional `reachableFrom`), `tipHasParents`, `branchAhead`, `commitCount`,\n`tagExists`, `danglingCommitExists`, `noDanglingCommits`, `fileStaged`, `fileInWorktree`,\n`gitConfig` (`key`, optional `value`), `gitCommandUsed` (`name` — a single string **or a list** =\nmatch on *one* of the commands, e.g. `[show, log]`; optional `argsContain`), `objectExists` (`kind`),\n`indexEntry` (`path`).\n\n`repoInitialized` = a repo exists (stays satisfied after commits); `headUnborn` = freshly\ninitialized, **no commit yet** (HEAD on an unborn branch) — becomes unsatisfied after the first\ncommit. For a \"`git init`\" goal `headUnborn` is precise, `repoInitialized` more robust.\n\n`commitExists` with `reachableFrom` is **fail-closed**: if the named ref does not exist (e.g.\n`origin/main` before the first push), nothing counts as reachable → no match. This lets you check\n\"the commit arrived in the remote/bare repo\" via the remote-tracking ref `origin/\u003cbranch\u003e` without\ninspecting the remote itself (it mirrors the remote's state after a successful push).\n\n### Checking git config (`gitConfig`)\n\nChecks the sandbox's effective config (global+local merged).\nWithout `value` the key only has to be set; with `value` it must match exactly; the `key` is\ncase-insensitive (`init.defaultBranch` == `init.defaultbranch`). Example:\n\n```yaml\ngoals:\n  - type: gitConfig\n    key: user.email\n    value: max@firma.de        # exact value required\n  - type: gitConfig\n    key: init.defaultBranch     # without value: must just be set\n```\n\n### Checking plumbing (B + A)\n\nThe default model is state-based and checks the *result*. For\nplumbing that is not enough: read-only commands (`cat-file -p`, `ls-files`, `rev-parse`) leave no\nstate, and the state cannot distinguish `update-index` from `git add`. Two opt-in mechanisms close\nthe gap:\n\n- **B — command observation (`gitCommandUsed`):** the sandbox shell exports\n  `GIT_TRACE2_EVENT=\"$HOME/trace2.jsonl\"` in its `.bashrc`; the `GitInspector` parses that JSONL\n  (`trace2.ts`) and knows *which* git subcommand ran with which arguments — including read-only\n  ones. From the `exit` events it reads the exit code; `gitCommandUsed` counts only **successful**\n  calls (code 0), so e.g. `cat-file -p \u003cinvalid-hash\u003e` does **not** satisfy the goal. Note: the\n  export lives **only** in the interactive shell (not in `buildSandboxEnv`), so the inspector calls\n  of each tick are **not** logged; the prompt probes (`__ws_git_branch`) run with\n  `GIT_TRACE2_EVENT=false`. The file lives under `$HOME` (= the sandbox dir, **outside** the repo\n  worktree) → invisible to worktree checks.\n- **A — effect checks (`objectExists`, `indexEntry`):** the snapshot is extended by the **object\n  database** (`objects`, from `cat-file --batch-all-objects`) and the **index** (`index`, from\n  `ls-files --stage`), so mutating plumbing commands are checkable via their result.\n\n```yaml\ngoals:\n  - type: gitCommandUsed      # B: the right command (with -w) ran\n    name: hash-object\n    argsContain: [\"-w\"]\n  - type: objectExists        # A: the effect – a blob is in the database\n    kind: blob\n```\n\n`gitCommandUsed` is monotonic (a command, once run, stays \"satisfied\"); when **resuming** from a\nsnapshot the command log starts empty (the trace file lives outside the repo and is not part of the\nsnapshot) — negligible for short plumbing exercises. Demo: `exercises/git-plumbing-blob/`\n(chapter 3.0): write a blob with `git hash-object -w`, read it back with `git cat-file -p`.\n\n\u003e Note: the sandbox presets `user.name`, `user.email` and `init.defaultBranch` (so `git commit`\n\u003e works without setup, see `renderGitConfig` in `sandbox/env.ts`). A config exercise therefore\n\u003e sensibly checks for a **concrete target value** (≠ the default) that the participant must actively\n\u003e set via `git config`.\n\n### Bundles are generated, not committed\n\nA bundle is a binary file; the committed starting history\nlives declaratively in the exercise's `bundle.yaml`. That's why the `.bundle` files are in\n`.gitignore`; `generate-bundles.ts` **scans** `exercises/*/bundle.yaml` and builds them reproducibly\n(deterministic OIDs via fixed author dates):\n\n```bash\nnpm run bundles:gen   # builds all exercises/\u003cid\u003e/\u003cid\u003e.bundle (also runs as postinstall)\n```\n\nA `bundle.yaml` reads like a sequence of git commands:\n\n```yaml\ndefaultBranch: main\nhead: main              # branch HEAD ends up on\nops:\n  - commit: Init Repo\n    files: { README.md: \"# Projekt\\n\" }\n  - branch: feature     # create a branch at the current HEAD (no switch)\n  - commit: Hinweise in main ergaenzen\n    files: { NOTES.md: \"Hinweise fuer main.\\n\" }\n  - switch: feature\n  - commit: Add feature\n    files: { feature.js: \"console.log('feature');\\n\" }\n  - switch: main\n```\n\nA **new exercise** just gets its own `bundle.yaml` in the exercise folder — the generator finds it\nautomatically. Transient starting state (uncommitted changes, detached HEAD, half-finished rebase)\ndoes **not** belong in the bundle but in the `setup:` field of the `exercise.yaml`.\n\n### Exercise with no repo at all\n\nFor example a `git init` exercise: set `provisioning: {}` (no `bundle`) in the\n`exercise.yaml` and create **no** `bundle.yaml`. The sandbox then starts as an empty directory; see\n`exercises/git-init-repo/`.\n\n### Unlocking exercises with a code (gating)\n\nOptionally an exercise can be **locked** until the\nlearner enters a code the trainer announces in the workshop — this controls the pace. The YAML\nstores **only a salted SHA-256 hash** of the code, never the plaintext (the repo is public → a\nplaintext code could be looked up there). Generate the block:\n\n```bash\nnpm run unlock:gen -- \"MY-CODE\"   # prints the unlock: block with a fresh salt\n```\n\nPaste the result into the `exercise.yaml` (you keep the plaintext code and announce it):\n\n```yaml\nunlock:\n  salt: \"0d8b0f605a2207d6\"\n  hash: \"53e798fac3da7a9483127a5e24aac1e245a6ed15c8ac337110592ba338c4d7e4\"\n```\n\nLocked exercises appear in the overview with 🔒 (not startable); an \"unlock\" field takes the code.\nCodes are compared **trimmed and case-insensitively**. Several exercises sharing the same code can\nbe unlocked with **one** entry (e.g. per chapter). The unlock state lives in progress\n(`progress.json`); \"reset progress\" locks them again. Validation happens in main against the hash —\nthe code/hash never reaches the renderer.\n\n### Trainer override\n\nIn `~/.git-workshop/settings.json`, `{\"unlockAll\": true}` opens every exercise\non your own machine (no code needed) — handy for demoing/preparing.\n\n\u003e Note: gating controls the **pace**, not the answers — the solutions are in the `hints` anyway.\n\u003e Trivial codes would be brute-forceable against the hash → choose **non-trivial** ones.\n\n---\n\n## 🚢 Deployment (packaging)\n\nDistributable artifacts are produced with **electron-builder** (configured in the `build` field of\n`package.json`). `npm run build` alone only produces the bundled code in `out/` — not an installable\nprogram.\n\n```bash\nnpm install                 # once; pulls electron-builder + builds node-pty natively\nnpm run dist:linux          # ON Linux: AppImage + tar.gz in dist/\nnpm run dist:win            # ON Windows: NSIS installer + portable .exe in dist/\n```\n\n### Cross-platform?\n\nYes (Linux + Windows + macOS), with two caveats:\n\n1. **Native modules ⇒ build per target OS.** `node-pty` cannot be cross-compiled reliably. Build the\n   Windows package on Windows, the Linux package on Linux (or via a CI matrix). electron-builder\n   rebuilds native modules against the Electron ABI; `node-pty` is excluded from the asar via\n   `asarUnpack` (native `.node` can't load from an asar).\n2. **Runtime toolchain** (`git`, `bash`, coreutils, `vim`):\n   - **Linux:** uses the system PATH (existing git/bash). For air-gapped machines without these\n     tools, bundle them yourself.\n   - **Windows:** no bash/git out of the box → **Git for Windows _Portable_** is fetched\n     **automatically**: `dist:win` first runs `npm run vendor:win` (`scripts/fetch-git-portable.ts`),\n     downloads PortableGit (needs internet + **7-Zip** on the build machine) and extracts it to\n     `vendor/git-portable/`. The build packs that as `extraResources` into `resources/toolchain/`;\n     the app finds it automatically at runtime (`resolveToolchain()` in `main.ts`). Existing content\n     is skipped (`FORCE=1` forces a re-fetch); version/source/checksum via\n     `GIT_FOR_WINDOWS_VERSION`/`_TAG`/`_URL`/`_SHA256`. Offline you can also fill the folder manually\n     (see `vendor/git-portable/README.md`). Runtime override anytime via `TOOLCHAIN_BIN`/`GIT_BIN`.\n\nThe exercise packages (`exercises/`) are packed as `extraResources` into `resources/exercises/`,\nmatching the production path resolution (`appRoot = dirname(app.getAppPath())`).\n\n### App icon\n\nThe source is `build/icon.svg` (versioned). `npm run icon:gen` rasterizes it to\n`build/icon.png` (1024×, via `@resvg/resvg-js`, offline-capable); the `dist:*` scripts **and**\n`postinstall` call it automatically beforehand (so the PNG exists even after a fresh clone for the\ndev run). electron-builder derives the platform formats (`.ico`/`.icns`) from it. The PNG is\ngenerated and **not** versioned (`.gitignore`) — like the bundles. To change the final logo, just\nreplace `build/icon.svg` and regenerate.\n\nIn addition **both windows** set the icon explicitly (`BrowserWindow({ icon })`), otherwise the\nwindow/taskbar entry on Linux/KDE shows the generic toolkit default. The path is `build/icon.png` in\nthe dev run and `resources/icon.png` in the package (copied via `build.extraResources`); if the file\nis missing, the icon is silently left unset (no crash). On **Windows** the packaged app's\ntaskbar/Explorer icon primarily comes from the **`.ico` embedded in the .exe** (electron-builder\nderives it from `build.icon`); main also sets `app.setAppUserModelId('de.workshop.git-workshop')` at\nruntime (must match `build.appId`) so taskbar grouping, pinning and notifications are attributed to\nthe app identity.\n\n---\n\n## 🧱 Architecture \u0026 project layout\n\nThe codebase follows **Clean Architecture** with the **dependency rule pointing inward**: `core`\n(pure: domain, validation, graph layout, ports) ← `application` (use cases) ← `infrastructure`\n(Git/Node adapters) ← `electron` (the shell and composition root, the only place that wires concrete\nclasses). The pure core is fully testable without Electron/Git (see `test/`).\n\n```\nsrc/\n  core/            # PURE. No IO/Node/Electron. domain, validation, graph layout, ports.\n  application/     # use cases + the runtime carrier (SessionContext)\n  infrastructure/  # adapters: git (runner/inspector/parsers), sandbox, exercise repo, progress store\n  electron/        # desktop shell: main (windows/menu/IPC), preload, renderer-console, renderer-graph\nexercises/         # exercise packages (exercise.yaml + bundle.yaml + generated schema/.bundle)\nscripts/           # generate-schema, generate-bundles, generate-icon, gen-unlock, clean, fetch-git-portable\ntest/              # Vitest mirror of the core\n```\n\nThe full annotated source tree, the design decisions (the *why* behind each choice), the project\nconventions, and an extension guide live in **[`CLAUDE.md`](./CLAUDE.md)**.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fshinji-san%2Fgit-workshop","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fshinji-san%2Fgit-workshop","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fshinji-san%2Fgit-workshop/lists"}