hypomnema 1.3.0 → 1.3.2
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +2 -2
- package/README.ko.md +12 -10
- package/README.md +12 -10
- package/commands/crystallize.md +8 -8
- package/commands/feedback.md +1 -1
- package/commands/resume.md +1 -1
- package/commands/upgrade.md +2 -0
- package/docs/ARCHITECTURE.md +3 -3
- package/docs/CONTRIBUTING.md +2 -2
- package/hooks/hypo-auto-minimal-crystallize.mjs +57 -11
- package/hooks/hypo-compact-guard.mjs +1 -1
- package/hooks/hypo-cwd-change.mjs +1 -1
- package/hooks/hypo-first-prompt.mjs +2 -2
- package/hooks/hypo-hot-rebuild.mjs +14 -1
- package/hooks/hypo-personal-check.mjs +91 -179
- package/hooks/hypo-pre-commit.mjs +1 -1
- package/hooks/hypo-session-end.mjs +1 -1
- package/hooks/hypo-session-start.mjs +26 -19
- package/hooks/hypo-shared.mjs +839 -58
- package/hooks/hypo-web-fetch-ingest.mjs +2 -2
- package/hooks/version-check-fetch.mjs +2 -8
- package/hooks/version-check.mjs +18 -0
- package/package.json +3 -2
- package/scripts/check-tracker-ids.mjs +329 -0
- package/scripts/crystallize.mjs +249 -109
- package/scripts/doctor.mjs +6 -9
- package/scripts/feedback-sync.mjs +1 -1
- package/scripts/init.mjs +1 -1
- package/scripts/install-git-hooks.mjs +75 -40
- package/scripts/lib/check-tracker-ids.mjs +140 -0
- package/scripts/lib/design-history-stale.mjs +59 -14
- package/scripts/lib/extensions.mjs +4 -4
- package/scripts/lib/fix-manifest.mjs +2 -2
- package/scripts/lib/fix-status-verify.mjs +5 -4
- package/scripts/lib/plugin-detect.mjs +60 -0
- package/scripts/lib/project-create.mjs +1 -1
- package/scripts/lint.mjs +63 -8
- package/scripts/rename.mjs +373 -0
- package/scripts/resume.mjs +127 -13
- package/scripts/smoke-pack.mjs +23 -2
- package/scripts/uninstall.mjs +1 -1
- package/scripts/upgrade.mjs +266 -47
- package/skills/crystallize/SKILL.md +10 -7
- package/templates/SCHEMA.md +2 -2
- package/templates/hypo-config.md +2 -2
- package/templates/hypo-guide.md +20 -1
- package/templates/projects/_template/index.md +1 -1
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* hypo-web-fetch-ingest.mjs — PostToolUse hook
|
|
3
|
+
* hypo-web-fetch-ingest.mjs — PostToolUse hook
|
|
4
4
|
*
|
|
5
5
|
* When the LLM uses WebFetch or WebSearch, nudge it to follow §8.4 path A:
|
|
6
6
|
* "external research → /hypo:ingest into sources/". This hook produces a
|
|
7
7
|
* *signal*, not an automatic ingest call — per spec §5.4.6 Q-5.4.6:
|
|
8
|
-
* "WebFetch/WebSearch 자동 ingest 시그널은 별도 hook
|
|
8
|
+
* "WebFetch/WebSearch 자동 ingest 시그널은 별도 hook으로 분리."
|
|
9
9
|
*
|
|
10
10
|
* Why signal-only (not direct ingest):
|
|
11
11
|
* - slug derivation is LLM-driven (commands/ingest.md), so a hook cannot
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* Usage: node version-check-fetch.mjs [cachePath]
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { defaultCachePath, mergeLatest } from './version-check.mjs';
|
|
17
|
+
import { defaultCachePath, mergeLatest, selectPluginVersion } from './version-check.mjs';
|
|
18
18
|
|
|
19
19
|
const NPM_URL = 'https://registry.npmjs.org/hypomnema/latest';
|
|
20
20
|
const PLUGIN_URL =
|
|
@@ -42,13 +42,7 @@ async function fetchNpmLatest() {
|
|
|
42
42
|
|
|
43
43
|
async function fetchPluginLatest() {
|
|
44
44
|
const data = await fetchJson(PLUGIN_URL);
|
|
45
|
-
|
|
46
|
-
// Select by name rather than plugins[0]: a future marketplace.json could list
|
|
47
|
-
// more than one plugin or reorder entries, which would otherwise read the
|
|
48
|
-
// wrong version.
|
|
49
|
-
const entry = data.plugins.find((p) => p && p.name === 'hypomnema');
|
|
50
|
-
const v = entry && entry.version;
|
|
51
|
-
return typeof v === 'string' ? v : null;
|
|
45
|
+
return selectPluginVersion(data && data.plugins);
|
|
52
46
|
}
|
|
53
47
|
|
|
54
48
|
async function main() {
|
package/hooks/version-check.mjs
CHANGED
|
@@ -138,6 +138,24 @@ export function detectChannel(pkgRoot) {
|
|
|
138
138
|
return 'unknown';
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Pick the plugin's published version out of a marketplace.json `plugins` array.
|
|
143
|
+
* Select by name rather than plugins[0]: the file could list more than one
|
|
144
|
+
* plugin or reorder entries. Accept the current name (`hypo`) and the legacy one
|
|
145
|
+
* (`hypomnema`) so a stale-cached or transitional marketplace.json still
|
|
146
|
+
* resolves. Returns the version string or null.
|
|
147
|
+
*/
|
|
148
|
+
export function selectPluginVersion(plugins) {
|
|
149
|
+
if (!Array.isArray(plugins)) return null;
|
|
150
|
+
// Prefer the current name over the legacy one so that a transitional
|
|
151
|
+
// marketplace.json listing BOTH aliases resolves to `hypo` regardless of the
|
|
152
|
+
// entries' order (a legacy-first list must not shadow a newer `hypo` entry).
|
|
153
|
+
const entry =
|
|
154
|
+
plugins.find((p) => p && p.name === 'hypo') || plugins.find((p) => p && p.name === 'hypomnema');
|
|
155
|
+
const v = entry && entry.version;
|
|
156
|
+
return typeof v === 'string' ? v : null;
|
|
157
|
+
}
|
|
158
|
+
|
|
141
159
|
/** Channel-specific one-line update instruction. */
|
|
142
160
|
export function buildUpdateLine(channel, current, latest) {
|
|
143
161
|
const head = `[Hypomnema] Update available! ${current} → ${latest}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hypomnema",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.2",
|
|
4
4
|
"description": "LLM-native personal wiki system for Claude Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -43,10 +43,11 @@
|
|
|
43
43
|
"graph": "node scripts/graph.mjs",
|
|
44
44
|
"smoke-pack": "node scripts/smoke-pack.mjs",
|
|
45
45
|
"check:bilingual": "node scripts/check-bilingual.mjs --changelog",
|
|
46
|
+
"check:tracker-ids": "node scripts/check-tracker-ids.mjs --all",
|
|
46
47
|
"format": "prettier --write .",
|
|
47
48
|
"format:check": "prettier --check .",
|
|
48
49
|
"prepare": "node scripts/install-git-hooks.mjs",
|
|
49
|
-
"prepublishOnly": "npm test && npm run lint && npm run smoke-pack && npm run check:bilingual"
|
|
50
|
+
"prepublishOnly": "npm test && npm run lint && npm run smoke-pack && npm run check:bilingual && npm run check:tracker-ids"
|
|
50
51
|
},
|
|
51
52
|
"devDependencies": {
|
|
52
53
|
"prettier": "^3.8.3"
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* check-tracker-ids.mjs — CLI gate: no wiki-internal tracker IDs (ISSUE-N,
|
|
4
|
+
* fix #N) in OSS-public artifacts.
|
|
5
|
+
*
|
|
6
|
+
* Rule source: CLAUDE.md learned_behaviors (no-internal-tracker-ids-in-oss-
|
|
7
|
+
* artifacts, 2026-06-09). The repo ships through npm + a Claude Code plugin;
|
|
8
|
+
* a `fix #N` / `ISSUE-N` in any shipped file or in README/CHANGELOG is a
|
|
9
|
+
* dangling pointer into the maintainer's PRIVATE wiki tracker. GitHub refs
|
|
10
|
+
* (`PR #N`, `(#N)`, `#N`, issue URLs) are legitimate and never flagged.
|
|
11
|
+
*
|
|
12
|
+
* Modes:
|
|
13
|
+
* --all (default) Walk the public-artifact scope and scan every text
|
|
14
|
+
* file. Wired into npm `prepublishOnly` and CI.
|
|
15
|
+
* --staged Scan the STAGED blob of every in-scope file in the
|
|
16
|
+
* index (git show :path), mirroring the pre-commit
|
|
17
|
+
* formatter's name-status/-z/diff-filter plumbing so
|
|
18
|
+
* deletes, type-changes, symlinks and submodules are
|
|
19
|
+
* skipped. Used by the pre-commit hook.
|
|
20
|
+
* --commit-msg <file> Scan a commit message. Drops the --verbose scissors
|
|
21
|
+
* diff, then strips comment lines ONLY when git added
|
|
22
|
+
* its template (editor/strip mode); for `commit -m` /
|
|
23
|
+
* whitespace / verbatim (no template) it scans comment
|
|
24
|
+
* lines too, since git keeps them. Used by the
|
|
25
|
+
* commit-msg hook.
|
|
26
|
+
* --json Machine-readable output.
|
|
27
|
+
*
|
|
28
|
+
* Exit: 0 clean · 1 violations · 2 usage error.
|
|
29
|
+
*
|
|
30
|
+
* Scope (public): README.md, README.ko.md, CHANGELOG.md, package.json (npm
|
|
31
|
+
* auto-ships it) + shipped trees commands/ hooks/ scripts/ skills/ templates/
|
|
32
|
+
* docs/ .github/ .claude-plugin/. Excluded: tests/ and qa-runs/ (internal
|
|
33
|
+
* maintainer artifacts — a tracker id in a test description aids traceability
|
|
34
|
+
* and never reaches an installed user), node_modules/, .git/. The checker's OWN
|
|
35
|
+
* sources are scanned (NOT exempt); their examples use `N` placeholders so they
|
|
36
|
+
* stay clean.
|
|
37
|
+
*
|
|
38
|
+
* ADR scope (narrower): README.md, README.ko.md, and docs/ additionally block
|
|
39
|
+
* `ADR NNNN` / `decisions/NNNN` wiki-ADR pointers (USER_FACING_PATTERNS). Shipped
|
|
40
|
+
* code comments and CHANGELOG history keep their ADR anchors, so this set is
|
|
41
|
+
* applied per-file via patternsFor() only inside that doc surface.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
|
|
45
|
+
import { spawnSync } from 'node:child_process';
|
|
46
|
+
import { fileURLToPath } from 'node:url';
|
|
47
|
+
import { dirname, join, relative, sep, extname } from 'node:path';
|
|
48
|
+
import {
|
|
49
|
+
scanText,
|
|
50
|
+
stripScissors,
|
|
51
|
+
messageHasGitTemplate,
|
|
52
|
+
BLOCKED_PATTERNS,
|
|
53
|
+
USER_FACING_PATTERNS,
|
|
54
|
+
} from './lib/check-tracker-ids.mjs';
|
|
55
|
+
import {
|
|
56
|
+
parseNameStatus,
|
|
57
|
+
parseLsFilesStage,
|
|
58
|
+
filterRegularFiles,
|
|
59
|
+
} from './lib/pre-commit-format.mjs';
|
|
60
|
+
|
|
61
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
62
|
+
// Root resolves from this script's location (so --staged always gates the real
|
|
63
|
+
// Hypomnema index regardless of cwd). CHECK_TRACKER_ROOT is a TEST-ONLY seam;
|
|
64
|
+
// the installed git hooks `unset CHECK_TRACKER_ROOT` before invoking (see
|
|
65
|
+
// install-git-hooks.mjs gateLines) so an inherited/hostile value cannot redirect
|
|
66
|
+
// the real gate. Read-only either way (no writes), so it only changes what is
|
|
67
|
+
// scanned, never what is written.
|
|
68
|
+
const REPO_ROOT = process.env.CHECK_TRACKER_ROOT || join(__dirname, '..');
|
|
69
|
+
|
|
70
|
+
const RULE_REF =
|
|
71
|
+
'Rule: CLAUDE.md learned_behaviors (no-internal-tracker-ids-in-oss-artifacts). ' +
|
|
72
|
+
'OSS-public artifacts must not reference the private wiki tracker (ISSUE-N / fix #N). ' +
|
|
73
|
+
'Use a GitHub ref (#N / PR #N) or drop the reference.';
|
|
74
|
+
|
|
75
|
+
// Top-level public-artifact entry points. Files are scanned directly; dirs are
|
|
76
|
+
// walked recursively. Anything outside this set is out of scope. package.json is
|
|
77
|
+
// here because npm auto-ships it (it is not in the `files` allowlist but is
|
|
78
|
+
// always packed), so a tracker id in its metadata is a public leak too.
|
|
79
|
+
const SCOPE_FILES = ['README.md', 'README.ko.md', 'CHANGELOG.md', 'package.json'];
|
|
80
|
+
const SCOPE_DIRS = [
|
|
81
|
+
'commands',
|
|
82
|
+
'hooks',
|
|
83
|
+
'scripts',
|
|
84
|
+
'skills',
|
|
85
|
+
'templates',
|
|
86
|
+
'docs',
|
|
87
|
+
'.github',
|
|
88
|
+
'.claude-plugin',
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
// Never scanned even when under a scope dir. tests/ and qa-runs/ are
|
|
92
|
+
// internal-maintainer artifacts (a tracker id in a test name aids traceability
|
|
93
|
+
// and never reaches an installed user). The checker's OWN sources are NOT
|
|
94
|
+
// excluded — they use `N` placeholders in their examples so they scan clean.
|
|
95
|
+
const EXCLUDED_DIRS = new Set(['node_modules', '.git', 'tests', 'qa-runs']);
|
|
96
|
+
const EXCLUDED_FILES = new Set();
|
|
97
|
+
|
|
98
|
+
// Only text artifacts. Binary / lockfiles add noise and never carry prose refs.
|
|
99
|
+
const TEXT_EXT = new Set(['.md', '.mjs', '.js', '.cjs', '.json', '.yml', '.yaml', '.sh', '.txt']);
|
|
100
|
+
|
|
101
|
+
function isExcludedRel(relPath) {
|
|
102
|
+
const parts = relPath.split(sep);
|
|
103
|
+
if (parts.some((p) => EXCLUDED_DIRS.has(p))) return true;
|
|
104
|
+
if (EXCLUDED_FILES.has(relPath)) return true;
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isTextFile(p) {
|
|
109
|
+
return TEXT_EXT.has(extname(p).toLowerCase());
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Single public-scope predicate shared by --all and --staged so the two modes
|
|
113
|
+
// can never disagree on what counts as a public artifact (a root file like
|
|
114
|
+
// `foo.md` is out of scope for both; `package.json` and scope-dir files are in).
|
|
115
|
+
function isInScope(relPath) {
|
|
116
|
+
if (isExcludedRel(relPath)) return false;
|
|
117
|
+
if (!isTextFile(relPath)) return false;
|
|
118
|
+
if (SCOPE_FILES.includes(relPath)) return true;
|
|
119
|
+
return SCOPE_DIRS.includes(relPath.split(sep)[0]);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// User-facing prose surface: README.md, README.ko.md, and the docs/ tree. These
|
|
123
|
+
// additionally block `ADR NNNN` / `decisions/NNNN` (dangling wiki-ADR pointers).
|
|
124
|
+
// Everything else in scope — shipped code, CHANGELOG, package.json — keeps its ADR
|
|
125
|
+
// anchors, so the broader pattern set is applied here ONLY.
|
|
126
|
+
const USER_FACING_DOCS = new Set(['README.md', 'README.ko.md']);
|
|
127
|
+
function isUserFacingDoc(relPath) {
|
|
128
|
+
return USER_FACING_DOCS.has(relPath) || relPath.split(sep)[0] === 'docs';
|
|
129
|
+
}
|
|
130
|
+
function patternsFor(relPath) {
|
|
131
|
+
return isUserFacingDoc(relPath)
|
|
132
|
+
? [...BLOCKED_PATTERNS, ...USER_FACING_PATTERNS]
|
|
133
|
+
: BLOCKED_PATTERNS;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Recursively collect in-scope text files under an absolute dir.
|
|
137
|
+
function walk(absDir, acc) {
|
|
138
|
+
let entries;
|
|
139
|
+
try {
|
|
140
|
+
entries = readdirSync(absDir, { withFileTypes: true });
|
|
141
|
+
} catch {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
for (const ent of entries) {
|
|
145
|
+
const abs = join(absDir, ent.name);
|
|
146
|
+
const rel = relative(REPO_ROOT, abs);
|
|
147
|
+
if (isExcludedRel(rel)) continue;
|
|
148
|
+
if (ent.isDirectory()) {
|
|
149
|
+
walk(abs, acc);
|
|
150
|
+
} else if (ent.isFile() && isTextFile(abs)) {
|
|
151
|
+
acc.push(rel);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function collectAllScopeFiles() {
|
|
157
|
+
const files = [];
|
|
158
|
+
for (const f of SCOPE_FILES) {
|
|
159
|
+
if (existsSync(join(REPO_ROOT, f))) files.push(f);
|
|
160
|
+
}
|
|
161
|
+
for (const d of SCOPE_DIRS) {
|
|
162
|
+
walk(join(REPO_ROOT, d), files);
|
|
163
|
+
}
|
|
164
|
+
return files;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── violation reporting ──────────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
function report(violations, json) {
|
|
170
|
+
if (json) {
|
|
171
|
+
process.stdout.write(
|
|
172
|
+
JSON.stringify(
|
|
173
|
+
{
|
|
174
|
+
ok: violations.length === 0,
|
|
175
|
+
count: violations.length,
|
|
176
|
+
violations,
|
|
177
|
+
},
|
|
178
|
+
null,
|
|
179
|
+
2,
|
|
180
|
+
) + '\n',
|
|
181
|
+
);
|
|
182
|
+
} else if (violations.length === 0) {
|
|
183
|
+
process.stdout.write('[check-tracker-ids] OK: no wiki-internal tracker ids found.\n');
|
|
184
|
+
} else {
|
|
185
|
+
process.stderr.write(
|
|
186
|
+
`[check-tracker-ids] FAIL: ${violations.length} tracker-id reference(s):\n`,
|
|
187
|
+
);
|
|
188
|
+
for (const v of violations) {
|
|
189
|
+
process.stderr.write(
|
|
190
|
+
` ${v.file}:${v.line}:${v.col} ${v.match} (${v.label})\n` +
|
|
191
|
+
` ${v.lineText.trim()}\n`,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
process.stderr.write(`${RULE_REF}\n`);
|
|
195
|
+
}
|
|
196
|
+
return violations.length === 0 ? 0 : 1;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ── git helpers (scoped env not required: read-only, no GIT_DIR attack surface
|
|
200
|
+
// here — the hook shim already pins identity before invoking) ──────────────
|
|
201
|
+
|
|
202
|
+
function git(args, opts = {}) {
|
|
203
|
+
return spawnSync('git', args, { cwd: REPO_ROOT, encoding: 'utf-8', ...opts });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ── modes ────────────────────────────────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
function runAll(json) {
|
|
209
|
+
const files = collectAllScopeFiles();
|
|
210
|
+
const violations = [];
|
|
211
|
+
for (const rel of files) {
|
|
212
|
+
let text;
|
|
213
|
+
try {
|
|
214
|
+
text = readFileSync(join(REPO_ROOT, rel), 'utf-8');
|
|
215
|
+
} catch {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
for (const h of scanText(text, patternsFor(rel))) violations.push({ file: rel, ...h });
|
|
219
|
+
}
|
|
220
|
+
process.exit(report(violations, json));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function runStaged(json) {
|
|
224
|
+
const diff = git(['diff', '--cached', '--name-status', '-z', '--diff-filter=ACMR', '--']);
|
|
225
|
+
if (diff.status !== 0) {
|
|
226
|
+
// Can't read the index → fail OPEN (don't block a commit on a git probe
|
|
227
|
+
// failure); the --all CI gate is the hard backstop.
|
|
228
|
+
process.stdout.write(
|
|
229
|
+
'[check-tracker-ids] staged: git diff failed; skipping (CI is the backstop).\n',
|
|
230
|
+
);
|
|
231
|
+
process.exit(0);
|
|
232
|
+
}
|
|
233
|
+
const staged = parseNameStatus(diff.stdout || '');
|
|
234
|
+
// Restrict to in-scope public artifacts (same predicate --all uses).
|
|
235
|
+
const inScope = staged.filter((e) => isInScope(e.path));
|
|
236
|
+
if (!inScope.length) {
|
|
237
|
+
if (json) report([], json);
|
|
238
|
+
process.exit(0);
|
|
239
|
+
}
|
|
240
|
+
// Drop symlinks / submodules via the staged mode bits.
|
|
241
|
+
const ls = git(['ls-files', '--stage', '-z', '--', ...inScope.map((e) => e.path)]);
|
|
242
|
+
let regular = inScope;
|
|
243
|
+
if (ls.status === 0) {
|
|
244
|
+
regular = filterRegularFiles(inScope, parseLsFilesStage(ls.stdout || ''));
|
|
245
|
+
}
|
|
246
|
+
const violations = [];
|
|
247
|
+
for (const e of regular) {
|
|
248
|
+
// Scan the STAGED blob, not the working tree — only what is actually being
|
|
249
|
+
// committed is gated (partial stage safety).
|
|
250
|
+
const show = git(['show', `:${e.path}`]);
|
|
251
|
+
if (show.status !== 0) continue;
|
|
252
|
+
for (const h of scanText(show.stdout || '', patternsFor(e.path)))
|
|
253
|
+
violations.push({ file: e.path, ...h });
|
|
254
|
+
}
|
|
255
|
+
process.exit(report(violations, json));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function runCommitMsg(file, json) {
|
|
259
|
+
let raw;
|
|
260
|
+
try {
|
|
261
|
+
raw = readFileSync(file, 'utf-8');
|
|
262
|
+
} catch (err) {
|
|
263
|
+
process.stderr.write(
|
|
264
|
+
`[check-tracker-ids] cannot read commit-msg file ${file}: ${err.message}\n`,
|
|
265
|
+
);
|
|
266
|
+
process.exit(2);
|
|
267
|
+
}
|
|
268
|
+
// Decide comment/scissors handling by whether git added its editor template
|
|
269
|
+
// (NOT by config, which the hook can't fully resolve). Two branches:
|
|
270
|
+
//
|
|
271
|
+
// template present (editor commit) → git honors the --verbose scissors AND,
|
|
272
|
+
// in the default cleanup, strips `#` comment lines. Replicate: cut the
|
|
273
|
+
// scissors diff, then `stripspace --strip-comments`. This matches what git
|
|
274
|
+
// records and avoids a false-positive on the template's own branch/file
|
|
275
|
+
// names (e.g. a ticket-numbered branch in `# On branch ...`).
|
|
276
|
+
//
|
|
277
|
+
// template absent (`git commit -m` / `-F` / whitespace / verbatim) → git
|
|
278
|
+
// keeps `#` lines and does NOT treat a bare `>8` line as scissors, so scan
|
|
279
|
+
// the message VERBATIM. This is what closes both -m gaps (a `#`-prefixed
|
|
280
|
+
// tracker id, and content after a user-written `>8` line).
|
|
281
|
+
//
|
|
282
|
+
// KNOWN LIMITATION (documented, exotic): an EDITOR commit run with an explicit
|
|
283
|
+
// non-default `--cleanup=whitespace`/`verbatim` does carry a template yet keeps
|
|
284
|
+
// `#` lines, so a tracker id sitting in a comment line there is not caught.
|
|
285
|
+
// This needs a non-default flag AND a `#`-prefixed id; the prose path (the real
|
|
286
|
+
// risk) is always caught, and the file gate (--all/--staged/CI) is the hard
|
|
287
|
+
// guarantee. commit-msg is best-effort.
|
|
288
|
+
let text;
|
|
289
|
+
if (messageHasGitTemplate(raw)) {
|
|
290
|
+
const noScissors = stripScissors(raw);
|
|
291
|
+
const strip = git(['stripspace', '--strip-comments'], { input: noScissors });
|
|
292
|
+
text = strip.status === 0 ? strip.stdout || '' : noScissors;
|
|
293
|
+
} else {
|
|
294
|
+
text = raw;
|
|
295
|
+
}
|
|
296
|
+
const violations = scanText(text).map((h) => ({ file: relative(REPO_ROOT, file) || file, ...h }));
|
|
297
|
+
process.exit(report(violations, json));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ── arg parsing ────────────────────────────────────────────────────────────
|
|
301
|
+
|
|
302
|
+
function usage(code) {
|
|
303
|
+
process.stdout.write(
|
|
304
|
+
'Usage:\n' +
|
|
305
|
+
' node scripts/check-tracker-ids.mjs [--all] [--json]\n' +
|
|
306
|
+
' node scripts/check-tracker-ids.mjs --staged [--json]\n' +
|
|
307
|
+
' node scripts/check-tracker-ids.mjs --commit-msg <file> [--json]\n',
|
|
308
|
+
);
|
|
309
|
+
process.exit(code);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const argv = process.argv.slice(2);
|
|
313
|
+
if (argv.includes('--help') || argv.includes('-h')) usage(0);
|
|
314
|
+
const json = argv.includes('--json');
|
|
315
|
+
|
|
316
|
+
if (argv.includes('--commit-msg')) {
|
|
317
|
+
const i = argv.indexOf('--commit-msg');
|
|
318
|
+
const file = argv[i + 1];
|
|
319
|
+
if (!file || file.startsWith('--')) {
|
|
320
|
+
process.stderr.write('[check-tracker-ids] --commit-msg requires a file path\n');
|
|
321
|
+
usage(2);
|
|
322
|
+
}
|
|
323
|
+
runCommitMsg(file, json);
|
|
324
|
+
} else if (argv.includes('--staged')) {
|
|
325
|
+
runStaged(json);
|
|
326
|
+
} else {
|
|
327
|
+
// default + explicit --all
|
|
328
|
+
runAll(json);
|
|
329
|
+
}
|