@cldmv/git-embedded 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +145 -0
  3. package/bin/git-embedded.mjs +185 -0
  4. package/docs/design.md +122 -0
  5. package/docs/use-case-private-tests.md +97 -0
  6. package/hooks/_dispatch.template +67 -0
  7. package/hooks/reference-transaction +54 -0
  8. package/hooks/update-embedded-repos +56 -0
  9. package/messages/setup-bare-githooks.md +25 -0
  10. package/messages/setup-dispatcher-canonical-complete.md +30 -0
  11. package/messages/setup-dispatcher-missing-symlinks.md +56 -0
  12. package/messages/setup-dispatcher-non-conforming.md +35 -0
  13. package/messages/setup-husky.md +35 -0
  14. package/messages/setup-init-templatedir.md +21 -0
  15. package/messages/setup-lefthook.md +48 -0
  16. package/messages/setup-none.md +27 -0
  17. package/messages/setup-pre-commit.md +47 -0
  18. package/messages/setup-simple-git-hooks.md +40 -0
  19. package/messages/setup-system-hookspath.md +25 -0
  20. package/package.json +96 -0
  21. package/src/api/cli/doctor.mjs +15 -0
  22. package/src/api/cli/init.mjs +26 -0
  23. package/src/api/cli/install-hooks.mjs +124 -0
  24. package/src/api/cli/install-template.mjs +43 -0
  25. package/src/api/cli/link.mjs +42 -0
  26. package/src/api/cli/print-hook-script.mjs +35 -0
  27. package/src/api/cli/uninstall-hooks.mjs +21 -0
  28. package/src/api/cli/version.mjs +17 -0
  29. package/src/api/commander/custom-help.mjs +249 -0
  30. package/src/api/detect/dispatcher.mjs +218 -0
  31. package/src/api/detect/husky.mjs +23 -0
  32. package/src/api/detect/lefthook.mjs +37 -0
  33. package/src/api/detect/pre-commit.mjs +35 -0
  34. package/src/api/detect/run.mjs +71 -0
  35. package/src/api/detect/simple-git-hooks.mjs +26 -0
  36. package/src/api/git.mjs +59 -0
  37. package/src/api/install/dispatcher.mjs +51 -0
  38. package/src/api/install/hooks.mjs +80 -0
  39. package/src/api/install/template.mjs +15 -0
  40. package/src/api/link/batch.mjs +130 -0
  41. package/src/api/link/copy-executable.mjs +27 -0
  42. package/src/api/link/elevate-windows.mjs +52 -0
  43. package/src/api/log.mjs +38 -0
  44. package/src/api/messages/load.mjs +27 -0
  45. package/src/api/paths.mjs +43 -0
  46. package/src/api/prompt.mjs +26 -0
  47. package/src/api/report.mjs +83 -0
  48. package/src/lib/elevate-windows-child.mjs +43 -0
