speclock 5.5.6 → 5.5.7

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/src/core/hooks.js CHANGED
@@ -1,91 +1,109 @@
1
- // SpecLock Git Hook Management
2
- // Developed by Sandeep Roy (https://github.com/sgroy10)
3
-
4
- import fs from "fs";
5
- import path from "path";
6
-
7
- const HOOK_MARKER = "# SPECLOCK-HOOK";
8
-
9
- const HOOK_SCRIPT = `#!/bin/sh
10
- ${HOOK_MARKER} — Do not remove this line
11
- # SpecLock pre-commit hook: runs semantic audit of staged diff + commit message
12
- # against active locks. Unlike the legacy 'audit' subcommand, this one feeds
13
- # the actual diff content AND the commit message through the semantic conflict
14
- # engine — the same one used by 'speclock check'.
15
- # Install: npx speclock hook install
16
- # Remove: npx speclock hook remove
17
-
18
- npx speclock audit-semantic --pre-commit
19
- exit $?
20
- `;
21
-
22
- export function installHook(root) {
23
- const hooksDir = path.join(root, ".git", "hooks");
24
- if (!fs.existsSync(path.join(root, ".git"))) {
25
- return { success: false, error: "Not a git repository. Run 'git init' first." };
26
- }
27
-
28
- // Ensure hooks directory exists
29
- fs.mkdirSync(hooksDir, { recursive: true });
30
-
31
- const hookPath = path.join(hooksDir, "pre-commit");
32
-
33
- // Check if existing hook exists (not ours)
34
- if (fs.existsSync(hookPath)) {
35
- const existing = fs.readFileSync(hookPath, "utf-8");
36
- if (existing.includes(HOOK_MARKER)) {
37
- return { success: false, error: "SpecLock pre-commit hook is already installed." };
38
- }
39
- // Append to existing hook
40
- const appended = existing.trimEnd() + "\n\n" + HOOK_SCRIPT;
41
- fs.writeFileSync(hookPath, appended, { mode: 0o755 });
42
- return { success: true, message: "SpecLock hook appended to existing pre-commit hook." };
43
- }
44
-
45
- fs.writeFileSync(hookPath, HOOK_SCRIPT, { mode: 0o755 });
46
- return { success: true, message: "SpecLock pre-commit hook installed." };
47
- }
48
-
49
- export function removeHook(root) {
50
- const hookPath = path.join(root, ".git", "hooks", "pre-commit");
51
- if (!fs.existsSync(hookPath)) {
52
- return { success: false, error: "No pre-commit hook found." };
53
- }
54
-
55
- const content = fs.readFileSync(hookPath, "utf-8");
56
- if (!content.includes(HOOK_MARKER)) {
57
- return { success: false, error: "Pre-commit hook exists but was not installed by SpecLock." };
58
- }
59
-
60
- // Check if lines other than our speclock block exist
61
- const lines = content.split("\n");
62
- const nonSpeclockLines = lines.filter((line) => {
63
- const trimmed = line.trim();
64
- const lower = trimmed.toLowerCase();
65
- if (!trimmed || trimmed === "#!/bin/sh") return false;
66
- if (lower.includes("speclock")) return false;
67
- if (lower.includes("npx speclock")) return false;
68
- if (trimmed === "exit $?") return false;
69
- return true;
70
- });
71
-
72
- if (nonSpeclockLines.length === 0) {
73
- // Entire hook was ours — remove file
74
- fs.unlinkSync(hookPath);
75
- return { success: true, message: "SpecLock pre-commit hook removed." };
76
- }
77
-
78
- // Other hook content exists — remove our block, keep the rest
79
- const cleaned = content
80
- .replace(/\n*# SPECLOCK-HOOK[^\n]*\n.*?exit \$\?\n?/s, "\n")
81
- .trim();
82
- fs.writeFileSync(hookPath, cleaned + "\n", { mode: 0o755 });
83
- return { success: true, message: "SpecLock hook removed. Other hook content preserved." };
84
- }
85
-
86
- export function isHookInstalled(root) {
87
- const hookPath = path.join(root, ".git", "hooks", "pre-commit");
88
- if (!fs.existsSync(hookPath)) return false;
89
- const content = fs.readFileSync(hookPath, "utf-8");
90
- return content.includes(HOOK_MARKER);
91
- }
1
+ // SpecLock Git Hook Management
2
+ // Developed by Sandeep Roy (https://github.com/sgroy10)
3
+
4
+ import fs from "fs";
5
+ import path from "path";
6
+
7
+ const HOOK_MARKER = "# SPECLOCK-HOOK";
8
+
9
+ const HOOK_SCRIPT = `#!/bin/sh
10
+ ${HOOK_MARKER} — Do not remove this line
11
+ # SpecLock pre-commit hook: runs semantic audit of staged diff + commit message
12
+ # against active locks. Unlike the legacy 'audit' subcommand, this one feeds
13
+ # the actual diff content AND the commit message through the semantic conflict
14
+ # engine — the same one used by 'speclock check'.
15
+ # Install: npx speclock hook install
16
+ # Remove: npx speclock hook remove
17
+ #
18
+ # Enforcement mode precedence (first match wins):
19
+ # 1. SPECLOCK_STRICT=1 in the environment when git commit runs
20
+ # 2. brain.enforcement.mode === "hard" in .speclock/brain.json
21
+ # (set with: speclock enforce hard)
22
+ # 3. Default: warn mode — violations printed, commit allowed
23
+ #
24
+ # NOTE: Some git versions/shells sanitize the environment before running
25
+ # hooks, which can strip SPECLOCK_STRICT. The persistent brain mode set by
26
+ # 'speclock enforce hard' is the reliable way to enforce strict blocking.
27
+
28
+ # Explicitly export SPECLOCK_STRICT so it survives any sh -c subshells the
29
+ # CLI may spawn. If unset, leave it unset — the CLI will then fall back to
30
+ # reading brain.enforcement.mode from .speclock/brain.json.
31
+ if [ -n "\${SPECLOCK_STRICT:-}" ]; then
32
+ export SPECLOCK_STRICT
33
+ fi
34
+
35
+ # Marker so the CLI knows it's running inside the pre-commit hook.
36
+ export SPECLOCK_HOOK=1
37
+
38
+ npx speclock audit-semantic --pre-commit
39
+ exit $?
40
+ `;
41
+
42
+ export function installHook(root) {
43
+ const hooksDir = path.join(root, ".git", "hooks");
44
+ if (!fs.existsSync(path.join(root, ".git"))) {
45
+ return { success: false, error: "Not a git repository. Run 'git init' first." };
46
+ }
47
+
48
+ // Ensure hooks directory exists
49
+ fs.mkdirSync(hooksDir, { recursive: true });
50
+
51
+ const hookPath = path.join(hooksDir, "pre-commit");
52
+
53
+ // Check if existing hook exists (not ours)
54
+ if (fs.existsSync(hookPath)) {
55
+ const existing = fs.readFileSync(hookPath, "utf-8");
56
+ if (existing.includes(HOOK_MARKER)) {
57
+ return { success: false, error: "SpecLock pre-commit hook is already installed." };
58
+ }
59
+ // Append to existing hook
60
+ const appended = existing.trimEnd() + "\n\n" + HOOK_SCRIPT;
61
+ fs.writeFileSync(hookPath, appended, { mode: 0o755 });
62
+ return { success: true, message: "SpecLock hook appended to existing pre-commit hook." };
63
+ }
64
+
65
+ fs.writeFileSync(hookPath, HOOK_SCRIPT, { mode: 0o755 });
66
+ return { success: true, message: "SpecLock pre-commit hook installed." };
67
+ }
68
+
69
+ export function removeHook(root) {
70
+ const hookPath = path.join(root, ".git", "hooks", "pre-commit");
71
+ if (!fs.existsSync(hookPath)) {
72
+ return { success: false, error: "No pre-commit hook found." };
73
+ }
74
+
75
+ const content = fs.readFileSync(hookPath, "utf-8");
76
+ if (!content.includes(HOOK_MARKER)) {
77
+ return { success: false, error: "Pre-commit hook exists but was not installed by SpecLock." };
78
+ }
79
+
80
+ // Strip our block (from the SPECLOCK-HOOK marker through the trailing
81
+ // `exit $?` that terminates the script snippet). Everything inside the
82
+ // block is ours — comments, env exports, the npx invocation, etc.
83
+ const cleaned = content
84
+ .replace(/\n*# SPECLOCK-HOOK[^\n]*\n[\s\S]*?exit \$\?\n?/, "\n")
85
+ .trim();
86
+
87
+ // If nothing meaningful remains (just #!/bin/sh or empty), remove file.
88
+ const remaining = cleaned
89
+ .split("\n")
90
+ .map((l) => l.trim())
91
+ .filter((l) => l && l !== "#!/bin/sh")
92
+ .join("\n");
93
+
94
+ if (remaining.length === 0) {
95
+ fs.unlinkSync(hookPath);
96
+ return { success: true, message: "SpecLock pre-commit hook removed." };
97
+ }
98
+
99
+ // Other hook content exists — keep the rest.
100
+ fs.writeFileSync(hookPath, cleaned + "\n", { mode: 0o755 });
101
+ return { success: true, message: "SpecLock hook removed. Other hook content preserved." };
102
+ }
103
+
104
+ export function isHookInstalled(root) {
105
+ const hookPath = path.join(root, ".git", "hooks", "pre-commit");
106
+ if (!fs.existsSync(hookPath)) return false;
107
+ const content = fs.readFileSync(hookPath, "utf-8");
108
+ return content.includes(HOOK_MARKER);
109
+ }