diff --git a/.agents/skills/update-design/SKILL.md b/.agents/skills/update-design/SKILL.md index a264716..165b12f 100644 --- a/.agents/skills/update-design/SKILL.md +++ b/.agents/skills/update-design/SKILL.md @@ -15,7 +15,7 @@ Read only what is relevant: 2. `docs/development-memo.md` when present 3. the source directories touched by the design 4. build manifests and CI workflows that constrain implementation -5. `AGENT.md` +5. `AGENTS.md` If design docs do not exist, report that first and switch to proposing a minimal design outline derived from the current memo and README. diff --git a/.agents/skills/update-docs/SKILL.md b/.agents/skills/update-docs/SKILL.md index a40b4db..ab6d700 100644 --- a/.agents/skills/update-docs/SKILL.md +++ b/.agents/skills/update-docs/SKILL.md @@ -14,7 +14,7 @@ Read the smallest set of files that define behavior: 1. entry points and changed source files 2. build and dependency manifests 3. CI workflows -4. `README.md`, `docs/`, `AGENT.md` +4. `README.md`, `docs/`, `AGENTS.md` 5. `docs/development-memo.md` when present Do not update docs from memory. Derive every command and path from the repository. @@ -25,7 +25,7 @@ Use this split: - `README.md`: how to install, run, configure, and use the project - `docs/`: design rationale, ADRs, requirements, internal notes -- `AGENT.md`: repo-specific instructions for Codex +- `AGENTS.md`: repo-specific instructions for Codex For GitReal specifically, keep `README.md` and `docs/development-memo.md` aligned on: diff --git a/.agents/skills/update-plan/SKILL.md b/.agents/skills/update-plan/SKILL.md index 7e23250..ead0bb7 100644 --- a/.agents/skills/update-plan/SKILL.md +++ b/.agents/skills/update-plan/SKILL.md @@ -12,7 +12,7 @@ Run this immediately before presenting a substantial implementation plan. Collect the minimum relevant context: 1. the current draft plan -2. `AGENT.md` +2. `AGENTS.md` 3. related design docs, ADRs, requirements, roadmap items, issue notes, and `docs/development-memo.md` 4. the source files and manifests the plan touches diff --git a/.gitignore b/.gitignore index e02649c..39e6eff 100644 --- a/.gitignore +++ b/.gitignore @@ -1,148 +1,28 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* - -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# Snowpack dependency directory (https://snowpack.dev/) -web_modules/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional stylelint cache -.stylelintcache +# Build artifacts +/git-real +*.exe +*.dll +*.so +*.dylib -# Optional REPL history -.node_repl_history +# Test / profiling +coverage.out +*.test +*.out +*.prof -# Output of 'npm pack' -*.tgz +# Cache +.cache/ -# Yarn Integrity file -.yarn-integrity +# Editor / OS +*.swp +*.swo +.vscode/ +.idea/ +.DS_Store +Thumbs.db -# dotenv environment variable files +# dotenv .env .env.* !.env.example - -# parcel-bundler cache (https://parceljs.org/) -.cache -.parcel-cache - -# Next.js build output -.next -out - -# Nuxt.js build / generate output -.nuxt -dist -.output - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and not Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# vuepress v2.x temp directory -.temp - -# Sveltekit cache directory -.svelte-kit/ - -# vitepress build output -**/.vitepress/dist - -# vitepress cache directory -**/.vitepress/cache - -# Docusaurus cache and generated files -.docusaurus - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# Firebase cache directory -.firebase/ - -# TernJS port file -.tern-port - -# Stores VSCode versions used for testing VSCode extensions -.vscode-test - -# pnpm -.pnpm-store - -# yarn v3 -.pnp.* -.yarn/* -!.yarn/patches -!.yarn/plugins -!.yarn/releases -!.yarn/sdks -!.yarn/versions - -# Vite files -vite.config.js.timestamp-* -vite.config.ts.timestamp-* -.vite/ - -# Go -.cache/ -coverage.out -/git-real diff --git a/AGENT.md b/AGENT.md deleted file mode 100644 index e7c4157..0000000 --- a/AGENT.md +++ /dev/null @@ -1,151 +0,0 @@ -# AGENT.md - -This repository uses a repo-local Codex guide and skills. - -## Scope - -This file defines the default working rules for Codex in this repository. -Repo-local skills live under `skills/`. - -Note: the reference repository uses `.agents/skills/`, but this workspace mounts hidden top-level agent directories read-only. Keep the same skill content here under `skills/`. - -## Project Overview - -GitReal is a Git subcommand distributed as a `git-real` executable and invoked by users as `git real`. - -The current product shape from `README.md` and `docs/development-memo.md` is: - -- 2-minute push challenge triggered by notification timing -- default dry-run behavior -- destructive mode enabled only by explicit `git real arm` -- backup and recovery through `refs/gitreal/backups/...` -- planned implementation language: Go - -Current MVP command set: - -```text -git real init -git real status -git real once -git real start -git real arm -git real disarm -git real rescue list -git real rescue restore -``` - -## Current Source Of Truth - -Until the Go implementation exists, treat these as the primary project documents: - -- `README.md` -- `docs/development-memo.md` -- `AGENT.md` - -Keep those three aligned. - -## Working Baseline - -1. Inspect the repository before making assumptions about language, framework, build system, or test runner. -2. Prefer the commands and conventions already present in the repo over introducing new tooling. -3. Keep changes small, explicit, and easy to validate. -4. Update related docs when behavior, commands, or project structure change. - -## Discovery Order - -Before making changes, check the files that define how the project works: - -- `README.md` -- `docs/development-memo.md` -- CI workflows under `.github/workflows/` -- language/build manifests such as `package.json`, `pyproject.toml`, `Cargo.toml`, `go.mod`, `build.zig`, `Makefile` -- source entry points and top-level docs under `docs/` - -If those files do not exist, say so explicitly and proceed with the smallest reasonable assumption. - -## Completion Requirements - -Do not consider work complete until you have run the narrowest relevant validation available in the repository. - -Examples: - -- existing test command -- existing formatter or linter -- existing typecheck or build command - -For this repository today: - -- if only docs changed, verify `README.md`, `docs/development-memo.md`, and `AGENT.md` stay consistent -- if Go scaffolding exists, prefer the repo-native commands first -- once `go.mod` and `cmd/git-real` exist, the expected baseline is `go test ./...` and `go build ./cmd/git-real` - -If the repository does not yet define runnable validation commands, report that clearly instead of inventing a fake completion signal. - -## Engineering Approach - -### TDD When Practical - -When the repo already has tests or a clear place for them: - -1. write or update a failing test -2. implement the minimum change -3. refactor without changing behavior - -### Tidy First - -Separate structural cleanup from behavioral changes where practical. - -- reduce nesting with guard clauses -- remove dead code when encountered -- extract helpers when they clarify intent -- normalize similar code paths -- keep comments short and only where code is not self-evident - -### Iteration Size - -Split work into the smallest meaningful increment and finish that increment completely before moving on. - -## Planned Architecture - -Use the current memo as the default implementation direction unless newer code or docs replace it: - -- `cmd/git-real/main.go` for CLI entry -- `internal/git/` for Git command wrappers -- `internal/challenge/` for timing and challenge flow -- `internal/notify/` for desktop notifications -- `internal/config/` for Git config access -- `internal/daemon/` for foreground scheduler and later background support - -Prefer invoking Git commands over reading `.git` internals directly. - -## Documentation Rules - -- `README.md`: user-facing usage, install, and operation -- `docs/`: design notes, ADRs, requirements, implementation details -- `AGENT.md`: repo-specific instructions for Codex - -When a command, workflow, or architecture decision changes, update the relevant document in the same task when possible. - -For this repo in particular: - -- user-facing command behavior belongs in `README.md` -- command rationale and architecture notes belong in `docs/development-memo.md` -- Codex workflow rules belong in `AGENT.md` - -## Planning Rules - -When asked for a plan: - -1. ground the plan in the current repo state -2. identify missing design or requirement inputs -3. keep steps independently verifiable -4. note risks, dependencies, and affected files - -Run the `update-plan` skill before finalizing a substantial plan. - -## Available Repo Skills - -- `skills/update-design/` -- `skills/update-docs/` -- `skills/update-plan/` -- `skills/grill-me/` diff --git a/AGENTS.md b/AGENTS.md index a269174..89680fe 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,3 +1,151 @@ # AGENTS.md -See [AGENT.md](./AGENT.md) for the canonical Codex instructions for this repository. +This repository uses a repo-local Codex guide and skills. + +## Scope + +This file defines the default working rules for Codex in this repository. +Repo-local skills live under `skills/`. + +Note: the reference repository uses `.agents/skills/`, but this workspace mounts hidden top-level agent directories read-only. Keep the same skill content here under `skills/`. + +## Project Overview + +GitReal is a Git subcommand distributed as a `git-real` executable and invoked by users as `git real`. + +The current product shape from `README.md` and `docs/development-memo.md` is: + +- 2-minute push challenge triggered by notification timing +- default dry-run behavior +- destructive mode enabled only by explicit `git real arm` +- backup and recovery through `refs/gitreal/backups/...` +- planned implementation language: Go + +Current MVP command set: + +```text +git real init +git real status +git real once +git real start +git real arm +git real disarm +git real rescue list +git real rescue restore +``` + +## Current Source Of Truth + +Until the Go implementation exists, treat these as the primary project documents: + +- `README.md` +- `docs/development-memo.md` +- `AGENTS.md` + +Keep those three aligned. + +## Working Baseline + +1. Inspect the repository before making assumptions about language, framework, build system, or test runner. +2. Prefer the commands and conventions already present in the repo over introducing new tooling. +3. Keep changes small, explicit, and easy to validate. +4. Update related docs when behavior, commands, or project structure change. + +## Discovery Order + +Before making changes, check the files that define how the project works: + +- `README.md` +- `docs/development-memo.md` +- CI workflows under `.github/workflows/` +- language/build manifests such as `package.json`, `pyproject.toml`, `Cargo.toml`, `go.mod`, `build.zig`, `Makefile` +- source entry points and top-level docs under `docs/` + +If those files do not exist, say so explicitly and proceed with the smallest reasonable assumption. + +## Completion Requirements + +Do not consider work complete until you have run the narrowest relevant validation available in the repository. + +Examples: + +- existing test command +- existing formatter or linter +- existing typecheck or build command + +For this repository today: + +- if only docs changed, verify `README.md`, `docs/development-memo.md`, and `AGENTS.md` stay consistent +- if Go scaffolding exists, prefer the repo-native commands first +- once `go.mod` and `cmd/git-real` exist, the expected baseline is `go test ./...` and `go build ./cmd/git-real` + +If the repository does not yet define runnable validation commands, report that clearly instead of inventing a fake completion signal. + +## Engineering Approach + +### TDD When Practical + +When the repo already has tests or a clear place for them: + +1. write or update a failing test +2. implement the minimum change +3. refactor without changing behavior + +### Tidy First + +Separate structural cleanup from behavioral changes where practical. + +- reduce nesting with guard clauses +- remove dead code when encountered +- extract helpers when they clarify intent +- normalize similar code paths +- keep comments short and only where code is not self-evident + +### Iteration Size + +Split work into the smallest meaningful increment and finish that increment completely before moving on. + +## Planned Architecture + +Use the current memo as the default implementation direction unless newer code or docs replace it: + +- `cmd/git-real/main.go` for CLI entry +- `internal/git/` for Git command wrappers +- `internal/challenge/` for timing and challenge flow +- `internal/notify/` for desktop notifications +- `internal/config/` for Git config access +- `internal/daemon/` for foreground scheduler and later background support + +Prefer invoking Git commands over reading `.git` internals directly. + +## Documentation Rules + +- `README.md`: user-facing usage, install, and operation +- `docs/`: design notes, ADRs, requirements, implementation details +- `AGENTS.md`: repo-specific instructions for Codex + +When a command, workflow, or architecture decision changes, update the relevant document in the same task when possible. + +For this repo in particular: + +- user-facing command behavior belongs in `README.md` +- command rationale and architecture notes belong in `docs/development-memo.md` +- Codex workflow rules belong in `AGENTS.md` + +## Planning Rules + +When asked for a plan: + +1. ground the plan in the current repo state +2. identify missing design or requirement inputs +3. keep steps independently verifiable +4. note risks, dependencies, and affected files + +Run the `update-plan` skill before finalizing a substantial plan. + +## Available Repo Skills + +- `skills/update-design/` +- `skills/update-docs/` +- `skills/update-plan/` +- `skills/grill-me/` diff --git a/Makefile b/Makefile index 603d820..929546b 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,7 @@ GO ?= go COVERAGE_THRESHOLD ?= 95 GOFMT_TARGETS := cmd internal +MAIN_PKG := ./cmd/git-real CACHE_DIR ?= $(CURDIR)/.cache export GOCACHE ?= $(CACHE_DIR)/go-build @@ -22,7 +23,7 @@ LDFLAGS := -s -w \ .PHONY: build fmt fmt-check lint typecheck deadcode test test-race coverage vuln actionlint check build: - $(GO) build -trimpath -buildvcs=true -ldflags='$(LDFLAGS)' -o git-real ./cmd/git-real + $(GO) build -trimpath -buildvcs=true -ldflags='$(LDFLAGS)' -o git-real $(MAIN_PKG) fmt: $(GO) fmt ./... @@ -46,7 +47,7 @@ typecheck: $(GO) test -run '^$$' ./... deadcode: - $(GO) tool deadcode ./cmd/git-real + $(GO) tool deadcode $(MAIN_PKG) test: $(GO) test ./... diff --git a/README.md b/README.md index 376d812..f12650d 100644 --- a/README.md +++ b/README.md @@ -202,4 +202,4 @@ git real --version ## More Detail -Design notes and implementation rationale live in [docs/development-memo.md](/workspaces/gitreal/docs/development-memo.md). +Design notes and implementation rationale live in [docs/development-memo.md](docs/development-memo.md). diff --git a/docs/development-memo.md b/docs/development-memo.md index f53e9e6..de1bba7 100644 --- a/docs/development-memo.md +++ b/docs/development-memo.md @@ -1,7 +1,5 @@ # GitReal Development Memo -最終更新: 2026-05-01 - ## 結論 - 実行ファイル名は `git-real` にする。 @@ -49,7 +47,7 @@ git-real/ README.md ``` -現状の実装は `cmd/git-real/main.go` から `internal/cli` を呼び、Git 実行は `internal/git`、通知は `internal/notify` に分離している。`config` と `daemon` は将来拡張用の想定で、MVP ではまだ導入していない。 +現状の実装は `cmd/git-real/main.go` から `internal/cli` を呼び、Git 実行は `internal/git`、通知は `internal/notify` に分離している。`config` と `daemon` は将来拡張用として未実装。 ## ユーザー体験 @@ -108,7 +106,7 @@ Hook だけで作るのは不向き。Git hooks は `commit`、`push`、`merge` - `git real arm`: `gitreal.armed=true` を書く - `git real rescue`: `refs/gitreal/backups/...` に退避した `HEAD` を一覧・復旧する -MVP では hook は使わない。後から追加するなら `post-commit` で「未 push commit ができたので GitReal 対象になった」と通知する程度に留める。 +hook は当面使わない。将来追加するとしても `post-commit` で「未 push commit ができたので GitReal 対象になった」と通知する程度に留める。 ## Git 状態判定 @@ -139,7 +137,7 @@ git stash pop `git real rescue restore ` も破壊的操作なので、restore 前に現在の `HEAD` を `refs/gitreal/backups/...` に退避する。worktree が dirty な場合は stash してから restore し、restore 後に stash pop を試みる。 -## MVP コマンド +## コマンド一覧 ```text git real init @@ -161,551 +159,6 @@ git real rescue restore - Release には `SHA256SUMS` を含める - `git real daemon` と Homebrew tap は次フェーズの課題として残す -## 初期プロトタイプ - -初期の単一ファイル案は以下。現在の repository 実装はこの責務を `internal/cli`、`internal/git`、`internal/notify` に分割している。 - -```go -package main - -import ( - "errors" - "flag" - "fmt" - "math/rand" - "os" - "os/exec" - "path/filepath" - "runtime" - "strconv" - "strings" - "time" -) - -type GitRealError struct { - Message string -} - -func (e GitRealError) Error() string { - return e.Message -} - -type Git struct { - Repo string -} - -func main() { - exitCode := run(os.Args) - os.Exit(exitCode) -} - -func run(args []string) int { - if len(args) < 2 { - printHelp() - return 0 - } - - command := args[1] - - switch command { - case "init": - return commandInit(args[2:]) - case "status": - return commandStatus(args[2:]) - case "once": - return commandOnce(args[2:]) - case "start": - return commandStart(args[2:]) - case "arm": - return commandArm(args[2:]) - case "disarm": - return commandDisarm(args[2:]) - case "rescue": - return commandRescue(args[2:]) - case "help", "-h", "--help": - printHelp() - return 0 - default: - fmt.Fprintf(os.Stderr, "git-real: unknown command: %s\n", command) - printHelp() - return 2 - } -} - -func printHelp() { - fmt.Println(`git-real - BeReal-inspired punishment CLI for Git - -Usage: - git real init - git real status - git real once [--grace-seconds=120] - git real start [--grace-seconds=120] - git real arm - git real disarm - git real rescue list - git real rescue restore - -Git invokes this binary as "git real" when the executable is named "git-real" and is on PATH.`) -} - -func commandInit(args []string) int { - repo, err := discoverRepo(".") - if err != nil { - return fail(err) - } - - g := Git{Repo: repo} - - if err := g.Run("config", "--local", "gitreal.enabled", "true"); err != nil { - return fail(err) - } - if err := g.Run("config", "--local", "gitreal.armed", "false"); err != nil { - return fail(err) - } - if err := g.Run("config", "--local", "gitreal.graceSeconds", "120"); err != nil { - return fail(err) - } - - fmt.Println("GitReal initialized for:") - fmt.Println(repo) - fmt.Println("Mode: dry-run") - fmt.Println("Run: git real once") - return 0 -} - -func commandStatus(args []string) int { - repo, err := discoverRepo(".") - if err != nil { - return fail(err) - } - - g := Git{Repo: repo} - - branch, err := g.CurrentBranch() - if err != nil { - return fail(err) - } - - upstream, err := g.Upstream() - if err != nil { - return fail(err) - } - - _ = g.FetchQuiet() - - ahead, err := g.AheadCount() - if err != nil { - return fail(err) - } - - armed := g.ConfigBool("gitreal.armed", false) - - fmt.Printf("repo: %s\n", repo) - fmt.Printf("branch: %s\n", branch) - fmt.Printf("upstream: %s\n", upstream) - fmt.Printf("ahead: %d\n", ahead) - fmt.Printf("armed: %t\n", armed) - - return 0 -} - -func commandOnce(args []string) int { - fs := flag.NewFlagSet("once", flag.ContinueOnError) - graceSeconds := fs.Int("grace-seconds", 120, "seconds before punishment") - if err := fs.Parse(args); err != nil { - return 2 - } - - repo, err := discoverRepo(".") - if err != nil { - return fail(err) - } - - g := Git{Repo: repo} - armed := g.ConfigBool("gitreal.armed", false) - - if err := runChallenge(g, *graceSeconds, armed); err != nil { - return fail(err) - } - - return 0 -} - -func commandStart(args []string) int { - fs := flag.NewFlagSet("start", flag.ContinueOnError) - graceSeconds := fs.Int("grace-seconds", 120, "seconds before punishment") - if err := fs.Parse(args); err != nil { - return 2 - } - - repo, err := discoverRepo(".") - if err != nil { - return fail(err) - } - - g := Git{Repo: repo} - armed := g.ConfigBool("gitreal.armed", false) - - rng := rand.New(rand.NewSource(time.Now().UnixNano())) - next := nextRandomSlot(time.Now(), rng) - - fmt.Printf("GitReal started for %s\n", repo) - fmt.Printf("next challenge: %s\n", next.Format(time.RFC3339)) - - for { - sleepUntil(next) - - if err := runChallenge(g, *graceSeconds, armed); err != nil { - fmt.Fprintf(os.Stderr, "git-real: %s\n", err.Error()) - } - - next = nextRandomSlot(time.Now().Add(time.Hour), rng) - fmt.Printf("next challenge: %s\n", next.Format(time.RFC3339)) - } -} - -func commandArm(args []string) int { - repo, err := discoverRepo(".") - if err != nil { - return fail(err) - } - - g := Git{Repo: repo} - - if err := g.Run("config", "--local", "gitreal.armed", "true"); err != nil { - return fail(err) - } - - fmt.Println("GitReal is now armed for this repository.") - return 0 -} - -func commandDisarm(args []string) int { - repo, err := discoverRepo(".") - if err != nil { - return fail(err) - } - - g := Git{Repo: repo} - - if err := g.Run("config", "--local", "gitreal.armed", "false"); err != nil { - return fail(err) - } - - fmt.Println("GitReal is now in dry-run mode for this repository.") - return 0 -} - -func commandRescue(args []string) int { - if len(args) < 1 { - fmt.Fprintln(os.Stderr, "usage: git real rescue list | git real rescue restore ") - return 2 - } - - repo, err := discoverRepo(".") - if err != nil { - return fail(err) - } - - g := Git{Repo: repo} - - switch args[0] { - case "list": - out, err := g.Output("for-each-ref", "refs/gitreal/backups", "--format=%(refname)") - if err != nil { - return fail(err) - } - text := strings.TrimSpace(out) - if text == "" { - fmt.Println("No GitReal backup refs found.") - return 0 - } - fmt.Println(text) - return 0 - - case "restore": - if len(args) != 2 { - fmt.Fprintln(os.Stderr, "usage: git real rescue restore ") - return 2 - } - backupRef := args[1] - if !strings.HasPrefix(backupRef, "refs/gitreal/backups/") { - fmt.Fprintln(os.Stderr, "ref must start with refs/gitreal/backups/") - return 2 - } - if err := g.Run("reset", "--hard", backupRef); err != nil { - return fail(err) - } - fmt.Printf("Restored: %s\n", backupRef) - return 0 - - default: - fmt.Fprintf(os.Stderr, "unknown rescue command: %s\n", args[0]) - return 2 - } -} - -func runChallenge(g Git, graceSeconds int, armed bool) error { - branch, err := g.CurrentBranch() - if err != nil { - return err - } - - upstream, err := g.Upstream() - if err != nil { - return err - } - - _ = g.FetchQuiet() - - ahead, err := g.AheadCount() - if err != nil { - return err - } - - deadline := time.Now().Add(time.Duration(graceSeconds) * time.Second) - - message := fmt.Sprintf("%s has %d unpushed commit(s). Push before %s.", branch, ahead, deadline.Format("15:04:05")) - notify("GitReal", message) - - fmt.Printf("repo: %s\n", g.Repo) - fmt.Printf("branch: %s\n", branch) - fmt.Printf("upstream: %s\n", upstream) - fmt.Printf("ahead: %d\n", ahead) - fmt.Printf("deadline: %s\n", deadline.Format(time.RFC3339)) - - sleepUntil(deadline) - - if err := g.FetchQuiet(); err != nil { - notify("GitReal", "fetch failed; punishment skipped for safety.") - return nil - } - - aheadAfter, err := g.AheadCount() - if err != nil { - return err - } - - if aheadAfter == 0 { - notify("GitReal", "Push confirmed. You are GitReal.") - return nil - } - - if !armed { - notify("GitReal dry-run", fmt.Sprintf("%d commit(s) would be reset.", aheadAfter)) - fmt.Printf("dry-run: would reset %d commit(s) to @{u}\n", aheadAfter) - return nil - } - - backupRef, err := g.BackupHead(branch) - if err != nil { - return err - } - - stashed, err := g.StashDirtyWorktree(backupRef) - if err != nil { - return err - } - - if err := g.Run("reset", "--hard", "@{u}"); err != nil { - return err - } - - if stashed { - if err := g.Run("stash", "pop"); err != nil { - fmt.Println("stash pop failed; your stash remains available via git stash list") - } - } - - notify("GitReal", fmt.Sprintf("Local commits made unreal. Backup: %s", backupRef)) - fmt.Printf("backup ref: %s\n", backupRef) - fmt.Printf("restore: git real rescue restore %s\n", backupRef) - - return nil -} - -func discoverRepo(path string) (string, error) { - cmd := exec.Command("git", "-C", path, "rev-parse", "--show-toplevel") - out, err := cmd.CombinedOutput() - if err != nil { - return "", GitRealError{Message: "not inside a Git repository"} - } - return strings.TrimSpace(string(out)), nil -} - -func (g Git) Run(args ...string) error { - _, err := g.Output(args...) - return err -} - -func (g Git) Output(args ...string) (string, error) { - fullArgs := append([]string{"-C", g.Repo}, args...) - cmd := exec.Command("git", fullArgs...) - out, err := cmd.CombinedOutput() - if err != nil { - return "", GitRealError{ - Message: fmt.Sprintf("git %s failed: %s", strings.Join(args, " "), strings.TrimSpace(string(out))), - } - } - return string(out), nil -} - -func (g Git) CurrentBranch() (string, error) { - out, err := g.Output("symbolic-ref", "--quiet", "--short", "HEAD") - if err != nil { - return "", GitRealError{Message: "detached HEAD is not supported"} - } - return strings.TrimSpace(out), nil -} - -func (g Git) Upstream() (string, error) { - out, err := g.Output("rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}") - if err != nil { - return "", GitRealError{Message: "no upstream configured; run: git push -u origin HEAD"} - } - return strings.TrimSpace(out), nil -} - -func (g Git) FetchQuiet() error { - return g.Run("fetch", "--quiet", "--prune") -} - -func (g Git) AheadCount() (int, error) { - out, err := g.Output("rev-list", "--count", "@{u}..HEAD") - if err != nil { - return 0, err - } - value := strings.TrimSpace(out) - if value == "" { - return 0, nil - } - count, err := strconv.Atoi(value) - if err != nil { - return 0, err - } - return count, nil -} - -func (g Git) BackupHead(branch string) (string, error) { - safeBranch := strings.ReplaceAll(branch, string(filepath.Separator), "-") - safeBranch = strings.ReplaceAll(safeBranch, "/", "-") - - timestamp := time.Now().UTC().Format("20060102T150405Z") - ref := fmt.Sprintf("refs/gitreal/backups/%s/%s", safeBranch, timestamp) - - if err := g.Run("update-ref", ref, "HEAD"); err != nil { - return "", err - } - - return ref, nil -} - -func (g Git) StashDirtyWorktree(backupRef string) (bool, error) { - out, err := g.Output("status", "--porcelain=v1", "-z") - if err != nil { - return false, err - } - - if out == "" { - return false, nil - } - - message := fmt.Sprintf("gitreal preserve worktree before penalty %s", backupRef) - - if err := g.Run("stash", "push", "--include-untracked", "--message", message); err != nil { - return false, err - } - - return true, nil -} - -func (g Git) ConfigBool(key string, fallback bool) bool { - out, err := g.Output("config", "--bool", "--get", key) - if err != nil { - return fallback - } - - value := strings.ToLower(strings.TrimSpace(out)) - - switch value { - case "true", "yes", "on", "1": - return true - case "false", "no", "off", "0": - return false - default: - return fallback - } -} - -func nextRandomSlot(base time.Time, rng *rand.Rand) time.Time { - hourStart := base.Truncate(time.Hour) - offset := time.Duration(rng.Intn(3600)) * time.Second - candidate := hourStart.Add(offset) - - if !candidate.After(time.Now()) { - candidate = hourStart.Add(time.Hour).Add(time.Duration(rng.Intn(3600)) * time.Second) - } - - return candidate -} - -func sleepUntil(target time.Time) { - for { - remaining := time.Until(target) - if remaining <= 0 { - return - } - if remaining > 30*time.Second { - time.Sleep(30 * time.Second) - } else { - time.Sleep(remaining) - } - } -} - -func notify(title string, message string) { - fmt.Printf("\a%s: %s\n", title, message) - - switch runtime.GOOS { - case "darwin": - escapedTitle := strings.ReplaceAll(title, `"`, `\"`) - escapedMessage := strings.ReplaceAll(message, `"`, `\"`) - script := fmt.Sprintf(`display notification "%s" with title "%s"`, escapedMessage, escapedTitle) - _ = exec.Command("osascript", "-e", script).Run() - - case "linux": - if commandExists("notify-send") { - _ = exec.Command("notify-send", title, message).Run() - } - - case "windows": - if commandExists("powershell") { - _ = exec.Command("powershell", "-NoProfile", "-Command", "[console]::beep(880,300)").Run() - } - } -} - -func commandExists(name string) bool { - _, err := exec.LookPath(name) - return err == nil -} - -func fail(err error) int { - if err == nil { - return 0 - } - - var gitRealError GitRealError - if errors.As(err, &gitRealError) { - fmt.Fprintf(os.Stderr, "git-real: %s\n", gitRealError.Message) - return 1 - } - - fmt.Fprintf(os.Stderr, "git-real: %s\n", err.Error()) - return 1 -} -``` - ## 初期ビルド ```bash diff --git a/internal/cli/app.go b/internal/cli/app.go index e6688aa..4ad9851 100644 --- a/internal/cli/app.go +++ b/internal/cli/app.go @@ -523,18 +523,24 @@ func nextRandomSlot(base time.Time, rng *rand.Rand) time.Time { return slot } +var commandUsages = []string{ + "git real init", + "git real status", + "git real once [--grace-seconds=120]", + "git real start [--grace-seconds=120]", + "git real arm", + "git real disarm", + "git real rescue list", + "git real rescue restore ", +} + func printHelp(w io.Writer) { - fmt.Fprintln(w, `git-real - BeReal-inspired punishment CLI for Git - -Usage: - git real init - git real status - git real once [--grace-seconds=120] - git real start [--grace-seconds=120] - git real arm - git real disarm - git real rescue list - git real rescue restore `) + fmt.Fprintln(w, "git-real - BeReal-inspired punishment CLI for Git") + fmt.Fprintln(w) + fmt.Fprintln(w, "Usage:") + for _, usage := range commandUsages { + fmt.Fprintf(w, " %s\n", usage) + } } func (a *app) fail(err error) int {