@brimveyn/aimux 1.22.4 → 1.22.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.
package/package.json
CHANGED
package/src/git/worktree.ts
CHANGED
|
@@ -206,7 +206,7 @@ export async function removeGitWorktree({
|
|
|
206
206
|
force: boolean
|
|
207
207
|
}): Promise<void> {
|
|
208
208
|
if (!isInsideAimuxWorktreeRoot(targetPath)) {
|
|
209
|
-
throw new Error(`refusing to delete worktree outside Aimux
|
|
209
|
+
throw new Error(`refusing to delete worktree outside Aimux worktree root: ${targetPath}`)
|
|
210
210
|
}
|
|
211
211
|
const result = force
|
|
212
212
|
? await $`git -C ${repoPath} worktree remove --force ${targetPath}`.quiet().nothrow()
|
|
@@ -141,6 +141,7 @@ function findPlayer(platform: string): string | null {
|
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
let lastPlayedAt = 0
|
|
144
|
+
let playing: { kill: () => void } | null = null
|
|
144
145
|
|
|
145
146
|
/**
|
|
146
147
|
* Whether enough time has passed since the last sound. Separate from the play
|
|
@@ -151,16 +152,17 @@ export function shouldPlayNow(now: number, previous: number): boolean {
|
|
|
151
152
|
}
|
|
152
153
|
|
|
153
154
|
/**
|
|
154
|
-
* Play a sound file. Returns
|
|
155
|
-
* what the settings screen's "Test
|
|
155
|
+
* Play a sound file. Returns false only when nothing could play at all — a
|
|
156
|
+
* missing player or a failed spawn — which is what the settings screen's "Test
|
|
157
|
+
* sound" row reports. A throttled call is not a failure.
|
|
158
|
+
*
|
|
159
|
+
* The throttle covers every caller, the test row included: held down, its key
|
|
160
|
+
* repeats tens of times a second, and one live player per press is enough to
|
|
161
|
+
* take CoreAudio down with it.
|
|
156
162
|
*/
|
|
157
|
-
export function playSoundFile(
|
|
158
|
-
path: string,
|
|
159
|
-
options?: { ignoreThrottle?: boolean; volume?: number }
|
|
160
|
-
): boolean {
|
|
163
|
+
export function playSoundFile(path: string, options?: { volume?: number }): boolean {
|
|
161
164
|
const now = Date.now()
|
|
162
|
-
|
|
163
|
-
if (throttled && !shouldPlayNow(now, lastPlayedAt)) return false
|
|
165
|
+
if (!shouldPlayNow(now, lastPlayedAt)) return true
|
|
164
166
|
const platform = process.platform
|
|
165
167
|
const bin = findPlayer(platform)
|
|
166
168
|
if (bin == null) {
|
|
@@ -169,10 +171,11 @@ export function playSoundFile(
|
|
|
169
171
|
}
|
|
170
172
|
const argv = soundPlayerArgv(platform, bin, path, options?.volume ?? DEFAULT_VOLUME)
|
|
171
173
|
try {
|
|
172
|
-
|
|
173
|
-
//
|
|
174
|
-
|
|
175
|
-
|
|
174
|
+
// Cut the previous one rather than layer on top of it. Players come and go
|
|
175
|
+
// on their own, so killing an already-exited one has to be harmless.
|
|
176
|
+
playing?.kill()
|
|
177
|
+
playing = Bun.spawn(argv, { stderr: 'ignore', stdin: 'ignore', stdout: 'ignore' })
|
|
178
|
+
lastPlayedAt = now
|
|
176
179
|
return true
|
|
177
180
|
} catch (error) {
|
|
178
181
|
logDebug('platform.playSound.error', {
|
|
@@ -2,12 +2,23 @@ import { createHash } from 'node:crypto'
|
|
|
2
2
|
import { lstat, mkdir, realpath, rmdir } from 'node:fs/promises'
|
|
3
3
|
import { join, resolve } from 'node:path'
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
// Worktrees hold uncommitted work, so they live in the XDG data dir, not /tmp:
|
|
6
|
+
// a reboot clears /tmp on macOS and on most Linux distros, and took the work
|
|
7
|
+
// with it. The old root stays recognized (never generated) so worktrees created
|
|
8
|
+
// before this change are still classified and deleted as aimux-managed.
|
|
9
|
+
const LEGACY_WORKTREE_ROOT = '/tmp/aimux-wt'
|
|
6
10
|
const MAX_SLUG_LENGTH = 24
|
|
7
11
|
|
|
12
|
+
function defaultWorktreeRoot(): string {
|
|
13
|
+
const xdgData = process.env.XDG_DATA_HOME
|
|
14
|
+
const base =
|
|
15
|
+
xdgData != null && xdgData !== '' ? xdgData : join(process.env.HOME ?? '.', '.local', 'share')
|
|
16
|
+
return join(base, 'aimux', 'worktrees')
|
|
17
|
+
}
|
|
18
|
+
|
|
8
19
|
export function getAimuxWorktreeRoot(): string {
|
|
9
20
|
const root = process.env.AIMUX_WORKTREE_ROOT
|
|
10
|
-
return root != null && root !== '' ? root :
|
|
21
|
+
return root != null && root !== '' ? root : defaultWorktreeRoot()
|
|
11
22
|
}
|
|
12
23
|
|
|
13
24
|
export function sanitizePathSegment(input: string, maxLength = MAX_SLUG_LENGTH): string {
|
|
@@ -42,9 +53,10 @@ export function makeWorktreePath({
|
|
|
42
53
|
|
|
43
54
|
export function isInsideAimuxWorktreeRoot(path: string): boolean {
|
|
44
55
|
const normalizeTmp = (value: string) => value.replace(/^\/private\/tmp(?=\/|$)/, '/tmp')
|
|
45
|
-
const root = `${normalizeTmp(resolve(getAimuxWorktreeRoot()))}/`
|
|
46
56
|
const target = `${normalizeTmp(resolve(path))}/`
|
|
47
|
-
return
|
|
57
|
+
return [getAimuxWorktreeRoot(), LEGACY_WORKTREE_ROOT].some((root) =>
|
|
58
|
+
target.startsWith(`${normalizeTmp(resolve(root))}/`)
|
|
59
|
+
)
|
|
48
60
|
}
|
|
49
61
|
|
|
50
62
|
export async function ensureAimuxWorktreeRoot(): Promise<string> {
|
|
@@ -80,13 +92,13 @@ export async function pruneEmptyWorktreeParent(worktreePath: string): Promise<vo
|
|
|
80
92
|
export async function assertSafeAimuxWorktreePath(path: string): Promise<void> {
|
|
81
93
|
const root = await ensureAimuxWorktreeRoot()
|
|
82
94
|
if (!isInsideAimuxWorktreeRoot(path)) {
|
|
83
|
-
throw new Error(`refusing worktree path outside Aimux
|
|
95
|
+
throw new Error(`refusing worktree path outside Aimux worktree root: ${path}`)
|
|
84
96
|
}
|
|
85
97
|
// Create the repo-scoped parent (<root>/r-<hash>) before resolving it: git
|
|
86
98
|
// worktree add does not create intermediate dirs, and realpath() would throw
|
|
87
99
|
// ENOENT on the first worktree for a repo. mkdir(recursive) leaves an
|
|
88
100
|
// existing symlink in place, so the realpath check below still catches an
|
|
89
|
-
// escape out of the
|
|
101
|
+
// escape out of the worktree root.
|
|
90
102
|
const parent = resolve(path, '..')
|
|
91
103
|
await mkdir(parent, { recursive: true })
|
|
92
104
|
const realRoot = await realpath(root)
|
|
@@ -45,10 +45,10 @@ function selectedVolume(): number {
|
|
|
45
45
|
* user has set the row to `off`, which is what makes it safe to call on every
|
|
46
46
|
* status edge.
|
|
47
47
|
*/
|
|
48
|
-
export function playNotificationSound(
|
|
48
|
+
export function playNotificationSound(): boolean {
|
|
49
49
|
const path = resolveSoundPath(selectedSoundId())
|
|
50
50
|
if (path == null) return false
|
|
51
|
-
return playSoundFile(path, {
|
|
51
|
+
return playSoundFile(path, { volume: selectedVolume() })
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
export const NOTIFICATIONS_SECTION: SettingSection = {
|
|
@@ -89,7 +89,7 @@ export const NOTIFICATIONS_SECTION: SettingSection = {
|
|
|
89
89
|
toast.info('Notification sound is off')
|
|
90
90
|
return
|
|
91
91
|
}
|
|
92
|
-
if (!playNotificationSound(
|
|
92
|
+
if (!playNotificationSound()) {
|
|
93
93
|
toast.error('Could not play that sound — no audio player found')
|
|
94
94
|
}
|
|
95
95
|
},
|