-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathshared.ts
More file actions
153 lines (143 loc) · 4.5 KB
/
shared.ts
File metadata and controls
153 lines (143 loc) · 4.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
/** Shared utilities for MAXSIM hooks. */
import * as fs from 'node:fs';
import { spawnSync } from 'node:child_process';
import * as os from 'node:os';
import * as path from 'node:path';
export function readStdinJson<T>(callback: (data: T) => void): void {
let input = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', (chunk: string) => {
input += chunk;
});
process.stdin.on('end', () => {
try {
const data = JSON.parse(input) as T;
callback(data);
} catch {
process.exit(0);
}
});
}
export const CLAUDE_DIR = '.claude';
/** Returns true when .claude/maxsim/config.json exists in the given directory. */
export function isMaxsimProject(projectDir: string): boolean {
try {
return fs.existsSync(path.join(projectDir, CLAUDE_DIR, 'maxsim', 'config.json'));
} catch {
return false;
}
}
/** Reads the last N git commits as oneline strings. Returns empty array on failure. */
export function recentCommits(projectDir: string, n = 5): string[] {
try {
const result = spawnSync(
'git',
['log', '--oneline', `-${n}`],
{
cwd: projectDir,
encoding: 'utf8',
timeout: 4000,
stdio: ['ignore', 'pipe', 'ignore'],
windowsHide: true,
},
);
if (result.status !== 0) return [];
return (result.stdout ?? '')
.split('\n')
.map((l) => l.trim())
.filter(Boolean);
} catch {
return [];
}
}
/** Returns true when running on Windows. */
export function isWindows(): boolean {
return os.platform() === 'win32';
}
/** Returns true when running on macOS. */
export function isMac(): boolean {
return os.platform() === 'darwin';
}
/**
* Resolve a bundled WAV asset relative to the running hook script, or return null.
*
* When compiled to a CJS bundle the file sits next to the .cjs file in
* dist/assets/hooks/. We look for a sibling sounds/ directory.
*/
export function bundledSound(name: string): string | null {
const candidates = [
path.join(path.dirname(process.argv[1] ?? __filename), 'sounds', name),
path.join(__dirname, 'sounds', name),
];
for (const p of candidates) {
try {
// eslint-disable-next-line @typescript-eslint/no-require-imports
if (require('node:fs').existsSync(p)) return p;
} catch {
// ignore
}
}
return null;
}
/**
* Play a system sound file cross-platform.
* Never throws — sound failure is always silently swallowed.
*
* @param soundFile Absolute path to a WAV/MP3/etc. file, or a named system
* sound token recognised by the platform helper (e.g. the
* Windows-only SystemAsterisk token).
*/
export function playSound(soundFile: string): void {
try {
if (isWindows()) {
// PowerShell's SoundPlayer works with WAV files synchronously.
// For named system sounds (no extension) fall back to rundll32.
const isWav = soundFile.toLowerCase().endsWith('.wav');
if (isWav) {
// Use double-quoted string which handles spaces and most special chars
const escaped = soundFile.replace(/"/g, '\\"');
spawnSync(
'powershell',
[
'-NoProfile',
'-NonInteractive',
'-Command',
`$p="${escaped}"; (New-Object System.Media.SoundPlayer $p).PlaySync()`,
],
{ stdio: 'ignore' },
);
} else {
// Named system sound token (e.g. "SystemAsterisk") or unsupported format —
// use the rundll32 winsound bridge.
spawnSync(
'rundll32',
['user32.dll,MessageBeep'],
{ stdio: 'ignore' },
);
}
} else if (isMac()) {
spawnSync('afplay', [soundFile], { stdio: 'ignore' });
} else {
// Linux: try paplay (PulseAudio) then aplay (ALSA)
const paplay = spawnSync('paplay', [soundFile], { stdio: 'ignore' });
if (paplay.status !== 0) {
spawnSync('aplay', [soundFile], { stdio: 'ignore' });
}
}
} catch {
// Never crash on sound failure
}
}
/**
* Send a JSON stop signal to terminate a teammate.
* Outputs the stop payload to stdout and exits cleanly.
* Use this when a teammate should be permanently stopped (not just blocked).
*
* For blocking (retry behavior), use: process.stderr.write(msg); process.exit(2);
* For stopping (permanent), use: stopTeammate(reason);
*/
export function stopTeammate(reason: string): never {
const payload = JSON.stringify({ continue: false, stopReason: reason });
process.stdout.write(`${payload}\n`);
process.exit(0);
}