@marcomigozzi/trailhead 0.10.0 → 0.11.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/README.it.md +2 -2
- package/README.md +2 -2
- package/bin/lib/install-verify.js +257 -0
- package/bin/lib/install-verify.test.js +379 -0
- package/bin/trailhead.js +80 -1
- package/bin/trailhead.test.js +390 -2
- package/package.json +1 -1
- package/plugins/trailhead/.claude-plugin/plugin.json +1 -1
- package/plugins/trailhead/hooks/lib/commit-message-check.js +58 -2
- package/plugins/trailhead/hooks/lib/commit-message-check.test.js +103 -13
- package/plugins/trailhead/hooks/lib/commit-msg-sync.test.js +375 -0
- package/plugins/trailhead/hooks/lib/fixtures/commit-msg-a319156 +57 -0
- package/plugins/trailhead/hooks/trailhead-secret-read-guard.js +32 -3
- package/plugins/trailhead/hooks/trailhead-secret-read-guard.test.js +41 -0
- package/plugins/trailhead/skills/_shared/configuration-reference.md +5 -5
- package/plugins/trailhead/skills/_shared/principles.md +1 -1
- package/plugins/trailhead/skills/_shared/substrate-commands.md +1 -1
- package/plugins/trailhead/skills/_shared/teamwork.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques/code-review.md +15 -3
- package/plugins/trailhead/skills/_shared/techniques/verify.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques.md +1 -1
- package/plugins/trailhead/skills/trailhead-work/SKILL.md +3 -2
- package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +6 -6
- package/plugins/trailhead/templates/trailhead-commit-msg +49 -2
- package/plugins/trailhead/templates/trailhead-commit-msg-sync.js +171 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// trailhead commit-msg hook sync (self-contained, ships in templates/ so it
|
|
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
|
|
9
|
+
// (non-trailhead) hook. #185.
|
|
10
|
+
//
|
|
11
|
+
// CLI: node trailhead-commit-msg-sync.js [--repo <dir>] (default cwd)
|
|
12
|
+
// Prints exactly one JSON line: {"status","hook","message"}.
|
|
13
|
+
// status: installed | upgraded | current | foreign | skipped
|
|
14
|
+
// hook: absolute path to the hook file, or null
|
|
15
|
+
// Exit 0 on every normal outcome (including "not a repo" and any unexpected
|
|
16
|
+
// error: this must never block the caller); exit 2 only on a usage error
|
|
17
|
+
// (unknown flag, or --repo with no value).
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const crypto = require('crypto');
|
|
22
|
+
const { execFileSync } = require('child_process');
|
|
23
|
+
|
|
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
|
+
|
|
28
|
+
function isTrailheadHook(text) {
|
|
29
|
+
const lines = String(text == null ? '' : text).split('\n').slice(0, 5);
|
|
30
|
+
return lines.some((line) => line.trim().startsWith(TRAILHEAD_MARK));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Uniforma i fine riga per il confronto di contenuto: CRLF -> LF, non tocca
|
|
34
|
+
// il file su disco (usato solo per decidere current vs upgraded).
|
|
35
|
+
function normalize(text) {
|
|
36
|
+
return String(text == null ? '' : text).replace(/\r\n/g, '\n');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// stdio 'pipe' su tutti e tre i canali: un git fallito (es. "not a repo") non
|
|
40
|
+
// deve stampare rumore sul terminale dell'invocante, solo far fallire questa
|
|
41
|
+
// chiamata (il chiamante decide come reagire).
|
|
42
|
+
function defaultRunGit(args, cwd) {
|
|
43
|
+
return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Risolve la hooksDir effettiva del repo (rispetta core.hooksPath e i
|
|
47
|
+
// worktree collegati: `git rev-parse --git-path hooks` la restituisce già
|
|
48
|
+
// corretta, eventualmente relativa a cwd). { skip: 'no-repo' } se repoDir non
|
|
49
|
+
// è un repo git.
|
|
50
|
+
function resolveHooksTarget({ repoDir, runGit = defaultRunGit }) {
|
|
51
|
+
try {
|
|
52
|
+
runGit(['rev-parse', '--git-dir'], repoDir);
|
|
53
|
+
} catch {
|
|
54
|
+
return { skip: 'no-repo' };
|
|
55
|
+
}
|
|
56
|
+
const hooksPathOut = runGit(['rev-parse', '--git-path', 'hooks'], repoDir);
|
|
57
|
+
const hooksDir = path.resolve(repoDir, hooksPathOut);
|
|
58
|
+
let hooksPathSet = false;
|
|
59
|
+
try {
|
|
60
|
+
const value = runGit(['config', '--get', 'core.hooksPath'], repoDir);
|
|
61
|
+
hooksPathSet = Boolean(value && value.trim());
|
|
62
|
+
} catch {
|
|
63
|
+
hooksPathSet = false;
|
|
64
|
+
}
|
|
65
|
+
return { hooksDir, hooksPathSet };
|
|
66
|
+
}
|
|
67
|
+
|
|
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 }) {
|
|
71
|
+
const hookPath = path.join(hooksDir, 'commit-msg');
|
|
72
|
+
|
|
73
|
+
if (!fsMod.existsSync(hookPath)) {
|
|
74
|
+
if (hooksPathSet) {
|
|
75
|
+
return {
|
|
76
|
+
status: 'skipped',
|
|
77
|
+
hook: null,
|
|
78
|
+
message: 'core.hooksPath is set and has no commit-msg; trailhead does not add files there, copy the template yourself.',
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
fsMod.mkdirSync(hooksDir, { recursive: true });
|
|
82
|
+
fsMod.writeFileSync(hookPath, templateText);
|
|
83
|
+
fsMod.chmodSync(hookPath, 0o755);
|
|
84
|
+
return { status: 'installed', hook: hookPath, message: 'installed the trailhead commit-msg hook.' };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const stat = fsMod.lstatSync(hookPath);
|
|
88
|
+
if (stat.isSymbolicLink()) {
|
|
89
|
+
return { status: 'foreign', hook: hookPath, message: 'a foreign commit-msg hook (symlink) is present; left untouched.' };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const existingText = fsMod.readFileSync(hookPath, 'utf8');
|
|
93
|
+
if (!isTrailheadHook(existingText)) {
|
|
94
|
+
return { status: 'foreign', hook: hookPath, message: 'a foreign commit-msg hook is present; left untouched.' };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (normalize(existingText) === normalize(templateText)) {
|
|
98
|
+
if ((stat.mode & 0o777) !== 0o755) fsMod.chmodSync(hookPath, 0o755);
|
|
99
|
+
return { status: 'current', hook: hookPath, message: 'the trailhead commit-msg hook is already current.' };
|
|
100
|
+
}
|
|
101
|
+
|
|
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
|
+
}
|
|
113
|
+
return { status: 'upgraded', hook: hookPath, message: 'upgraded the trailhead commit-msg hook to the current template.' };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function parseArgs(argv) {
|
|
117
|
+
let repo = null;
|
|
118
|
+
for (let i = 0; i < argv.length; i++) {
|
|
119
|
+
const arg = argv[i];
|
|
120
|
+
if (arg === '--repo') {
|
|
121
|
+
i++;
|
|
122
|
+
if (i >= argv.length) return { error: 'usage: --repo requires a value' };
|
|
123
|
+
repo = argv[i];
|
|
124
|
+
} else {
|
|
125
|
+
return { error: `usage: unknown argument: ${arg}` };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return { repo };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function writeResult(stdout, result) {
|
|
132
|
+
stdout.write(JSON.stringify(result) + '\n');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function main(argv, { cwd, stdout } = {}) {
|
|
136
|
+
const cwd_ = cwd || process.cwd();
|
|
137
|
+
const out = stdout || process.stdout;
|
|
138
|
+
|
|
139
|
+
const parsed = parseArgs(argv || []);
|
|
140
|
+
if (parsed.error) {
|
|
141
|
+
process.stderr.write(parsed.error + '\n');
|
|
142
|
+
return 2;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const repoDir = parsed.repo ? path.resolve(cwd_, parsed.repo) : cwd_;
|
|
146
|
+
let hooksDirForError = null;
|
|
147
|
+
try {
|
|
148
|
+
const target = resolveHooksTarget({ repoDir });
|
|
149
|
+
if (target.skip) {
|
|
150
|
+
writeResult(out, { status: 'skipped', hook: null, message: 'not a git repository; nothing to sync.' });
|
|
151
|
+
return 0;
|
|
152
|
+
}
|
|
153
|
+
hooksDirForError = target.hooksDir;
|
|
154
|
+
const templatePath = path.join(__dirname, 'trailhead-commit-msg');
|
|
155
|
+
const templateText = fs.readFileSync(templatePath, 'utf8');
|
|
156
|
+
const result = syncCommitMsgHook({ hooksDir: target.hooksDir, templateText, hooksPathSet: target.hooksPathSet, fs });
|
|
157
|
+
writeResult(out, result);
|
|
158
|
+
return 0;
|
|
159
|
+
} catch (err) {
|
|
160
|
+
const hookGuess = hooksDirForError ? path.join(hooksDirForError, 'commit-msg') : null;
|
|
161
|
+
writeResult(out, { status: 'skipped', hook: hookGuess, message: 'error: ' + (err && err.message ? err.message : String(err)) });
|
|
162
|
+
return 0;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
module.exports = { resolveHooksTarget, isTrailheadHook, normalize, syncCommitMsgHook, parseArgs, main };
|
|
167
|
+
|
|
168
|
+
if (require.main === module) {
|
|
169
|
+
const code = main(process.argv.slice(2), { cwd: process.cwd(), stdout: process.stdout });
|
|
170
|
+
process.exit(code);
|
|
171
|
+
}
|