@marcomigozzi/trailhead 0.13.0 → 0.15.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 (120) hide show
  1. package/README.it.md +142 -29
  2. package/README.md +141 -28
  3. package/bin/base-preflight-docs.test.js +65 -0
  4. package/bin/execute-split-docs.test.js +146 -0
  5. package/bin/isolation-answer-docs.test.js +41 -0
  6. package/bin/isolation-copy-docs.test.js +28 -0
  7. package/bin/lib/codex-projection.js +17 -10
  8. package/bin/lib/codex-projection.test.js +80 -11
  9. package/bin/lib/install-verify.js +10 -5
  10. package/bin/lib/install-verify.test.js +51 -0
  11. package/bin/report-status-docs.test.js +195 -0
  12. package/bin/resolve-helpers-docs.test.js +36 -0
  13. package/bin/scope-confirm-skip-docs.test.js +70 -0
  14. package/bin/trailhead.js +40 -28
  15. package/bin/trailhead.test.js +503 -6
  16. package/bin/uat-setup-docs.test.js +65 -0
  17. package/package.json +4 -1
  18. package/plugins/trailhead/.claude-plugin/plugin.json +3 -2
  19. package/plugins/trailhead/agents/trailhead-code-review.md +15 -2
  20. package/plugins/trailhead/agents/trailhead-codebase-map.md +20 -1
  21. package/plugins/trailhead/agents/trailhead-debug.md +20 -1
  22. package/plugins/trailhead/agents/trailhead-executor.md +29 -2
  23. package/plugins/trailhead/agents/trailhead-fix.md +29 -2
  24. package/plugins/trailhead/agents/trailhead-plan.md +20 -1
  25. package/plugins/trailhead/agents/trailhead-research.md +22 -3
  26. package/plugins/trailhead/agents/trailhead-verify.md +15 -2
  27. package/plugins/trailhead/commands/panel.md +10 -0
  28. package/plugins/trailhead/hooks/hooks.json +3 -18
  29. package/plugins/trailhead/hooks/lib/base-preflight.test.js +513 -0
  30. package/plugins/trailhead/hooks/lib/body-sources.js +795 -0
  31. package/plugins/trailhead/hooks/lib/commit-message-check.js +1 -1
  32. package/plugins/trailhead/hooks/lib/commit-message-check.test.js +3 -2
  33. package/plugins/trailhead/hooks/lib/commit-msg-module-repo.test.js +116 -0
  34. package/plugins/trailhead/hooks/lib/commit-msg-sync.test.js +100 -9
  35. package/plugins/trailhead/hooks/lib/dashboard.test.js +409 -0
  36. package/plugins/trailhead/hooks/lib/fixtures/commit-msg-legacy-js +104 -0
  37. package/plugins/trailhead/hooks/lib/gh-subcommand.js +92 -10
  38. package/plugins/trailhead/hooks/lib/gh-subcommand.test.js +55 -0
  39. package/plugins/trailhead/hooks/lib/isolation-answer.test.js +258 -0
  40. package/plugins/trailhead/hooks/lib/isolation-copy.test.js +298 -0
  41. package/plugins/trailhead/hooks/lib/issue-body.test.js +470 -0
  42. package/plugins/trailhead/hooks/lib/mockup-link.js +21 -5
  43. package/plugins/trailhead/hooks/lib/report-status.test.js +366 -0
  44. package/plugins/trailhead/hooks/lib/shell-scan.js +532 -65
  45. package/plugins/trailhead/hooks/lib/shell-scan.test.js +434 -0
  46. package/plugins/trailhead/hooks/lib/uat-backfill.test.js +150 -0
  47. package/plugins/trailhead/hooks/lib/uat-setup.test.js +340 -0
  48. package/plugins/trailhead/hooks/lib/unmodelled.js +345 -0
  49. package/plugins/trailhead/hooks/lib/unmodelled.test.js +173 -0
  50. package/plugins/trailhead/hooks/lib/work-state.test.js +297 -0
  51. package/plugins/trailhead/hooks/mod/README.md +219 -0
  52. package/plugins/trailhead/hooks/mod/map-pane-view.ts +606 -0
  53. package/plugins/trailhead/hooks/mod/map-pane.tsx +832 -0
  54. package/plugins/trailhead/hooks/mod/register.tsx +293 -0
  55. package/plugins/trailhead/hooks/mod/work-state-prompt.ts +69 -0
  56. package/plugins/trailhead/hooks/mod/work-state-view.ts +110 -0
  57. package/plugins/trailhead/hooks/trailhead-bash-guards.js +140 -0
  58. package/plugins/trailhead/hooks/trailhead-bash-guards.test.js +233 -0
  59. package/plugins/trailhead/hooks/trailhead-body-guard.js +145 -61
  60. package/plugins/trailhead/hooks/trailhead-body-guard.test.js +224 -3
  61. package/plugins/trailhead/hooks/trailhead-commit-guard.js +41 -14
  62. package/plugins/trailhead/hooks/trailhead-commit-guard.test.js +2 -1
  63. package/plugins/trailhead/hooks/trailhead-install-guard.js +187 -42
  64. package/plugins/trailhead/hooks/trailhead-install-guard.test.js +182 -2
  65. package/plugins/trailhead/hooks/trailhead-mockup-link-guard.js +48 -11
  66. package/plugins/trailhead/hooks/trailhead-mockup-link-guard.test.js +75 -3
  67. package/plugins/trailhead/hooks/trailhead-search-guard.js +59 -13
  68. package/plugins/trailhead/hooks/trailhead-search-guard.test.js +68 -3
  69. package/plugins/trailhead/hooks/trailhead-secret-guard.js +541 -57
  70. package/plugins/trailhead/hooks/trailhead-secret-guard.test.js +1000 -4
  71. package/plugins/trailhead/hooks/trailhead-secret-read-guard.js +171 -6
  72. package/plugins/trailhead/hooks/trailhead-secret-read-guard.test.js +170 -0
  73. package/plugins/trailhead/skills/_shared/choices.md +7 -3
  74. package/plugins/trailhead/skills/_shared/configuration-reference.md +15 -3
  75. package/plugins/trailhead/skills/_shared/load-first.md +3 -3
  76. package/plugins/trailhead/skills/_shared/multi-map.md +3 -3
  77. package/plugins/trailhead/skills/_shared/session-handoff.md +1 -1
  78. package/plugins/trailhead/skills/_shared/substrate-commands.md +22 -11
  79. package/plugins/trailhead/skills/_shared/substrate.md +6 -4
  80. package/plugins/trailhead/skills/_shared/teamwork.md +17 -6
  81. package/plugins/trailhead/skills/_shared/techniques/acceptance-testing.md +17 -9
  82. package/plugins/trailhead/skills/_shared/techniques/code-review.md +1 -1
  83. package/plugins/trailhead/skills/_shared/techniques/codebase-map.md +1 -1
  84. package/plugins/trailhead/skills/_shared/techniques/debug.md +2 -0
  85. package/plugins/trailhead/skills/_shared/techniques/prototype.md +1 -1
  86. package/plugins/trailhead/skills/_shared/techniques/research.md +4 -2
  87. package/plugins/trailhead/skills/_shared/techniques/verify.md +1 -1
  88. package/plugins/trailhead/skills/_shared/techniques.md +12 -2
  89. package/plugins/trailhead/skills/trailhead/SKILL.md +3 -3
  90. package/plugins/trailhead/skills/trailhead-capture/SKILL.md +1 -1
  91. package/plugins/trailhead/skills/trailhead-capture/references/capture.md +2 -2
  92. package/plugins/trailhead/skills/trailhead-chart/SKILL.md +1 -1
  93. package/plugins/trailhead/skills/trailhead-chart/references/charting.md +1 -1
  94. package/plugins/trailhead/skills/trailhead-manage/SKILL.md +1 -1
  95. package/plugins/trailhead/skills/trailhead-manage/references/auditing.md +1 -1
  96. package/plugins/trailhead/skills/trailhead-view/SKILL.md +8 -6
  97. package/plugins/trailhead/skills/trailhead-work/SKILL.md +16 -13
  98. package/plugins/trailhead/skills/trailhead-work/references/auto.md +6 -4
  99. package/plugins/trailhead/skills/trailhead-work/references/executor-brief.md +29 -0
  100. package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +28 -10
  101. package/plugins/trailhead/templates/trailhead-base-preflight.js +305 -0
  102. package/plugins/trailhead/templates/trailhead-commit-msg +38 -102
  103. package/plugins/trailhead/templates/trailhead-commit-msg-sync.js +58 -27
  104. package/plugins/trailhead/templates/trailhead-commit-msg.cjs +114 -0
  105. package/plugins/trailhead/templates/trailhead-dashboard.js +340 -0
  106. package/plugins/trailhead/templates/trailhead-isolation-answer.js +227 -0
  107. package/plugins/trailhead/templates/trailhead-isolation-copy.js +239 -0
  108. package/plugins/trailhead/templates/trailhead-issue-body.js +459 -0
  109. package/plugins/trailhead/templates/trailhead-report-status.js +274 -0
  110. package/plugins/trailhead/templates/trailhead-uat-backfill.js +114 -0
  111. package/plugins/trailhead/templates/trailhead-uat-setup.js +385 -0
  112. package/plugins/trailhead/templates/trailhead-work-state.js +217 -0
  113. package/plugins/trailhead/tests/map-pane-ui.test.ts +1921 -0
  114. package/plugins/trailhead/tests/map-pane-view.test.ts +853 -0
  115. package/plugins/trailhead/tests/mod.test.ts +14 -0
  116. package/plugins/trailhead/tests/work-state-prompt-hook.test.ts +207 -0
  117. package/plugins/trailhead/tests/work-state-prompt.test.ts +109 -0
  118. package/plugins/trailhead/tests/work-state-ui.test.ts +365 -0
  119. package/plugins/trailhead/tests/work-state-view.test.ts +110 -0
  120. package/plugins/trailhead/types/index.d.ts +103 -0