@@ -0,0 +1,97 @@
1
+ # Use case: private tests for public OSS code
2
+
3
+ The motivating use case for `@cldmv/git-embedded` is open-source software that wants comprehensive test coverage running in CI but cannot afford to publish the full test suite. This document describes the threat model, the strategy, and the licensing considerations.
4
+
5
+ ## The threat model
6
+
7
+ Comprehensive tests are a high-fidelity behavioral specification of the code under test. A test that asserts "given input X, the function produces output Y" describes a fact about the API at a level of abstraction far more compact and reusable than the source code itself. With AI tools that can convert behavioral specifications into working implementations, comprehensive tests become a near-complete substitute for the source code in the hands of someone who wants to reimplement the software.
8
+
9
+ The classical clean-room reimplementation defense — used most famously by Phoenix in 1984 to clone the IBM PC BIOS — relies on a two-team structure. A "dirty team" reads the original and produces a functional specification that strips protectable expression and retains only ideas, methods, and observable behaviors. A "clean team" implements from the spec without ever having seen the original. The output is functionally equivalent but legally non-derivative because no protected expression was copied.
10
+
11
+ The defense is strong when the dirty team is genuinely good at abstraction. It is weak when the "spec" they hand off is mostly the original code transliterated into prose — that intermediate spec is itself a derivative work, and so is the downstream output. Humans trained as engineers tend to be careful at this; large language models tend not to be, paraphrasing more than abstracting.
12
+
13
+ Comprehensive tests bypass the dirty team entirely. They are already a behavioral specification — that's their entire purpose — and they were written to be implementation-independent. A test file is *not* a derivative work of the implementation in any meaningful sense; courts have held repeatedly that purely functional descriptions, API surfaces, and behavioral specifications are not protectable expression (Google v. Oracle 2021, Computer Associates v. Altai 1992, Sega v. Accolade 1992). The output produced by an AI given test files as input is not, on its face, substantially similar to the original code — especially if the reimplementation is in a different programming language.
14
+
15
+ The threat is therefore:
16
+
17
+ 1. **Tests describe behavior, not expression** — they are exactly the kind of artifact that strips protectable expression while retaining functional content.
18
+ 2. **Cross-language reimplementation collapses substantial-similarity arguments** — the output isn't even textually similar to the original.
19
+ 3. **No attribution falls out naturally** — there is no claim of copying, so there is nothing to attribute.
20
+ 4. **Scale** — one operator can reproduce a project in hours that took years to build.
21
+
22
+ For a project whose test suite is comprehensive (high line and branch coverage, configuration-matrix tests, edge cases worked out over time), publishing the tests publicly is materially equivalent to publishing the implementation behavioral spec. The current state of AI-copyright law (Doe v. GitHub, NYT v. OpenAI, Bartz v. Anthropic — see references at the end of this document) does not provide meaningful protection against this kind of laundering. Hiding the tests is the most direct mitigation.
23
+
24
+ ## The strategy
25
+
26
+ The strategy this package supports has four components.
27
+
28
+ **Code in a public repository under an open-source license.** Default choice is whatever fits the project — MIT, Apache-2.0, or AGPL-3.0 if the project wants the strongest copyleft compatible with OSI's Open Source Definition. The code is publicly visible, freely cloneable, and openly contributable.
29
+
30
+ **Tests in a separate private repository under a more restrictive license.** Typical choice is a custom license that explicitly forbids use as AI training data, fine-tuning input, prompt input, runtime context, or any other machine-learning-pipeline ingestion. This is what shifts the legal posture from "you had no rights and ignored none of mine" to "you had to actively breach a stated term." See the licensing strategy section below.
31
+
32
+ **Public CI runs whatever public test surface exists** — lint, build, type-check, smoke tests, integration tests intended for community contributors. Fork pull requests run only this surface, by GitHub Actions' default behavior (secrets are not exposed to fork-PR workflow runs, which is the correct security posture).
33
+
34
+ **Private CI runs the full suite after merge to integration branches.** Workflows triggered by pushes to internal branches (`next`, `hotfixes`, release branches) have access to the secrets needed to clone the private tests repo, and run the full suite. If the full suite fails on an integration branch, the merge is reverted — a recoverable outcome, and secrets never touch fork-PR code.
35
+
36
+ The `@cldmv/git-embedded` hooks are what make this strategy ergonomic for maintainers locally. Without them, the maintainer's local clone has to manually keep the tests directory in sync with the parent on every checkout, switch, and bisect. With them, the standard git workflow works as expected — `git checkout <release-branch>` updates both the code and the tests to the snapshot that was tested against, and `git bisect` walks both repos correctly in lockstep.
37
+
38
+ ## Why anonymous gitlinks specifically
39
+
40
+ Several alternatives were considered for tracking the pinned tests version:
41
+
42
+ - **A plain pin file** (`.tests-pin` committed in the parent containing the expected child SHA). Works, but the file's contents are publicly visible, and updating the pin requires a hook that rewrites the file before every commit. Standard git tooling (`git status`, `git bisect`) doesn't understand the file and won't help.
43
+ - **Git notes** on parent commits recording the child SHA. Most private — git notes are not displayed by GitHub or GitLab UIs, only via the CLI to someone who has explicitly fetched the notes ref. But notes require an extra push (`git push origin refs/notes/*`) on every change, and CI has to fetch and parse them.
44
+ - **A submodule with a real URL** in `.gitmodules`. Standard git automation works, but the child's URL is publicly advertised — the exact privacy leak the strategy is designed to avoid.
45
+ - **A submodule with a fake URL** in `.gitmodules`, overridden locally via `.git/config`. Git automation works because the registry entry exists; the URL leak is reduced because the public URL is a non-resolving decoy. But the decoy itself advertises "we are deliberately hiding the real URL," which signals more intent than no entry at all.
46
+ - **An anonymous gitlink** (gitlink in the tree with no `.gitmodules` entry). Most opaque — the public parent shows the gitlink and SHA but no URL, which could plausibly be a leftover, mistake, or any other innocuous explanation. Loses git's automation, which is the gap this package fills with its hooks.
47
+
48
+ The choice is partly aesthetic but mostly about how the parent repo presents itself to outside observers. The anonymous gitlink presents minimum signal: an embedded reference whose target is not declared, the way most other features in a repo work (you can see *that* something exists without being told everything about *what* it is).
49
+
50
+ ## Licensing strategy
51
+
52
+ The hooks themselves are infrastructure. The licensing question is what to do with the actual tests in the private repo, which is where the bulk of the engineering investment lives.
53
+
54
+ A few practical points:
55
+
56
+ **Different licenses for different parts of a single repo are valid.** Copyright attaches at the file level; nothing in copyright law requires one license per repository. The standard mechanism is the [REUSE specification](https://reuse.software/) from FSF Europe, which puts a license header at the top of every file and a `LICENSES/` directory containing the full text of each license used.
57
+
58
+ **OSI's Open Source Definition (clause 6) forbids use-based restrictions.** "No discrimination against fields of endeavor" means "no AI training" is not, by OSI's definition, an open-source restriction. A custom license that includes such a clause is **source-available** rather than open source, and the project should describe itself accordingly.
59
+
60
+ **AGPL-3.0 is the strongest OSI-compliant copyleft.** It does not mention AI training, but its broad copyleft language plausibly catches AI-augmented derivative works. AGPL-licensed code can be argued to require any model trained on it to be released under AGPL, an argument AI companies dispute and which is not yet tested in court. Combined with explicit non-consent records, it is the best the OSI-compliant landscape currently offers.
61
+
62
+ **OpenRAIL family licenses** (BigCode OpenRAIL-M for code, BigScience RAIL for models) include AI-misuse clauses and downstream propagation requirements. They are explicitly not OSI open source but are well-defined and widely understood in the AI ecosystem.
63
+
64
+ **EU CDSM Article 4** gives rightsholders a legally-enforceable opt-out from text-and-data-mining in EU jurisdictions. Combined with a machine-readable opt-out signal (TDMRep, robots.txt, `noai` meta tags), it provides a concrete legal hook absent from US copyright law's current state.
65
+
66
+ A reasonable layering for the private tests repo is therefore:
67
+
68
+ 1. AGPL-3.0 or a custom source-available license for the tests themselves
69
+ 2. Explicit AI-training-and-derivation prohibition clause in the license text
70
+ 3. Acceptable Use Policy on the project website documenting non-consent
71
+ 4. REUSE-spec headers on every file
72
+ 5. EU TDM opt-out signals on any hosted version
73
+
74
+ None of these provide certainty against the threat model, but they collectively shift the legal posture as far as currently available tools allow.
75
+
76
+ ## What this package does not provide
77
+
78
+ - **A solution to the broader AI-and-copyright problem.** The hooks make a specific defensive posture (hide the tests) more ergonomic. They do not stop anyone who has already obtained the tests from doing whatever they want with them. They do not protect the public code from AI training (which is a separate question with separate answers).
79
+ - **Watermarking, canary tokens, or AI-output traceability.** Some practitioners embed unique identifiers in tests to detect if those tests later appear in model outputs. That is a complementary defense; this package neither implements nor opposes it.
80
+ - **Legal advice.** This document describes a strategy and references public cases. It is not a substitute for consultation with a lawyer for any specific situation.
81
+
82
+ ## CI integration
83
+
84
+ The corresponding CI behavior — auto-detecting embedded gitlinks in workflow runs, cloning the corresponding private repos via the org bot App, running the full test suite — is provided by the CLDMV reusable workflows. See [`docs/conventions/embedded-tests-ci.md`](https://github.com/CLDMV/.github/blob/master/docs/conventions/embedded-tests-ci.md) in `CLDMV/.github` for the workflow design: the opt-in input (`enable_embedded_tests: true`), the two convention-only URL mappings (primary per-path `<repo>-<path-with-dashes>` and secondary consolidated `<repo>-embedded`), fork-PR silent-skip behavior, and the failure modes. The CLI in this package handles the local-machine side; the CLDMV workflows handle the CI side. Both work off the same anonymous-gitlink signal in the parent's tree. No tracked config file is involved on either side — keeping the mapping convention-only is what preserves the privacy intent of using anonymous gitlinks in the first place.
85
+
86
+ ## References
87
+
88
+ - **Phoenix Technologies IBM PC BIOS clone (1984)** — the canonical clean-room reimplementation. Never actually litigated; the technique deterred IBM from suing.
89
+ - **Computer Associates v. Altai, 982 F.2d 693 (2d Cir. 1992)** — established the abstraction-filtration-comparison test still used today for evaluating substantial similarity in software.
90
+ - **Sega v. Accolade, 977 F.2d 1510 (9th Cir. 1992)** — reverse engineering for interoperability is fair use.
91
+ - **Sony v. Connectix, 203 F.3d 596 (9th Cir. 2000)** — reverse engineering the PlayStation BIOS to build an emulator was legal.
92
+ - **Google LLC v. Oracle America, Inc., 593 U.S. 1 (2021)** — API declarations are subject to fair use even when reimplemented.
93
+ - **Doe v. GitHub** (N.D. Cal., filed November 2022) — anonymous developers v. GitHub/Microsoft/OpenAI over Copilot training on open-source code. Most copyright claims dismissed over 2023-2024 on substantial-similarity grounds.
94
+ - **The New York Times v. Microsoft and OpenAI** (S.D.N.Y., filed December 2023) — NYT sued over training on its content; NYT demonstrated verbatim regurgitation, breaking the Copilot-style "no substantial similarity" defense. Ongoing as of writing.
95
+ - **Bartz v. Anthropic** (N.D. Cal., filed August 2024) — authors v. Anthropic over training on pirated book datasets. Mid-2025 ruling split the question: training on legitimately purchased books was fair use; using pirated copies was infringement.
96
+ - **REUSE Specification** (FSF Europe) — `https://reuse.software/`
97
+ - **EU CDSM Directive Article 4** — text-and-data-mining opt-out mechanism.
@@ -0,0 +1,67 @@
1
+ #!/bin/sh
2
+ #
3
+ # Global git hook dispatcher (canonical, policy-free).
4
+ #
5
+ # Installed by `git embedded install-hooks` when no prior hook setup is
6
+ # detected. The installer:
7
+ # 1. Writes this script to the user's chosen hooks directory (default
8
+ # ~/.config/git/hooks/_dispatch).
9
+ # 2. Links every standard hook name (applypatch-msg, commit-msg,
10
+ # post-applypatch, post-checkout, post-commit, post-merge,
11
+ # post-rewrite, pre-applypatch, pre-auto-gc, pre-commit,
12
+ # pre-merge-commit, pre-push, pre-rebase, prepare-commit-msg,
13
+ # reference-transaction) to this script — symlink on Linux/macOS,
14
+ # hard link on Windows (with --no-symlinks), or copy as fallback.
15
+ # 3. Sets `git config --global core.hooksPath` to the chosen directory.
16
+ #
17
+ # Behavior at hook-fire time:
18
+ # 1. (Optional) apply user-added global policy below.
19
+ # 2. Chain to the per-repo `.git/hooks/<hook-name>` if one exists.
20
+ # This is what makes a global `core.hooksPath` coexist with per-repo
21
+ # hooks — without the chain, per-repo hooks would be silently
22
+ # shadowed.
23
+ #
24
+ # Customizing:
25
+ # Add your own policies in the `--- global policy ---` block below.
26
+ # `$hook` holds the hook name (e.g. `commit-msg`, `pre-push`).
27
+ # `$1`...`$N` and stdin are the hook's native arguments / input per
28
+ # the git hooks documentation.
29
+ # Exit non-zero from a case branch to reject the operation.
30
+ #
31
+ # DO NOT EDIT the chain-to-repo-hook block at the bottom unless you
32
+ # understand the consequences. It's load-bearing for `git-embedded`'s
33
+ # own hooks (post-checkout, post-merge, post-rewrite,
34
+ # reference-transaction) to reach the per-repo `.git/hooks/` directory
35
+ # where they're installed.
36
+
37
+ hook=$(basename "$0")
38
+
39
+ # --- global policy --------------------------------------------------------
40
+ # Add your hook-event-specific logic here. Example template:
41
+ #
42
+ # case "$hook" in
43
+ # commit-msg)
44
+ # # $1 is the path to the proposed commit message file.
45
+ # # if grep -qiE 'YOUR_PATTERN' "$1"; then
46
+ # # echo >&2 "✗ commit rejected: <reason>"
47
+ # # exit 1
48
+ # # fi
49
+ # ;;
50
+ # pre-push)
51
+ # # stdin has one line per ref:
52
+ # # <local_ref> <local_sha> <remote_ref> <remote_sha>
53
+ # # while IFS=' ' read -r local_ref local_sha remote_ref remote_sha; do
54
+ # # ...
55
+ # # done
56
+ # ;;
57
+ # esac
58
+
59
+ # --- chain to the repository's own hook, if any --------------------------
60
+ # (Load-bearing for git-embedded; do not remove.)
61
+ git_dir=$(git rev-parse --absolute-git-dir 2>/dev/null) || exit 0
62
+ repo_hook="$git_dir/hooks/$hook"
63
+ if [ -x "$repo_hook" ] && [ "$repo_hook" != "$0" ]; then
64
+ exec "$repo_hook" "$@"
65
+ fi
66
+
67
+ exit 0
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env bash
2
+ # reference-transaction
3
+ #
4
+ # Refuses HEAD-moving git operations if any embedded git repo in the parent's
5
+ # tree has uncommitted changes. Without this, a `git checkout B` in the parent
6
+ # would silently leave the child stale (with the post-checkout updater unable
7
+ # to update over dirty state), producing a confusing inconsistent state.
8
+ #
9
+ # Requires git 2.28+ (released July 2020, when reference-transaction was
10
+ # introduced).
11
+ #
12
+ # Phases (passed as $1 by git):
13
+ # - prepared: updates queued, not yet applied. Exiting non-zero ABORTS the
14
+ # transaction. This is the only phase we act on.
15
+ # - committed: updates already applied (informational; we ignore).
16
+ # - aborted: updates rejected by some other handler (informational).
17
+ #
18
+ # The hook reads proposed ref updates from stdin, one per line:
19
+ # <old_sha> <new_sha> <ref-name>
20
+ #
21
+ # We filter to HEAD updates with old != new (an actual HEAD move). Other ref
22
+ # updates (branch fast-forwards from fetch, stash refs, tag creation, etc.)
23
+ # don't touch the working tree and shouldn't be guarded.
24
+
25
+ # Only the 'prepared' phase can reject the transaction.
26
+ [ "$1" = "prepared" ] || exit 0
27
+
28
+ # Detect whether any update in this transaction moves HEAD.
29
+ moving_head=0
30
+ while read old_sha new_sha ref; do
31
+ [ "$ref" = "HEAD" ] || continue
32
+ [ "$old_sha" = "$new_sha" ] && continue
33
+ moving_head=1
34
+ done
35
+
36
+ # Nothing to check if HEAD is not moving.
37
+ [ "$moving_head" = "1" ] || exit 0
38
+
39
+ # Walk every gitlink in the current HEAD and check for dirty state.
40
+ # A gitlink is a tree entry with type 'commit' (mode 160000). The path is
41
+ # the directory in the parent's working tree that holds the embedded repo.
42
+ while read mode type sha path; do
43
+ [ "$type" = "commit" ] || continue
44
+ [ -d "$path/.git" ] || [ -f "$path/.git" ] || continue
45
+
46
+ # Refuse if there are uncommitted changes inside the embedded repo.
47
+ if ! (cd "$path" && git diff-index --quiet HEAD --) 2>/dev/null; then
48
+ echo "git-embedded: ✗ $path has uncommitted changes" >&2
49
+ echo " commit or stash inside $path/ before moving HEAD here" >&2
50
+ exit 1
51
+ fi
52
+ done < <(git ls-tree -r HEAD 2>/dev/null)
53
+
54
+ exit 0
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env bash
2
+ # update-embedded-repos
3
+ #
4
+ # Generic gitlink updater: finds every embedded git repo (gitlink with a
5
+ # working .git/ in the parent's tree) and updates it to the pinned SHA recorded
6
+ # in the parent's HEAD. Uses the child's own remote config — does NOT read or
7
+ # require .gitmodules.
8
+ #
9
+ # Install as post-checkout, post-merge, AND post-rewrite to cover the full
10
+ # set of standard checkout-flavored git operations (checkout, switch, bisect,
11
+ # pull, merge, rebase, cherry-pick). The script is identical for each hook
12
+ # name; link or copy to the right names.
13
+ #
14
+ # Exit status:
15
+ # - Always 0. This hook runs AFTER the parent operation has completed, so
16
+ # signalling failure here cannot undo the parent move. The companion
17
+ # reference-transaction hook is what prevents unsafe moves in the first
18
+ # place; this hook only carries out the working-tree side of the work.
19
+ # - A failure to update a single child is reported to stderr and the script
20
+ # continues to the next child.
21
+
22
+ # Walk every gitlink in the current HEAD and update the embedded child.
23
+ # Tree entries from `git ls-tree -r HEAD`:
24
+ # <mode> SP <type> SP <sha> TAB <path>
25
+ # Bash 'read' splits on $IFS (whitespace), so the four fields land in the
26
+ # four variables below. A path with a tab in it would split further; gitlink
27
+ # paths in practice are directory names and don't contain tabs.
28
+ while read mode type sha path; do
29
+ [ "$type" = "commit" ] || continue
30
+ [ -d "$path/.git" ] || [ -f "$path/.git" ] || continue
31
+
32
+ # Skip if already at the pinned SHA — idempotent re-runs are no-ops.
33
+ current=$(cd "$path" && git rev-parse HEAD 2>/dev/null) || continue
34
+ [ "$current" = "$sha" ] && continue
35
+
36
+ # Fetch from the child's own origin if the pinned SHA isn't local.
37
+ # The child has its own remote config (set up at clone time); no need
38
+ # to consult .gitmodules for the URL.
39
+ if ! (cd "$path" && git cat-file -e "${sha}^{commit}" 2>/dev/null); then
40
+ if ! (cd "$path" && git fetch --quiet 2>/dev/null); then
41
+ echo "git-embedded: warning: $path: fetch from child's own remote failed" >&2
42
+ echo " (cannot resolve pinned SHA $sha)" >&2
43
+ continue
44
+ fi
45
+ fi
46
+
47
+ # Detached HEAD on the pinned SHA. The parent pins a commit, not a branch,
48
+ # so the child ends up detached after each parent operation. Re-attach to
49
+ # a branch manually inside the child if you intend to make changes there.
50
+ if ! (cd "$path" && git checkout --quiet --detach "$sha" 2>/dev/null); then
51
+ echo "git-embedded: warning: $path: could not check out $sha" >&2
52
+ echo " (parent at this commit pins $path to $sha — resolve manually in $path/)" >&2
53
+ fi
54
+ done < <(git ls-tree -r HEAD 2>/dev/null)
55
+
56
+ exit 0
@@ -0,0 +1,25 @@
1
+ ## Bare `.githooks/` directory detected
2
+
3
+ This repo has `core.hooksPath` set to an in-repo path (typically `.githooks/`) containing hook scripts directly — no dispatcher script, no symlinks. The directory is tracked, so the hooks travel with the repo. This is the "hand-rolled, minimum dependency" setup.
4
+
5
+ `git-embedded` could install its scripts into this directory, but doing so means the scripts become tracked files in your repo. That is a deliberate choice the maintainer should make, not something the CLI should do silently.
6
+
7
+ ## Why the CLI will not auto-install
8
+
9
+ Tracked files alter the repo's content and history. The decision to commit `git-embedded`'s scripts into the repo (vs keeping them as an install-time artifact via the npm dependency) is a project-level call about how the hooks are sourced. The CLI defers this.
10
+
11
+ ## Integration options
12
+
13
+ **Option 1: commit the scripts into the tracked `.githooks/` directory.**
14
+
15
+ Run `git embedded copy-hooks-to .githooks/` (or whatever path your `core.hooksPath` points at). The CLI copies the four hook scripts into that directory. You then `git add .githooks/ && git commit`. From that point, the scripts ship with the repo — `npm install` is not required to get them.
16
+
17
+ Tradeoff: a copy of the scripts lives in your repo's history, separately from the npm-tracked version. Updates require re-running `copy-hooks-to` and committing.
18
+
19
+ **Option 2: convert to the dispatcher pattern.**
20
+
21
+ If you want a cleaner separation between "infrastructure that ships with the repo" and "policies the dispatcher enforces," the canonical dispatcher pattern gives you that: a small `_dispatch` script installed at `~/.config/git/hooks/` runs global policy and chains to whatever per-repo hooks each project ships. The bare `.githooks/` becomes a `.d/`-style directory underneath. `git embedded install-hooks` after the switch will detect the new dispatcher and install cleanly without committing files to the repo.
22
+
23
+ **Option 3: bring-your-own mode.**
24
+
25
+ `git embedded print-hook-script <name>` outputs each script's content. Paste them into your `.githooks/` files yourself, however you'd like to combine them with any existing hooks there.
@@ -0,0 +1,30 @@
1
+ ## Dispatcher pattern detected — ready to install
2
+
3
+ This machine has the canonical dispatcher pattern installed at the path printed above. The dispatcher chains to per-repo hooks at `.git/hooks/<name>` after running any global policy, and all required hook symlinks are present.
4
+
5
+ ## What the CLI will do
6
+
7
+ - Install `git-embedded`'s hook scripts into this repo's `.git/hooks/` directory:
8
+ - `post-checkout` — updates embedded children after checkout-style operations
9
+ - `post-merge` — updates embedded children after merges
10
+ - `post-rewrite` — updates embedded children after rebases
11
+ - `reference-transaction` — refuses HEAD-moving operations when any embedded child is dirty
12
+ - Set the executable bit on each script.
13
+ - Log the install to the transaction file so it can be reversed with `git embedded uninstall-hooks`.
14
+
15
+ No global state is modified. The dispatcher you already have activates the new scripts automatically.
16
+
17
+ ## After install
18
+
19
+ Test the setup:
20
+
21
+ ```bash
22
+ # Should report all four hooks installed and active
23
+ git embedded doctor
24
+ ```
25
+
26
+ To remove later:
27
+
28
+ ```bash
29
+ git embedded uninstall-hooks
30
+ ```
@@ -0,0 +1,56 @@
1
+ ## Dispatcher pattern detected — missing required hook entries
2
+
3
+ This machine has a dispatcher script at the path printed above, but the dispatcher directory is missing one or more entries (symlink, hard link, or copy) needed for `git-embedded` to function.
4
+
5
+ The required hook events are:
6
+
7
+ - `post-checkout`
8
+ - `post-merge`
9
+ - `post-rewrite`
10
+ - `reference-transaction`
11
+
12
+ Any missing entry means git skips that hook event entirely — even if the per-repo `.git/hooks/<name>` exists, git never reaches it because the dispatcher directory has nothing to invoke.
13
+
14
+ ## What the CLI proposes
15
+
16
+ Heal the dispatcher by creating the missing entries — symlinks by default, or hard links if you pass `--no-symlinks`. On Windows without elevation, creating symlinks requires UAC; the CLI requests elevation and creates all missing entries in one batch when you accept. Declining the UAC prompt exits the CLI without changes; use `--no-symlinks` if you want the no-prompt hard-link path instead.
17
+
18
+ The dispatcher script itself is not modified — only missing entries are added.
19
+
20
+ This is a global change to `core.hooksPath`'s directory. It will affect all repos that use this dispatcher, but only by enabling hook events that were previously disabled. Existing entries are not touched.
21
+
22
+ ## After healing
23
+
24
+ The CLI will then install `git-embedded`'s hook scripts into this repo's `.git/hooks/`, and the now-complete dispatcher will activate them.
25
+
26
+ ## Decline
27
+
28
+ If you decline the heal, the CLI exits without changes. You can heal manually:
29
+
30
+ **Linux / macOS (symlinks, no elevation needed):**
31
+ ```bash
32
+ ln -sf _dispatch <dispatcher-dir>/post-checkout
33
+ ln -sf _dispatch <dispatcher-dir>/post-merge
34
+ ln -sf _dispatch <dispatcher-dir>/post-rewrite
35
+ ln -sf _dispatch <dispatcher-dir>/reference-transaction
36
+ ```
37
+
38
+ **Windows (symlinks, run from an elevated Command Prompt):**
39
+ ```cmd
40
+ mklink "<dispatcher-dir>\post-checkout" "<dispatcher-dir>\_dispatch"
41
+ mklink "<dispatcher-dir>\post-merge" "<dispatcher-dir>\_dispatch"
42
+ mklink "<dispatcher-dir>\post-rewrite" "<dispatcher-dir>\_dispatch"
43
+ mklink "<dispatcher-dir>\reference-transaction" "<dispatcher-dir>\_dispatch"
44
+ ```
45
+
46
+ **Windows (hard links, no elevation needed — alternative if you don't want to elevate):**
47
+ ```cmd
48
+ mklink /H "<dispatcher-dir>\post-checkout" "<dispatcher-dir>\_dispatch"
49
+ mklink /H "<dispatcher-dir>\post-merge" "<dispatcher-dir>\_dispatch"
50
+ mklink /H "<dispatcher-dir>\post-rewrite" "<dispatcher-dir>\_dispatch"
51
+ mklink /H "<dispatcher-dir>\reference-transaction" "<dispatcher-dir>\_dispatch"
52
+ ```
53
+
54
+ (Hard links work on NTFS without elevated permissions. If your dispatcher dir is on a non-NTFS volume, copy `_dispatch` to each missing name instead.)
55
+
56
+ Re-run `git embedded install-hooks` after healing.
@@ -0,0 +1,35 @@
1
+ ## Non-conforming dispatcher detected
2
+
3
+ This machine has `core.hooksPath` pointed at a directory that contains a dispatcher-style script (multiple hook names redirect to one common script), but that dispatcher does NOT chain to per-repo hooks.
4
+
5
+ That means anything `git-embedded` places in `.git/hooks/<name>` is never reached. The dispatcher swallows each hook event without falling through to per-repo customization.
6
+
7
+ ## Why the CLI will not modify the dispatcher
8
+
9
+ Even with consent, rewriting someone else's dispatcher script is too invasive. The dispatcher enforces policies that may be load-bearing for other tools, security workflows, or team agreements. Modifying it risks breaking those.
10
+
11
+ ## Integration options
12
+
13
+ **Option 1: modify the dispatcher to chain to per-repo hooks.**
14
+
15
+ Add a block at the end of the dispatcher script that exec's the per-repo hook:
16
+
17
+ ```sh
18
+ # --- chain to the repository's own hook, if any ---
19
+ git_dir=$(git rev-parse --absolute-git-dir 2>/dev/null) || exit 0
20
+ repo_hook="$git_dir/hooks/$(basename "$0")"
21
+ if [ -x "$repo_hook" ] && [ "$repo_hook" != "$0" ]; then
22
+ exec "$repo_hook" "$@"
23
+ fi
24
+ exit 0
25
+ ```
26
+
27
+ Once added, re-run `git embedded install-hooks`. The dispatcher becomes conforming and the install proceeds normally.
28
+
29
+ **Option 2: switch to the canonical dispatcher pattern.**
30
+
31
+ If your dispatcher's policies are simple enough to port, the canonical dispatcher pattern (a small `_dispatch` script at `~/.config/git/hooks/` plus one symlink per standard hook name, chaining to per-repo hooks at the end) is a drop-in replacement. `git embedded install-hooks` after the switch will detect the new dispatcher and install cleanly.
32
+
33
+ **Option 3: bring-your-own mode.**
34
+
35
+ Run `git embedded print-hook-script <name>` for each hook event you want to enable. Paste the contents into the body of your dispatcher at the appropriate spot, gated by `basename "$0"` matching the hook name. Your dispatcher then runs the `git-embedded` logic inline instead of via chaining.
@@ -0,0 +1,35 @@
1
+ ## Husky detected
2
+
3
+ This repo uses [Husky](https://github.com/typicode/husky) for git hook management. Signals: `.husky/` directory at repo root, `"prepare": "husky"` script in `package.json`, and local `core.hooksPath` set to `.husky`.
4
+
5
+ Husky writes hook scripts directly into `.husky/<name>`. Those scripts shadow anything in `.git/hooks/`, so `git-embedded` cannot install via the normal per-repo path.
6
+
7
+ ## Why the CLI will not auto-integrate
8
+
9
+ Husky re-runs its setup on every `npm install` via the `prepare` lifecycle script. Files we write into `.husky/` could be overwritten or moved by Husky's reinstall, depending on Husky version and project configuration. Auto-edits to `.husky/<name>` files also lack a reliable rollback path if Husky's behavior changes between versions.
10
+
11
+ ## Manual integration options
12
+
13
+ **Option 1: append our invocation to Husky's hook files.**
14
+
15
+ Each `.husky/<name>` file is a plain shell script. Append a call to our hook script at the end (after Husky's own setup line, if present):
16
+
17
+ ```sh
18
+ # .husky/post-checkout
19
+ . "$(dirname -- "$0")/_/husky.sh" # Husky v8 only; omit on v9+
20
+
21
+ # ... any existing Husky-managed commands ...
22
+
23
+ # --- git-embedded: update embedded repos ---
24
+ npx git-embedded run-hook post-checkout "$@"
25
+ ```
26
+
27
+ Repeat for `post-merge`, `post-rewrite`, and `reference-transaction`. Husky's setup typically does not regenerate the body of hook files once they exist, so your additions persist — but verify on your specific Husky version.
28
+
29
+ **Option 2: replace Husky with the dispatcher pattern.**
30
+
31
+ The canonical dispatcher pattern (a small `_dispatch` script installed at `~/.config/git/hooks/`) covers the same ground as Husky — managed hooks plus per-repo extension — without the npm `prepare`-script execution path, which is a smaller supply-chain surface. Replacing Husky removes the `.husky/` directory, unsets local `core.hooksPath`, installs the dispatcher globally, and reruns `git embedded install-hooks`.
32
+
33
+ **Option 3: bring-your-own mode.**
34
+
35
+ Run `git embedded print-hook-script <name>` to get the script content, paste it inline into the relevant `.husky/<name>` file. Same as Option 1 but with the body inlined rather than invoked via `npx`.
@@ -0,0 +1,21 @@
1
+ ## `init.templateDir` detected
2
+
3
+ `git config --global init.templateDir` is set on this machine to the path printed above. That directory's contents are copied into `.git/` of every newly-created or freshly-cloned repository, including any hook scripts in `<template>/hooks/`.
4
+
5
+ This is complementary to per-repo hook installation, not a conflict. New repos start with whatever's in the template; existing repos are unaffected.
6
+
7
+ ## What the CLI offers
8
+
9
+ In addition to installing hooks into this repo's `.git/hooks/` normally, the CLI can copy `git-embedded`'s hook scripts into `<template>/hooks/` so that **future** new and cloned repos start with the hooks already in place. This is opt-in and requires confirmation because it modifies global state.
10
+
11
+ If you accept, the four hook scripts (`post-checkout`, `post-merge`, `post-rewrite`, `reference-transaction`) are written into the template's `hooks/` directory. The next `git init` or `git clone` on this machine copies them into the new repo automatically.
12
+
13
+ ## Caveats
14
+
15
+ - **Templates are copied, not symlinked.** Updates to the scripts in the template do not propagate to existing repos. To update, re-run `git embedded install-template` after updating the package; old repos retain whatever was in their template at clone time.
16
+ - **Doesn't fix existing repos.** Only future ones benefit. Run `git embedded install-hooks` per-repo for repos that already exist.
17
+ - **Per-repo install is still recommended** even with template install set up. The template install only matters at `git init` / `git clone` time. If you `git init` and forget the package was supposed to be installed, you still get the hooks. But if you clone an existing repo, the template still applies — which may or may not be what you want for repos that don't actually use `git-embedded`.
18
+
19
+ ## Decline
20
+
21
+ If you decline, nothing changes globally. The CLI proceeds with per-repo install only, which is the safer default.
@@ -0,0 +1,48 @@
1
+ ## Lefthook detected
2
+
3
+ This repo uses [Lefthook](https://github.com/evilmartians/lefthook) for git hook management. Signals: `lefthook.yml` (or `.lefthook.yml`, `lefthook.yaml`, `.lefthook.yaml`) at repo root, hook files in `.git/hooks/` with a Lefthook-generated header.
4
+
5
+ Lefthook writes hook files into `.git/hooks/` from its YAML config and may overwrite them on the next `lefthook install`. `git-embedded` cannot install into `.git/hooks/` without risk of being overwritten.
6
+
7
+ ## Why the CLI will not auto-integrate
8
+
9
+ Lefthook is the source of truth for `.git/hooks/` content in projects that use it. Editing those files directly is undone the next time Lefthook regenerates. Modifying `lefthook.yml` is the correct integration path, but parsing and editing arbitrary user YAML reliably (preserving comments, formatting, ordering) is too easy to get wrong for an automated tool to do silently.
10
+
11
+ ## Manual integration
12
+
13
+ Edit `lefthook.yml` to add `git-embedded`'s scripts as commands under each relevant hook event:
14
+
15
+ ```yaml
16
+ # lefthook.yml
17
+
18
+ post-checkout:
19
+ commands:
20
+ git-embedded:
21
+ run: npx git-embedded run-hook post-checkout {1} {2} {3}
22
+
23
+ post-merge:
24
+ commands:
25
+ git-embedded:
26
+ run: npx git-embedded run-hook post-merge {1}
27
+
28
+ post-rewrite:
29
+ commands:
30
+ git-embedded:
31
+ run: npx git-embedded run-hook post-rewrite {1}
32
+
33
+ reference-transaction:
34
+ commands:
35
+ git-embedded:
36
+ run: npx git-embedded run-hook reference-transaction {1}
37
+ # NOTE: lefthook's support for reference-transaction may vary by version.
38
+ # If your lefthook version doesn't list reference-transaction as a
39
+ # supported event, install that one hook outside of lefthook (directly
40
+ # into .git/hooks/reference-transaction) and lefthook will leave it
41
+ # alone.
42
+ ```
43
+
44
+ Then run `lefthook install` to regenerate `.git/hooks/` from the updated config. Run `git embedded doctor` to confirm the hooks are wired correctly.
45
+
46
+ ## Bring-your-own mode
47
+
48
+ `git embedded print-hook-script <name>` outputs the script body. You can inline it inside a lefthook `run:` block, or put it in a script file in the repo and reference that file from `lefthook.yml`.
@@ -0,0 +1,27 @@
1
+ ## No hook setup detected
2
+
3
+ This machine has no `core.hooksPath` set at any scope (system, global, or local), and the repo's `.git/hooks/` directory holds only git's default `.sample` files.
4
+
5
+ That means git is using its out-of-the-box per-repo hooks model — hooks placed in `.git/hooks/` will run, but they don't travel with a clone, and there's no shared infrastructure for global policy (commit-message validation, push-time checks, etc.).
6
+
7
+ ## Recommended: install the dispatcher pattern globally
8
+
9
+ The dispatcher pattern is a one-time setup that benefits every repo on this machine. It:
10
+
11
+ - Adds a small dispatcher script to `~/.config/git/hooks/` (or a path of your choice).
12
+ - Creates a symlink for each standard hook name pointing at the dispatcher. On Linux and macOS this requires no special permissions. On Windows it requires admin elevation — the CLI requests UAC and creates all symlinks in one batch on accept. If you prefer to avoid the elevation prompt, pass `--no-symlinks` to use hard links instead (works on NTFS without elevation).
13
+ - Sets `git config --global core.hooksPath` to that directory.
14
+ - The dispatcher chains to each repo's `.git/hooks/<name>` after running any global policy, so per-repo hooks keep working everywhere.
15
+
16
+ Once installed, `git-embedded` can install its scripts into the current repo's `.git/hooks/` and the global dispatcher activates them automatically. Future repos work the same way with no per-repo setup beyond running `git embedded install-hooks` once.
17
+
18
+ ## Alternative: install scripts into this repo only
19
+
20
+ If you don't want a global change, the CLI will install hooks directly into the current repo's `.git/hooks/`. Limitations:
21
+
22
+ - Hooks don't travel with the repo (`.git/` is not tracked). Other developers cloning this repo need to install separately.
23
+ - No global policy hooks are available (commit-msg, pre-push). Only what `git-embedded` provides will fire.
24
+
25
+ ## Bring-your-own mode
26
+
27
+ If you have a different setup in mind, run `git embedded print-hook-script <hook-name>` to see the script content and wire it in however you like.
@@ -0,0 +1,47 @@
1
+ ## pre-commit detected
2
+
3
+ This repo uses [pre-commit](https://pre-commit.com/) for git hook management. Signals: `.pre-commit-config.yaml` at repo root, `.git/hooks/<name>` files with a `# File generated by pre-commit` header.
4
+
5
+ pre-commit writes wrapper scripts into `.git/hooks/` and regenerates them on `pre-commit install`. `git-embedded` cannot install into `.git/hooks/` without risk of being overwritten.
6
+
7
+ ## Why the CLI will not auto-integrate
8
+
9
+ pre-commit's `.pre-commit-config.yaml` is the source of truth. The right integration is to add `git-embedded` as a local hook entry. As with Lefthook, parsing and editing user YAML reliably is too easy to get wrong for automated tooling.
10
+
11
+ ## Manual integration
12
+
13
+ Add a `local` hook block to `.pre-commit-config.yaml`:
14
+
15
+ ```yaml
16
+ # .pre-commit-config.yaml
17
+
18
+ repos:
19
+ - repo: local
20
+ hooks:
21
+ - id: git-embedded-update
22
+ name: git-embedded — update embedded repos
23
+ entry: npx git-embedded run-hook
24
+ language: system
25
+ pass_filenames: false
26
+ stages: [post-checkout, post-merge, post-rewrite]
27
+ verbose: true
28
+
29
+ - id: git-embedded-guard
30
+ name: git-embedded — guard HEAD moves when children are dirty
31
+ entry: npx git-embedded run-hook reference-transaction
32
+ language: system
33
+ pass_filenames: false
34
+ stages: [reference-transaction] # support for this stage depends on
35
+ # the pre-commit version
36
+ verbose: true
37
+ ```
38
+
39
+ Then `pre-commit install --hook-type post-checkout --hook-type post-merge --hook-type post-rewrite --hook-type reference-transaction` to wire all four event types.
40
+
41
+ ## Caveat about reference-transaction
42
+
43
+ `reference-transaction` is a relatively new git hook (2020) and pre-commit's stage coverage varies by version. Check your pre-commit version's documentation. If it doesn't support `reference-transaction`, install that one hook manually into `.git/hooks/reference-transaction` outside pre-commit; pre-commit will not regenerate hook files it didn't create.
44
+
45
+ ## Bring-your-own mode
46
+
47
+ `git embedded print-hook-script <name>` outputs the script body. You can drop the script into a tracked file in the repo and reference it from the `entry:` field instead of `npx git-embedded run-hook`, removing the indirection through `npx`.