kastell 2.2.3 → 2.2.5

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 (75) hide show
  1. package/.claude-plugin/marketplace.json +18 -18
  2. package/.claude-plugin/plugin.json +45 -39
  3. package/CHANGELOG.md +1294 -1266
  4. package/LICENSE +201 -201
  5. package/NOTICE +5 -5
  6. package/README.md +1 -1
  7. package/README.tr.md +1 -1
  8. package/bin/kastell +2 -2
  9. package/bin/kastell-mcp +5 -5
  10. package/dist/adapters/coolify.js +92 -92
  11. package/dist/adapters/dokploy.js +99 -99
  12. package/dist/core/audit/formatters/badge.js +20 -20
  13. package/dist/core/completions.js +631 -631
  14. package/dist/mcp/server.d.ts.map +1 -1
  15. package/dist/mcp/server.js +25 -31
  16. package/dist/mcp/server.js.map +1 -1
  17. package/dist/mcp/tools/serverExplain.d.ts.map +1 -1
  18. package/dist/mcp/tools/serverExplain.js.map +1 -1
  19. package/dist/mcp/tools/serverFleet.d.ts.map +1 -1
  20. package/dist/mcp/tools/serverFleet.js.map +1 -1
  21. package/dist/mcp/tools/serverInfo.d.ts +1 -1
  22. package/dist/mcp/tools/serverInfo.js +1 -1
  23. package/dist/mcp/tools/serverPlugin.d.ts.map +1 -1
  24. package/dist/mcp/tools/serverPlugin.js.map +1 -1
  25. package/dist/mcp-bundle.mjs +101015 -0
  26. package/dist/utils/cloudInit.js +58 -58
  27. package/dist/utils/version.d.ts.map +1 -1
  28. package/dist/utils/version.js +19 -4
  29. package/dist/utils/version.js.map +1 -1
  30. package/kastell-plugin/.claude-plugin/plugin.json +20 -20
  31. package/kastell-plugin/.mcp.json +15 -8
  32. package/kastell-plugin/README.md +113 -113
  33. package/kastell-plugin/agents/kastell-auditor.md +77 -77
  34. package/kastell-plugin/agents/scripts/bucket_mapper.sh +101 -101
  35. package/kastell-plugin/agents/scripts/trend_report.sh +91 -91
  36. package/kastell-plugin/hooks/destroy-block.cjs +31 -31
  37. package/kastell-plugin/hooks/hooks.json +57 -57
  38. package/kastell-plugin/hooks/pre-commit-audit-guard.cjs +75 -75
  39. package/kastell-plugin/hooks/session-audit.cjs +86 -86
  40. package/kastell-plugin/hooks/session-log.cjs +56 -56
  41. package/kastell-plugin/hooks/stop-quality-check.cjs +72 -72
  42. package/kastell-plugin/skills/kastell-careful/SKILL.md +64 -64
  43. package/kastell-plugin/skills/kastell-ops/SKILL.md +139 -139
  44. package/kastell-plugin/skills/kastell-ops/references/commands.md +45 -45
  45. package/kastell-plugin/skills/kastell-ops/references/mcp-tools.md +50 -50
  46. package/kastell-plugin/skills/kastell-ops/references/patterns.md +145 -145
  47. package/kastell-plugin/skills/kastell-ops/references/pitfalls.md +136 -136
  48. package/kastell-plugin/skills/kastell-ops/scripts/check_coverage.sh +101 -101
  49. package/kastell-plugin/skills/kastell-ops/scripts/fleet_report.sh +73 -73
  50. package/kastell-plugin/skills/kastell-ops/scripts/parse_audit.sh +76 -76
  51. package/kastell-plugin/skills/kastell-research/SKILL.md +90 -90
  52. package/kastell-plugin/skills/kastell-scaffold/SKILL.md +104 -104
  53. package/kastell-plugin/skills/kastell-scaffold/references/template-audit-check.md +150 -150
  54. package/kastell-plugin/skills/kastell-scaffold/references/template-command.md +80 -80
  55. package/kastell-plugin/skills/kastell-scaffold/references/template-mcp-tool.md +72 -72
  56. package/kastell-plugin/skills/kastell-scaffold/references/template-provider.md +67 -67
  57. package/kastell-plugin/skills/kastell-scaffold/scripts/scaffold.sh +180 -180
  58. package/kastell-plugin/skills/kastell-scaffold/templates/check-test.ts.tpl +27 -27
  59. package/kastell-plugin/skills/kastell-scaffold/templates/check.ts.tpl +50 -50
  60. package/kastell-plugin/skills/kastell-scaffold/templates/command-core.ts.tpl +18 -18
  61. package/kastell-plugin/skills/kastell-scaffold/templates/command-test.ts.tpl +17 -17
  62. package/kastell-plugin/skills/kastell-scaffold/templates/command.ts.tpl +25 -25
  63. package/kastell-plugin/skills/kastell-scaffold/templates/mcp-tool-test.ts.tpl +30 -30
  64. package/kastell-plugin/skills/kastell-scaffold/templates/mcp-tool.ts.tpl +29 -29
  65. package/kastell-plugin/skills/kastell-scaffold/templates/provider-test.ts.tpl +34 -34
  66. package/kastell-plugin/skills/kastell-scaffold/templates/provider.ts.tpl +32 -32
  67. package/package.json +125 -122
  68. package/dist/commands/interactive.d.ts +0 -11
  69. package/dist/commands/interactive.d.ts.map +0 -1
  70. package/dist/commands/interactive.js +0 -1079
  71. package/dist/commands/interactive.js.map +0 -1
  72. package/dist/core/lock.d.ts +0 -66
  73. package/dist/core/lock.d.ts.map +0 -1
  74. package/dist/core/lock.js +0 -556
  75. package/dist/core/lock.js.map +0 -1