@@ -1,104 +1,40 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
- // trailhead commit-msg hook (git). Host-independent guardrail: installed into
4
- // .git/hooks/commit-msg at repo first-use so trailhead's commit discipline is
5
- // enforced on EVERY `git commit`, whatever agent host drives it (Codex has no
6
- // hook bus; this git hook is how the commit-guard survives there). Decision #16.
7
- //
8
- // Self-contained by design: detached from the trailhead install, it inlines the
9
- // same rules as plugins/trailhead/hooks/lib/commit-message-check.js. That
10
- // lib's test drives this hook end-to-end so the copies cannot drift.
11
- //
12
- // git passes the commit-message file path as argv[2]. On a violation: print the
13
- // reason to stderr and exit 1 (git aborts the commit). Otherwise exit 0.
14
- // Crash-safe: any unexpected error exits 0 (never wedge a commit).
15
- //
16
- // Note: a merge/revert/fixup message already fails the conventional-subject
17
- // rule below, so it needs no special case; `git commit --amend` keeps whatever
18
- // trailer the original message carried; `git commit --no-verify` bypasses this
19
- // hook entirely, as before.
20
-
21
- const fs = require('fs');
22
- const path = require('path');
23
-
24
- const TYPES = 'feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert';
25
- const CONVENTIONAL = new RegExp(`^(${TYPES})(\\([^)]+\\))?!?:\\s.+`);
26
- const CO_AUTHORED = /co-authored-by\s*:/i;
27
- const SCISSORS = '# ------------------------ >8';
28
- const REFS_LINE = /^\s*refs\s*:(.*)$/i;
29
-
30
- function messageRegion(raw) {
31
- const text = String(raw == null ? '' : raw);
32
- const i = text.indexOf(SCISSORS);
33
- return i === -1 ? text : text.slice(0, i);
34
- }
35
- function subjectOf(raw) {
36
- for (const line of messageRegion(raw).split('\n')) {
37
- if (line.trim() === '' || line.startsWith('#')) continue;
38
- return line.trim();
39
- }
40
- return '';
1
+ #!/bin/sh
2
+ # trailhead commit-msg hook (git). Thin sh wrapper around the CommonJS logic in
3
+ # trailhead-commit-msg.cjs beside it (#257: an extensionless node script would
4
+ # inherit the "type" of the user repo's package.json, so "type": "module" broke it).
5
+ #
6
+ # Never blocks a commit on a startup failure: node missing, the .cjs missing or
7
+ # unreadable, or node crashing at load all exit 0, but print a one-line "inactive"
8
+ # notice on stderr so the failure is not silent. Only the .cjs's dedicated
9
+ # rule-violation code (3) becomes exit 1, which makes git abort the commit.
10
+
11
+ # Avviso unico su stderr, stesso formato per tutti i casi.
12
+ inactive() {
13
+ echo "trailhead: commit-msg hook inactive ($1); commit rules not enforced." >&2
41
14
  }
42
15
 
43
- // Mirrors parseSessionTicket() in commit-message-check.js: parses the first
44
- // line of a .trailhead/session-ticket marker (`#<n> <title>`).
45
- function parseSessionTicket(text) {
46
- const raw = String(text == null ? '' : text).replace(/^/, '');
47
- const firstLine = raw.split('\n')[0].replace(/\r$/, '');
48
- const m = firstLine.match(/^#([1-9]\d*)(?:\s|$)/);
49
- return m ? Number(m[1]) : null;
50
- }
51
-
52
- // Mirrors hasRefsFor() in commit-message-check.js.
53
- function hasRefsFor(message, n) {
54
- const target = new RegExp('#' + n + '(?![\\w])');
55
- for (const line of messageRegion(message).split('\n')) {
56
- if (line.trim().startsWith('#')) continue;
57
- const m = line.match(REFS_LINE);
58
- if (m && target.test(m[1])) return true;
59
- }
60
- return false;
61
- }
62
-
63
- // Reads the session-ticket marker at the worktree top-level. git runs
64
- // commit-msg from the worktree top-level (linked worktrees included), so
65
- // process.cwd() is the right root here. Its own try/catch: a read error
66
- // (marker absent, unreadable, etc.) must never skip the other rules below,
67
- // it just leaves the Refs rule off.
68
- function readTicket() {
69
- try {
70
- const markerPath = path.join(process.cwd(), '.trailhead', 'session-ticket');
71
- return parseSessionTicket(fs.readFileSync(markerPath, 'utf8'));
72
- } catch {
73
- return null;
74
- }
75
- }
76
-
77
- try {
78
- const file = process.argv[2];
79
- const raw = file ? fs.readFileSync(file, 'utf8') : '';
80
- const region = messageRegion(raw);
81
-
82
- if (CO_AUTHORED.test(region)) {
83
- process.stderr.write('trailhead: do not add a Co-Authored-By trailer to commits. Remove it and commit again.\n');
84
- process.exit(1);
85
- }
86
- const subject = subjectOf(raw);
87
- if (subject && !CONVENTIONAL.test(subject)) {
88
- process.stderr.write(`trailhead: commit subject must be Conventional Commits: <type>(<scope>)?: <subject>. Valid types: ${TYPES.replace(/\|/g, ', ')}.\n`);
89
- process.exit(1);
90
- }
91
- if (subject.length > 72) {
92
- process.stderr.write('trailhead: commit subject must be 72 characters or less.\n');
93
- process.exit(1);
94
- }
95
-
96
- const ticket = readTicket();
97
- if (Number.isInteger(ticket) && ticket > 0 && !hasRefsFor(raw, ticket)) {
98
- process.stderr.write(`trailhead: this commit is on ticket #${ticket} (per .trailhead/session-ticket) but has no "Refs: #${ticket}" trailer. Add "Refs: #${ticket}" as the last line of the message and commit again. If this commit is not part of ticket #${ticket}, remove the stale .trailhead/session-ticket marker first.\n`);
99
- process.exit(1);
100
- }
101
- } catch {
102
- process.exit(0); // never wedge a commit on an unexpected error
103
- }
104
- process.exit(0);
16
+ case $0 in
17
+ */*) dir=${0%/*} ;;
18
+ *) dir=. ;;
19
+ esac
20
+ cjs="$dir/trailhead-commit-msg.cjs"
21
+
22
+ if [ ! -r "$cjs" ]; then
23
+ inactive "trailhead-commit-msg.cjs missing or unreadable"
24
+ exit 0
25
+ fi
26
+ if ! command -v node >/dev/null 2>&1; then
27
+ inactive "node not found on PATH"
28
+ exit 0
29
+ fi
30
+
31
+ node "$cjs" "$@"
32
+ code=$?
33
+ case $code in
34
+ 0) exit 0 ;;
35
+ 3) exit 1 ;;
36
+ *)
37
+ inactive "trailhead-commit-msg.cjs failed to run, exit $code"
38
+ exit 0
39
+ ;;
40
+ esac
@@ -2,10 +2,13 @@
2
2
  'use strict';
3
3
  // trailhead commit-msg hook sync (self-contained, ships in templates/ so it
4
4
  // reaches every install channel: bin/trailhead.js copies templates/ wholesale
5
- // for Claude/npm and Codex alike). Installs the sibling template
6
- // trailhead-commit-msg into the repo's git hooks dir, upgrades it in place
7
- // when an OLDER trailhead hook is already there (recognised by its own
8
- // header line, never by trusting the filename), and never touches a foreign
5
+ // for Claude/npm and Codex alike). Installs the sibling templates into the
6
+ // repo's git hooks dir: trailhead-commit-msg (a small sh wrapper, installed as
7
+ // commit-msg) and trailhead-commit-msg.cjs (the logic, installed beside it; a
8
+ // .cjs is always CommonJS, so a "type": "module" repo cannot break it, #257).
9
+ // Upgrades in place when an OLDER trailhead hook (the legacy single-file JS
10
+ // one, or an outdated wrapper/.cjs) is already there, recognised by its own
11
+ // header line, never by trusting the filename, and never touches a foreign
9
12
  // (non-trailhead) hook. #185.
10
13
  //
11
14
  // CLI: node trailhead-commit-msg-sync.js [--repo <dir>] (default cwd)
@@ -21,13 +24,17 @@ const path = require('path');
21
24
  const crypto = require('crypto');
22
25
  const { execFileSync } = require('child_process');
23
26
 
24
- // La prima riga riconosciuta di ogni versione dell'hook trailhead: basta che
25
- // una delle prime 5 righe (trim) inizi con questa stringa.
26
- const TRAILHEAD_MARK = '// trailhead commit-msg hook (git).';
27
+ // Nome del file di logica installato accanto al wrapper commit-msg.
28
+ const CJS_NAME = 'trailhead-commit-msg.cjs';
29
+
30
+ // La riga riconosciuta di ogni versione dell'hook trailhead: basta che una
31
+ // delle prime 5 righe (trim) inizi con una di queste stringhe. La prima è
32
+ // l'hook JS monofile legacy, la seconda il wrapper sh (layout #257).
33
+ const TRAILHEAD_MARKS = ['// trailhead commit-msg hook (git).', '# trailhead commit-msg hook (git).'];
27
34
 
28
35
  function isTrailheadHook(text) {
29
36
  const lines = String(text == null ? '' : text).split('\n').slice(0, 5);
30
- return lines.some((line) => line.trim().startsWith(TRAILHEAD_MARK));
37
+ return lines.some((line) => TRAILHEAD_MARKS.some((mark) => line.trim().startsWith(mark)));
31
38
  }
32
39
 
33
40
  // Uniforma i fine riga per il confronto di contenuto: CRLF -> LF, non tocca
@@ -65,10 +72,36 @@ function resolveHooksTarget({ repoDir, runGit = defaultRunGit }) {
65
72
  return { hooksDir, hooksPathSet };
66
73
  }
67
74
 
68
- // Applica la sincronizzazione vera e propria dentro hooksDir. fs è iniettato
69
- // per testabilità (default: il modulo fs reale).
70
- function syncCommitMsgHook({ hooksDir, templateText, hooksPathSet, fs: fsMod = fs }) {
75
+ // Scrittura atomica: temp file unico nella stessa dir, poi rename sul
76
+ // destinazione (mai un truncate in place).
77
+ function writeAtomic(fsMod, hooksDir, finalPath, text) {
78
+ const tmpPath = path.join(hooksDir, `.commit-msg.trailhead-${process.pid}-${crypto.randomBytes(4).toString('hex')}`);
79
+ try {
80
+ fsMod.writeFileSync(tmpPath, text);
81
+ fsMod.chmodSync(tmpPath, 0o755);
82
+ fsMod.renameSync(tmpPath, finalPath);
83
+ } catch (err) {
84
+ try { fsMod.unlinkSync(tmpPath); } catch { /* best effort cleanup */ }
85
+ throw err;
86
+ }
87
+ }
88
+
89
+ // true se il file esiste, non è un symlink e ha esattamente il contenuto atteso.
90
+ function matchesTemplate(fsMod, filePath, text) {
91
+ try {
92
+ if (fsMod.lstatSync(filePath).isSymbolicLink()) return false;
93
+ return normalize(fsMod.readFileSync(filePath, 'utf8')) === normalize(text);
94
+ } catch {
95
+ return false;
96
+ }
97
+ }
98
+
99
+ // Applica la sincronizzazione vera e propria dentro hooksDir. templateText è il
100
+ // wrapper sh (installato come commit-msg), cjsText la logica (installata come
101
+ // trailhead-commit-msg.cjs). fs è iniettato per testabilità.
102
+ function syncCommitMsgHook({ hooksDir, templateText, cjsText, hooksPathSet, fs: fsMod = fs }) {
71
103
  const hookPath = path.join(hooksDir, 'commit-msg');
104
+ const cjsPath = path.join(hooksDir, CJS_NAME);
72
105
 
73
106
  if (!fsMod.existsSync(hookPath)) {
74
107
  if (hooksPathSet) {
@@ -79,8 +112,9 @@ function syncCommitMsgHook({ hooksDir, templateText, hooksPathSet, fs: fsMod = f
79
112
  };
80
113
  }
81
114
  fsMod.mkdirSync(hooksDir, { recursive: true });
82
- fsMod.writeFileSync(hookPath, templateText);
83
- fsMod.chmodSync(hookPath, 0o755);
115
+ // Prima la logica, poi il wrapper: mai un wrapper senza il suo script.
116
+ writeAtomic(fsMod, hooksDir, cjsPath, cjsText);
117
+ writeAtomic(fsMod, hooksDir, hookPath, templateText);
84
118
  return { status: 'installed', hook: hookPath, message: 'installed the trailhead commit-msg hook.' };
85
119
  }
86
120
 
@@ -94,22 +128,18 @@ function syncCommitMsgHook({ hooksDir, templateText, hooksPathSet, fs: fsMod = f
94
128
  return { status: 'foreign', hook: hookPath, message: 'a foreign commit-msg hook is present; left untouched.' };
95
129
  }
96
130
 
97
- if (normalize(existingText) === normalize(templateText)) {
131
+ const wrapperCurrent = normalize(existingText) === normalize(templateText);
132
+ const cjsCurrent = matchesTemplate(fsMod, cjsPath, cjsText);
133
+
134
+ if (wrapperCurrent && cjsCurrent) {
98
135
  if ((stat.mode & 0o777) !== 0o755) fsMod.chmodSync(hookPath, 0o755);
99
136
  return { status: 'current', hook: hookPath, message: 'the trailhead commit-msg hook is already current.' };
100
137
  }
101
138
 
102
- // Upgrade atomico: scrive su un temp file unico nella stessa dir, poi rename
103
- // sopra l'hook esistente (mai un truncate in place).
104
- const tmpPath = path.join(hooksDir, `.commit-msg.trailhead-${process.pid}-${crypto.randomBytes(4).toString('hex')}`);
105
- try {
106
- fsMod.writeFileSync(tmpPath, templateText);
107
- fsMod.chmodSync(tmpPath, 0o755);
108
- fsMod.renameSync(tmpPath, hookPath);
109
- } catch (err) {
110
- try { fsMod.unlinkSync(tmpPath); } catch { /* best effort cleanup */ }
111
- throw err;
112
- }
139
+ // Upgrade: logica per prima, wrapper per ultimo (mai un wrapper senza script).
140
+ if (!cjsCurrent) writeAtomic(fsMod, hooksDir, cjsPath, cjsText);
141
+ if (!wrapperCurrent) writeAtomic(fsMod, hooksDir, hookPath, templateText);
142
+ else if ((stat.mode & 0o777) !== 0o755) fsMod.chmodSync(hookPath, 0o755);
113
143
  return { status: 'upgraded', hook: hookPath, message: 'upgraded the trailhead commit-msg hook to the current template.' };
114
144
  }
115
145
 
@@ -153,7 +183,8 @@ function main(argv, { cwd, stdout } = {}) {
153
183
  hooksDirForError = target.hooksDir;
154
184
  const templatePath = path.join(__dirname, 'trailhead-commit-msg');
155
185
  const templateText = fs.readFileSync(templatePath, 'utf8');
156
- const result = syncCommitMsgHook({ hooksDir: target.hooksDir, templateText, hooksPathSet: target.hooksPathSet, fs });
186
+ const cjsText = fs.readFileSync(path.join(__dirname, CJS_NAME), 'utf8');
187
+ const result = syncCommitMsgHook({ hooksDir: target.hooksDir, templateText, cjsText, hooksPathSet: target.hooksPathSet, fs });
157
188
  writeResult(out, result);
158
189
  return 0;
159
190
  } catch (err) {
@@ -163,7 +194,7 @@ function main(argv, { cwd, stdout } = {}) {
163
194
  }
164
195
  }
165
196
 
166
- module.exports = { resolveHooksTarget, isTrailheadHook, normalize, syncCommitMsgHook, parseArgs, main };
197
+ module.exports = { CJS_NAME, resolveHooksTarget, isTrailheadHook, normalize, syncCommitMsgHook, parseArgs, main };
167
198
 
168
199
  if (require.main === module) {
169
200
  const code = main(process.argv.slice(2), { cwd: process.cwd(), stdout: process.stdout });
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // trailhead commit-msg hook logic (git, CommonJS). Host-independent guardrail:
4
+ // installed beside the sh wrapper .git/hooks/commit-msg (which calls it) at repo
5
+ // first-use so trailhead's commit discipline is enforced on EVERY `git commit`,
6
+ // whatever agent host drives it (Codex has no hook bus; this git hook is how the
7
+ // commit-guard survives there). Decision #16.
8
+ //
9
+ // The .cjs extension is deliberate (#257): Node takes the module format of an
10
+ // extensionless file from the nearest package.json "type", and under .git/hooks/
11
+ // that is the user repo's, so a "type": "module" repo would load this as ESM and
12
+ // fail on require(). A .cjs file is always CommonJS.
13
+ //
14
+ // Self-contained by design: detached from the trailhead install, it inlines the
15
+ // same rules as plugins/trailhead/hooks/lib/commit-message-check.js. That
16
+ // lib's test drives this hook end-to-end so the copies cannot drift.
17
+ //
18
+ // git passes the commit-message file path as argv[2] (the wrapper forwards it).
19
+ // On a violation: print the reason to stderr and exit VIOLATION (3); the wrapper
20
+ // maps exactly that code to exit 1 (git aborts the commit); any code other than
21
+ // 0 or 3 (a crash at load) is reported as the one-line "inactive" notice on
22
+ // stderr and exits 0. Otherwise exit 0. Crash-safe: any unexpected error exits
23
+ // 0 (never wedge a commit).
24
+ //
25
+ // Note: a merge/revert/fixup message already fails the conventional-subject
26
+ // rule below, so it needs no special case; `git commit --amend` keeps whatever
27
+ // trailer the original message carried; `git commit --no-verify` bypasses this
28
+ // hook entirely, as before.
29
+
30
+ const fs = require('fs');
31
+ const path = require('path');
32
+
33
+ const VIOLATION = 3; // codice dedicato, mappato a 1 dal wrapper sh
34
+ const TYPES = 'feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert';
35
+ const CONVENTIONAL = new RegExp(`^(${TYPES})(\\([^)]+\\))?!?:\\s.+`);
36
+ const CO_AUTHORED = /co-authored-by\s*:/i;
37
+ const SCISSORS = '# ------------------------ >8';
38
+ const REFS_LINE = /^\s*refs\s*:(.*)$/i;
39
+
40
+ function messageRegion(raw) {
41
+ const text = String(raw == null ? '' : raw);
42
+ const i = text.indexOf(SCISSORS);
43
+ return i === -1 ? text : text.slice(0, i);
44
+ }
45
+ function subjectOf(raw) {
46
+ for (const line of messageRegion(raw).split('\n')) {
47
+ if (line.trim() === '' || line.startsWith('#')) continue;
48
+ return line.trim();
49
+ }
50
+ return '';
51
+ }
52
+
53
+ // Mirrors parseSessionTicket() in commit-message-check.js: parses the first
54
+ // line of a .trailhead/session-ticket marker (`#<n> <title>`).
55
+ function parseSessionTicket(text) {
56
+ const raw = String(text == null ? '' : text).replace(/^/, '');
57
+ const firstLine = raw.split('\n')[0].replace(/\r$/, '');
58
+ const m = firstLine.match(/^#([1-9]\d*)(?:\s|$)/);
59
+ return m ? Number(m[1]) : null;
60
+ }
61
+
62
+ // Mirrors hasRefsFor() in commit-message-check.js.
63
+ function hasRefsFor(message, n) {
64
+ const target = new RegExp('#' + n + '(?![\\w])');
65
+ for (const line of messageRegion(message).split('\n')) {
66
+ if (line.trim().startsWith('#')) continue;
67
+ const m = line.match(REFS_LINE);
68
+ if (m && target.test(m[1])) return true;
69
+ }
70
+ return false;
71
+ }
72
+
73
+ // Reads the session-ticket marker at the worktree top-level. git runs
74
+ // commit-msg from the worktree top-level (linked worktrees included), so
75
+ // process.cwd() is the right root here. Its own try/catch: a read error
76
+ // (marker absent, unreadable, etc.) must never skip the other rules below,
77
+ // it just leaves the Refs rule off.
78
+ function readTicket() {
79
+ try {
80
+ const markerPath = path.join(process.cwd(), '.trailhead', 'session-ticket');
81
+ return parseSessionTicket(fs.readFileSync(markerPath, 'utf8'));
82
+ } catch {
83
+ return null;
84
+ }
85
+ }
86
+
87
+ try {
88
+ const file = process.argv[2];
89
+ const raw = file ? fs.readFileSync(file, 'utf8') : '';
90
+ const region = messageRegion(raw);
91
+
92
+ if (CO_AUTHORED.test(region)) {
93
+ process.stderr.write('trailhead: do not add a Co-Authored-By trailer to commits. Remove it and commit again.\n');
94
+ process.exit(VIOLATION);
95
+ }
96
+ const subject = subjectOf(raw);
97
+ if (subject && !CONVENTIONAL.test(subject)) {
98
+ process.stderr.write(`trailhead: commit subject must be Conventional Commits: <type>(<scope>)?: <subject>. Valid types: ${TYPES.replace(/\|/g, ', ')}.\n`);
99
+ process.exit(VIOLATION);
100
+ }
101
+ if (subject.length > 72) {
102
+ process.stderr.write('trailhead: commit subject must be 72 characters or less.\n');
103
+ process.exit(VIOLATION);
104
+ }
105
+
106
+ const ticket = readTicket();
107
+ if (Number.isInteger(ticket) && ticket > 0 && !hasRefsFor(raw, ticket)) {
108
+ process.stderr.write(`trailhead: this commit is on ticket #${ticket} (per .trailhead/session-ticket) but has no "Refs: #${ticket}" trailer. Add "Refs: #${ticket}" as the last line of the message and commit again. If this commit is not part of ticket #${ticket}, remove the stale .trailhead/session-ticket marker first.\n`);
109
+ process.exit(VIOLATION);
110
+ }
111
+ } catch {
112
+ process.exit(0); // never wedge a commit on an unexpected error
113
+ }
114
+ process.exit(0);