great-cto 2.95.0 → 2.96.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.
package/dist/main.js CHANGED
@@ -27,6 +27,7 @@ import { daemonSpec, decideEnsureAction, isBoardResponse } from "./board-daemon.
27
27
  import { readFileSync, writeFileSync, copyFileSync, chmodSync, mkdirSync, unlinkSync, existsSync as fsExistsSync } from "node:fs";
28
28
  import { dirname, join } from "node:path";
29
29
  import { fileURLToPath } from "node:url";
30
+ import { execFileSync } from "node:child_process";
30
31
  import { homedir } from "node:os";
31
32
  function getCliVersion() {
32
33
  try {
@@ -1154,11 +1155,48 @@ async function promoteTelemetryOptIn(opts) {
1154
1155
  * .git/hooks/pre-push so that future pushes are scanned for private project
1155
1156
  * name leaks. Best-effort — never throws.
1156
1157
  */
1158
+ /**
1159
+ * The directory git will actually read hooks from.
1160
+ *
1161
+ * `core.hooksPath` wins over `.git/hooks` when set, and a linked worktree reads
1162
+ * from the main checkout's git dir. Returns null when this is not a repository.
1163
+ */
1164
+ function effectiveGitHooksDir(projectDir) {
1165
+ const git = (args) => {
1166
+ try {
1167
+ return execFileSync("git", args, {
1168
+ cwd: projectDir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"],
1169
+ }).trim();
1170
+ }
1171
+ catch {
1172
+ return "";
1173
+ }
1174
+ };
1175
+ const top = git(["rev-parse", "--show-toplevel"]);
1176
+ if (!top)
1177
+ return null;
1178
+ const configured = git(["config", "--get", "core.hooksPath"]);
1179
+ if (configured)
1180
+ return resolve(top, configured);
1181
+ const common = git(["rev-parse", "--git-common-dir"]);
1182
+ return join(common ? resolve(top, common) : join(top, ".git"), "hooks");
1183
+ }
1157
1184
  function installPrePushHook(projectDir) {
1158
1185
  try {
1159
- const gitHooksDir = join(projectDir, ".git", "hooks");
1160
- if (!fsExistsSync(gitHooksDir))
1186
+ // Ask git where it reads hooks from, rather than assuming `.git/hooks`.
1187
+ //
1188
+ // This installer wrote the file it was asked to write and reported success
1189
+ // for months while `core.hooksPath` pointed at a directory the repository
1190
+ // had moved out of. Git honours that setting even when it does not exist, so
1191
+ // no hook ran, and the success message was the only evidence anyone had.
1192
+ const gitHooksDir = effectiveGitHooksDir(projectDir);
1193
+ if (!gitHooksDir)
1161
1194
  return; // not a git repo — skip silently
1195
+ if (!fsExistsSync(gitHooksDir)) {
1196
+ warn(`git is configured to read hooks from ${gitHooksDir}, which does not exist — ` +
1197
+ `no hook can run until that is fixed (git config --unset core.hooksPath)`);
1198
+ return;
1199
+ }
1162
1200
  const dest = join(gitHooksDir, "pre-push");
1163
1201
  if (fsExistsSync(dest)) {
1164
1202
  log(` ${dim("pre-push hook already present — skipped")}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "great-cto",
3
- "version": "2.95.0",
3
+ "version": "2.96.0",
4
4
  "description": "One command install for the great_cto Claude Code plugin. Auto-detects your stack, picks the right archetype, bootstraps PROJECT.md.",
5
5
  "keywords": [
6
6
  "claude-code",