@@ -1,75 +1,75 @@
1
- #!/usr/bin/env node
2
- // PreToolUse hook: Block git commit if audit score dropped since previous audit run
3
-
4
- const fs = require('fs');
5
- const path = require('path');
6
- const os = require('os');
7
-
8
- const HISTORY_FILE = path.join(os.homedir(), '.kastell', 'audit-history.json');
9
-
10
- // MANDATORY stdin guard — exit silently if stdin unavailable
11
- if (!process.stdin || process.stdin.destroyed || !process.stdin.readable) {
12
- process.exit(0);
13
- }
14
-
15
- let input = '';
16
- const stdinTimeout = setTimeout(() => process.exit(0), 1500);
17
- process.stdin.setEncoding('utf8');
18
- process.stdin.on('error', () => process.exit(0));
19
- process.stdin.on('data', chunk => { input += chunk; });
20
- process.stdin.on('end', () => {
21
- clearTimeout(stdinTimeout);
22
- try {
23
- const cwd = process.cwd();
24
-
25
- // Kastell project guard (two-phase: directory structure + package.json name)
26
- const isKastell = fs.existsSync(path.join(cwd, 'src', 'mcp')) &&
27
- fs.existsSync(path.join(cwd, 'package.json'));
28
- if (!isKastell) {
29
- try {
30
- const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
31
- if (pkg.name !== 'kastell') process.exit(0);
32
- } catch { process.exit(0); }
33
- }
34
-
35
- // Parse tool input — only act on git commit commands
36
- const data = JSON.parse(input);
37
- const cmd = (data.tool_input && data.tool_input.command) || '';
38
-
39
- if (!/\bgit\s+commit\b/.test(cmd)) {
40
- process.exit(0);
41
- }
42
-
43
- // Read audit history — fail-open if unavailable
44
- let history;
45
- try {
46
- history = JSON.parse(fs.readFileSync(HISTORY_FILE, 'utf8'));
47
- } catch {
48
- // History missing or unreadable — allow commit
49
- process.exit(0);
50
- }
51
-
52
- // Check each server for score regression between last two audit runs
53
- for (const serverIp of Object.keys(history)) {
54
- const entries = history[serverIp];
55
- if (!Array.isArray(entries) || entries.length < 2) continue;
56
-
57
- const current = entries[entries.length - 1].overallScore;
58
- const previous = entries[entries.length - 2].overallScore;
59
-
60
- if (typeof current !== 'number' || typeof previous !== 'number') continue;
61
- if (current < previous) {
62
- process.stdout.write(JSON.stringify({
63
- decision: 'block',
64
- reason: `Audit score dropped: ${previous} -> ${current} (${entries[entries.length - 1].serverName}). Run \`kastell audit\` to investigate before committing.`,
65
- }));
66
- process.exit(0);
67
- }
68
- }
69
-
70
- // No score drop detected — allow commit
71
- } catch {}
72
-
73
- // Fail-open: any unexpected error allows the commit through
74
- process.exit(0);
75
- });
1
+ #!/usr/bin/env node
2
+ // PreToolUse hook: Block git commit if audit score dropped since previous audit run
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const os = require('os');
7
+
8
+ const HISTORY_FILE = path.join(os.homedir(), '.kastell', 'audit-history.json');
9
+
10
+ // MANDATORY stdin guard — exit silently if stdin unavailable
11
+ if (!process.stdin || process.stdin.destroyed || !process.stdin.readable) {
12
+ process.exit(0);
13
+ }
14
+
15
+ let input = '';
16
+ const stdinTimeout = setTimeout(() => process.exit(0), 1500);
17
+ process.stdin.setEncoding('utf8');
18
+ process.stdin.on('error', () => process.exit(0));
19
+ process.stdin.on('data', chunk => { input += chunk; });
20
+ process.stdin.on('end', () => {
21
+ clearTimeout(stdinTimeout);
22
+ try {
23
+ const cwd = process.cwd();
24
+
25
+ // Kastell project guard (two-phase: directory structure + package.json name)
26
+ const isKastell = fs.existsSync(path.join(cwd, 'src', 'mcp')) &&
27
+ fs.existsSync(path.join(cwd, 'package.json'));
28
+ if (!isKastell) {
29
+ try {
30
+ const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
31
+ if (pkg.name !== 'kastell') process.exit(0);
32
+ } catch { process.exit(0); }
33
+ }
34
+
35
+ // Parse tool input — only act on git commit commands
36
+ const data = JSON.parse(input);
37
+ const cmd = (data.tool_input && data.tool_input.command) || '';
38
+
39
+ if (!/\bgit\s+commit\b/.test(cmd)) {
40
+ process.exit(0);
41
+ }
42
+
43
+ // Read audit history — fail-open if unavailable
44
+ let history;
45
+ try {
46
+ history = JSON.parse(fs.readFileSync(HISTORY_FILE, 'utf8'));
47
+ } catch {
48
+ // History missing or unreadable — allow commit
49
+ process.exit(0);
50
+ }
51
+
52
+ // Check each server for score regression between last two audit runs
53
+ for (const serverIp of Object.keys(history)) {
54
+ const entries = history[serverIp];
55
+ if (!Array.isArray(entries) || entries.length < 2) continue;
56
+
57
+ const current = entries[entries.length - 1].overallScore;
58
+ const previous = entries[entries.length - 2].overallScore;
59
+
60
+ if (typeof current !== 'number' || typeof previous !== 'number') continue;
61
+ if (current < previous) {
62
+ process.stdout.write(JSON.stringify({
63
+ decision: 'block',
64
+ reason: `Audit score dropped: ${previous} -> ${current} (${entries[entries.length - 1].serverName}). Run \`kastell audit\` to investigate before committing.`,
65
+ }));
66
+ process.exit(0);
67
+ }
68
+ }
69
+
70
+ // No score drop detected — allow commit
71
+ } catch {}
72
+
73
+ // Fail-open: any unexpected error allows the commit through
74
+ process.exit(0);
75
+ });
@@ -1,86 +1,86 @@
1
- #!/usr/bin/env node
2
- // SessionStart hook: Show last audit score from cache (no SSH, instant)
3
-
4
- const fs = require('fs');
5
- const path = require('path');
6
- const os = require('os');
7
-
8
- const HISTORY_FILE = path.join(os.homedir(), '.kastell', 'audit-history.json');
9
-
10
- // MANDATORY stdin guard — exit silently if stdin unavailable (e.g. after /clear)
11
- if (!process.stdin || process.stdin.destroyed || !process.stdin.readable) {
12
- process.exit(0);
13
- }
14
-
15
- let input = '';
16
- const stdinTimeout = setTimeout(() => process.exit(0), 1500);
17
- process.stdin.setEncoding('utf8');
18
- process.stdin.on('error', () => process.exit(0));
19
- process.stdin.on('data', chunk => { input += chunk; });
20
- process.stdin.on('end', () => {
21
- clearTimeout(stdinTimeout);
22
- try {
23
- const cwd = process.cwd();
24
-
25
- // Kastell project guard
26
- const isKastell = fs.existsSync(path.join(cwd, 'src', 'mcp')) &&
27
- fs.existsSync(path.join(cwd, 'package.json'));
28
- if (!isKastell) {
29
- try {
30
- const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
31
- if (pkg.name !== 'kastell') process.exit(0);
32
- } catch { process.exit(0); }
33
- }
34
-
35
- // Read audit history from cache — no SSH needed
36
- let history;
37
- try {
38
- history = JSON.parse(fs.readFileSync(HISTORY_FILE, 'utf8'));
39
- } catch {
40
- // No history file — exit silently
41
- process.exit(0);
42
- }
43
-
44
- // Find the most recent audit entry across all servers
45
- let latest = null;
46
- let latestTime = 0;
47
-
48
- if (typeof history === 'object' && !Array.isArray(history)) {
49
- // Format: { "ip": [ { overallScore, serverName, timestamp } ] }
50
- for (const entries of Object.values(history)) {
51
- if (!Array.isArray(entries)) continue;
52
- for (const entry of entries) {
53
- const ts = new Date(entry.timestamp || entry.date || 0).getTime();
54
- if (ts > latestTime) {
55
- latestTime = ts;
56
- latest = entry;
57
- }
58
- }
59
- }
60
- } else if (Array.isArray(history)) {
61
- // Format: [ { overallScore, serverName, timestamp } ]
62
- for (const entry of history) {
63
- const ts = new Date(entry.timestamp || entry.date || 0).getTime();
64
- if (ts > latestTime) {
65
- latestTime = ts;
66
- latest = entry;
67
- }
68
- }
69
- }
70
-
71
- if (latest && typeof latest.overallScore === 'number') {
72
- const serverName = latest.serverName || latest.server || 'unknown';
73
- const date = (latest.timestamp || latest.date || '').split('T')[0];
74
- const ageMs = Date.now() - latestTime;
75
- const ageDays = Math.floor(ageMs / (1000 * 60 * 60 * 24));
76
- const stale = ageDays > 7 ? ` (${ageDays} days ago — consider re-running)` : '';
77
-
78
- process.stdout.write(JSON.stringify({
79
- hookSpecificOutput: `[Kastell Audit] Last score: ${latest.overallScore}/100 (${serverName}, ${date})${stale}`,
80
- }));
81
- }
82
- } catch {}
83
-
84
- // Always exit 0 — SessionStart MUST NOT fail
85
- process.exit(0);
86
- });
1
+ #!/usr/bin/env node
2
+ // SessionStart hook: Show last audit score from cache (no SSH, instant)
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const os = require('os');
7
+
8
+ const HISTORY_FILE = path.join(os.homedir(), '.kastell', 'audit-history.json');
9
+
10
+ // MANDATORY stdin guard — exit silently if stdin unavailable (e.g. after /clear)
11
+ if (!process.stdin || process.stdin.destroyed || !process.stdin.readable) {
12
+ process.exit(0);
13
+ }
14
+
15
+ let input = '';
16
+ const stdinTimeout = setTimeout(() => process.exit(0), 1500);
17
+ process.stdin.setEncoding('utf8');
18
+ process.stdin.on('error', () => process.exit(0));
19
+ process.stdin.on('data', chunk => { input += chunk; });
20
+ process.stdin.on('end', () => {
21
+ clearTimeout(stdinTimeout);
22
+ try {
23
+ const cwd = process.cwd();
24
+
25
+ // Kastell project guard
26
+ const isKastell = fs.existsSync(path.join(cwd, 'src', 'mcp')) &&
27
+ fs.existsSync(path.join(cwd, 'package.json'));
28
+ if (!isKastell) {
29
+ try {
30
+ const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
31
+ if (pkg.name !== 'kastell') process.exit(0);
32
+ } catch { process.exit(0); }
33
+ }
34
+
35
+ // Read audit history from cache — no SSH needed
36
+ let history;
37
+ try {
38
+ history = JSON.parse(fs.readFileSync(HISTORY_FILE, 'utf8'));
39
+ } catch {
40
+ // No history file — exit silently
41
+ process.exit(0);
42
+ }
43
+
44
+ // Find the most recent audit entry across all servers
45
+ let latest = null;
46
+ let latestTime = 0;
47
+
48
+ if (typeof history === 'object' && !Array.isArray(history)) {
49
+ // Format: { "ip": [ { overallScore, serverName, timestamp } ] }
50
+ for (const entries of Object.values(history)) {
51
+ if (!Array.isArray(entries)) continue;
52
+ for (const entry of entries) {
53
+ const ts = new Date(entry.timestamp || entry.date || 0).getTime();
54
+ if (ts > latestTime) {
55
+ latestTime = ts;
56
+ latest = entry;
57
+ }
58
+ }
59
+ }
60
+ } else if (Array.isArray(history)) {
61
+ // Format: [ { overallScore, serverName, timestamp } ]
62
+ for (const entry of history) {
63
+ const ts = new Date(entry.timestamp || entry.date || 0).getTime();
64
+ if (ts > latestTime) {
65
+ latestTime = ts;
66
+ latest = entry;
67
+ }
68
+ }
69
+ }
70
+
71
+ if (latest && typeof latest.overallScore === 'number') {
72
+ const serverName = latest.serverName || latest.server || 'unknown';
73
+ const date = (latest.timestamp || latest.date || '').split('T')[0];
74
+ const ageMs = Date.now() - latestTime;
75
+ const ageDays = Math.floor(ageMs / (1000 * 60 * 60 * 24));
76
+ const stale = ageDays > 7 ? ` (${ageDays} days ago — consider re-running)` : '';
77
+
78
+ process.stdout.write(JSON.stringify({
79
+ hookSpecificOutput: `[Kastell Audit] Last score: ${latest.overallScore}/100 (${serverName}, ${date})${stale}`,
80
+ }));
81
+ }
82
+ } catch {}
83
+
84
+ // Always exit 0 — SessionStart MUST NOT fail
85
+ process.exit(0);
86
+ });
@@ -1,56 +1,56 @@
1
- #!/usr/bin/env node
2
- // PostToolUse hook: Log Bash command + output to ~/.kastell/session.log
3
-
4
- const fs = require('fs');
5
- const path = require('path');
6
- const os = require('os');
7
-
8
- const LOG_DIR = path.join(os.homedir(), '.kastell');
9
- const LOG_FILE = path.join(LOG_DIR, 'session.log');
10
- const MAX_LOG_SIZE = 5 * 1024 * 1024; // 5MB rotation threshold
11
- const SECRET_PATTERN = /(?:TOKEN|SECRET|PASSWORD|KEY|CREDENTIAL|BEARER|AUTHORIZATION)\s*[=:]\s*\S+/gi;
12
-
13
- // MANDATORY stdin guard — exit silently if stdin unavailable
14
- if (!process.stdin || process.stdin.destroyed || !process.stdin.readable) {
15
- process.exit(0);
16
- }
17
-
18
- let input = '';
19
- const stdinTimeout = setTimeout(() => process.exit(0), 1500);
20
- process.stdin.setEncoding('utf8');
21
- process.stdin.on('error', () => process.exit(0));
22
- process.stdin.on('data', chunk => { input += chunk; });
23
- process.stdin.on('end', () => {
24
- clearTimeout(stdinTimeout);
25
- try {
26
- const data = JSON.parse(input);
27
-
28
- // Safety check — matcher already filters, but guard explicitly
29
- if (data.tool_name !== 'Bash') {
30
- process.exit(0);
31
- }
32
-
33
- const cmd = ((data.tool_input && data.tool_input.command) || '').substring(0, 500);
34
- const rawOut = ((data.tool_response && data.tool_response.output) || '').substring(0, 2000);
35
- const out = rawOut.replace(SECRET_PATTERN, '[REDACTED]');
36
-
37
- const timestamp = new Date().toISOString();
38
- const entry = `[${timestamp}] CMD: ${cmd}\nOUT: ${out}\n---\n`;
39
-
40
- // Ensure log directory exists (guard avoids redundant mkdirSync on hot path)
41
- if (!fs.existsSync(LOG_DIR)) fs.mkdirSync(LOG_DIR, { recursive: true });
42
-
43
- // Log rotation: truncate when exceeding threshold
44
- try {
45
- const stat = fs.statSync(LOG_FILE);
46
- if (stat.size > MAX_LOG_SIZE) fs.writeFileSync(LOG_FILE, entry, { mode: 0o600 });
47
- else fs.appendFileSync(LOG_FILE, entry, 'utf8');
48
- } catch {
49
- // File does not exist yet — create with restrictive permissions
50
- fs.writeFileSync(LOG_FILE, entry, { mode: 0o600 });
51
- }
52
- } catch {}
53
-
54
- // Always exit 0 — logging must never block execution
55
- process.exit(0);
56
- });
1
+ #!/usr/bin/env node
2
+ // PostToolUse hook: Log Bash command + output to ~/.kastell/session.log
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const os = require('os');
7
+
8
+ const LOG_DIR = path.join(os.homedir(), '.kastell');
9
+ const LOG_FILE = path.join(LOG_DIR, 'session.log');
10
+ const MAX_LOG_SIZE = 5 * 1024 * 1024; // 5MB rotation threshold
11
+ const SECRET_PATTERN = /(?:TOKEN|SECRET|PASSWORD|KEY|CREDENTIAL|BEARER|AUTHORIZATION)\s*[=:]\s*\S+/gi;
12
+
13
+ // MANDATORY stdin guard — exit silently if stdin unavailable
14
+ if (!process.stdin || process.stdin.destroyed || !process.stdin.readable) {
15
+ process.exit(0);
16
+ }
17
+
18
+ let input = '';
19
+ const stdinTimeout = setTimeout(() => process.exit(0), 1500);
20
+ process.stdin.setEncoding('utf8');
21
+ process.stdin.on('error', () => process.exit(0));
22
+ process.stdin.on('data', chunk => { input += chunk; });
23
+ process.stdin.on('end', () => {
24
+ clearTimeout(stdinTimeout);
25
+ try {
26
+ const data = JSON.parse(input);
27
+
28
+ // Safety check — matcher already filters, but guard explicitly
29
+ if (data.tool_name !== 'Bash') {
30
+ process.exit(0);
31
+ }
32
+
33
+ const cmd = ((data.tool_input && data.tool_input.command) || '').substring(0, 500);
34
+ const rawOut = ((data.tool_response && data.tool_response.output) || '').substring(0, 2000);
35
+ const out = rawOut.replace(SECRET_PATTERN, '[REDACTED]');
36
+
37
+ const timestamp = new Date().toISOString();
38
+ const entry = `[${timestamp}] CMD: ${cmd}\nOUT: ${out}\n---\n`;
39
+
40
+ // Ensure log directory exists (guard avoids redundant mkdirSync on hot path)
41
+ if (!fs.existsSync(LOG_DIR)) fs.mkdirSync(LOG_DIR, { recursive: true });
42
+
43
+ // Log rotation: truncate when exceeding threshold
44
+ try {
45
+ const stat = fs.statSync(LOG_FILE);
46
+ if (stat.size > MAX_LOG_SIZE) fs.writeFileSync(LOG_FILE, entry, { mode: 0o600 });
47
+ else fs.appendFileSync(LOG_FILE, entry, 'utf8');
48
+ } catch {
49
+ // File does not exist yet — create with restrictive permissions
50
+ fs.writeFileSync(LOG_FILE, entry, { mode: 0o600 });
51
+ }
52
+ } catch {}
53
+
54
+ // Always exit 0 — logging must never block execution
55
+ process.exit(0);
56
+ });
@@ -1,72 +1,72 @@
1
- #!/usr/bin/env node
2
- // Stop hook: Warn if TypeScript errors, missing CHANGELOG entry, or stale README
3
-
4
- const fs = require('fs');
5
- const path = require('path');
6
- const { execSync } = require('child_process');
7
-
8
- // MANDATORY stdin guard — exit silently if stdin unavailable (e.g. after /clear)
9
- if (!process.stdin || process.stdin.destroyed || !process.stdin.readable) {
10
- process.exit(0);
11
- }
12
-
13
- let input = '';
14
- const stdinTimeout = setTimeout(() => process.exit(0), 1500);
15
- process.stdin.setEncoding('utf8');
16
- process.stdin.on('error', () => process.exit(0));
17
- process.stdin.on('data', chunk => { input += chunk; });
18
- process.stdin.on('end', () => {
19
- clearTimeout(stdinTimeout);
20
- try {
21
- const cwd = process.cwd();
22
-
23
- // Kastell project guard
24
- const isKastell = fs.existsSync(path.join(cwd, 'src', 'mcp')) &&
25
- fs.existsSync(path.join(cwd, 'package.json'));
26
- if (!isKastell) {
27
- try {
28
- const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
29
- if (pkg.name !== 'kastell') process.exit(0);
30
- } catch { process.exit(0); }
31
- }
32
-
33
- const warnings = [];
34
-
35
- // Check 1: TypeScript build
36
- try {
37
- execSync('npx tsc --noEmit 2>&1', { cwd, timeout: 10000, stdio: 'pipe', windowsHide: true });
38
- } catch {
39
- warnings.push('TypeScript errors detected - run `npm run build` to see details');
40
- }
41
-
42
- // Check 2: CHANGELOG version entry
43
- try {
44
- const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
45
- const changelog = fs.readFileSync(path.join(cwd, 'CHANGELOG.md'), 'utf8');
46
- if (!changelog.includes(`## [${pkg.version}]`) && !changelog.includes(`## ${pkg.version}`)) {
47
- warnings.push(`CHANGELOG.md missing entry for v${pkg.version}`);
48
- }
49
- } catch {}
50
-
51
- // Check 3: README staleness (compare last commit timestamps)
52
- try {
53
- const srcFiles = execSync('git log -1 --format=%ct -- src/', {
54
- cwd, encoding: 'utf8', timeout: 5000, windowsHide: true,
55
- }).trim();
56
- const readmeCommit = execSync('git log -1 --format=%ct -- README.md', {
57
- cwd, encoding: 'utf8', timeout: 5000, windowsHide: true,
58
- }).trim();
59
- if (srcFiles && readmeCommit && parseInt(srcFiles) > parseInt(readmeCommit)) {
60
- warnings.push('README.md may be stale - src/ has newer commits');
61
- }
62
- } catch {}
63
-
64
- // Output warnings to stderr — Stop hooks CANNOT block execution
65
- for (const w of warnings) {
66
- process.stderr.write(`WARNING: ${w}\n`);
67
- }
68
- } catch {}
69
-
70
- // Always exit 0 — Stop hooks must never fail session end
71
- process.exit(0);
72
- });
1
+ #!/usr/bin/env node
2
+ // Stop hook: Warn if TypeScript errors, missing CHANGELOG entry, or stale README
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const { execSync } = require('child_process');
7
+
8
+ // MANDATORY stdin guard — exit silently if stdin unavailable (e.g. after /clear)
9
+ if (!process.stdin || process.stdin.destroyed || !process.stdin.readable) {
10
+ process.exit(0);
11
+ }
12
+
13
+ let input = '';
14
+ const stdinTimeout = setTimeout(() => process.exit(0), 1500);
15
+ process.stdin.setEncoding('utf8');
16
+ process.stdin.on('error', () => process.exit(0));
17
+ process.stdin.on('data', chunk => { input += chunk; });
18
+ process.stdin.on('end', () => {
19
+ clearTimeout(stdinTimeout);
20
+ try {
21
+ const cwd = process.cwd();
22
+
23
+ // Kastell project guard
24
+ const isKastell = fs.existsSync(path.join(cwd, 'src', 'mcp')) &&
25
+ fs.existsSync(path.join(cwd, 'package.json'));
26
+ if (!isKastell) {
27
+ try {
28
+ const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
29
+ if (pkg.name !== 'kastell') process.exit(0);
30
+ } catch { process.exit(0); }
31
+ }
32
+
33
+ const warnings = [];
34
+
35
+ // Check 1: TypeScript build
36
+ try {
37
+ execSync('npx tsc --noEmit 2>&1', { cwd, timeout: 10000, stdio: 'pipe', windowsHide: true });
38
+ } catch {
39
+ warnings.push('TypeScript errors detected - run `npm run build` to see details');
40
+ }
41
+
42
+ // Check 2: CHANGELOG version entry
43
+ try {
44
+ const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
45
+ const changelog = fs.readFileSync(path.join(cwd, 'CHANGELOG.md'), 'utf8');
46
+ if (!changelog.includes(`## [${pkg.version}]`) && !changelog.includes(`## ${pkg.version}`)) {
47
+ warnings.push(`CHANGELOG.md missing entry for v${pkg.version}`);
48
+ }
49
+ } catch {}
50
+
51
+ // Check 3: README staleness (compare last commit timestamps)
52
+ try {
53
+ const srcFiles = execSync('git log -1 --format=%ct -- src/', {
54
+ cwd, encoding: 'utf8', timeout: 5000, windowsHide: true,
55
+ }).trim();
56
+ const readmeCommit = execSync('git log -1 --format=%ct -- README.md', {
57
+ cwd, encoding: 'utf8', timeout: 5000, windowsHide: true,
58
+ }).trim();
59
+ if (srcFiles && readmeCommit && parseInt(srcFiles) > parseInt(readmeCommit)) {
60
+ warnings.push('README.md may be stale - src/ has newer commits');
61
+ }
62
+ } catch {}
63
+
64
+ // Output warnings to stderr — Stop hooks CANNOT block execution
65
+ for (const w of warnings) {
66
+ process.stderr.write(`WARNING: ${w}\n`);
67
+ }
68
+ } catch {}
69
+
70
+ // Always exit 0 — Stop hooks must never fail session end
71
+ process.exit(0);
72
+ });