Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/macos-socket-path-length-limit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@nanocollective/nanocoder": patch
---

Handle macOS socket path length limit
19 changes: 18 additions & 1 deletion source/daemon/lockfile.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,23 @@ test('getSocketPath returns a project-local .sock file on non-Windows', t => {
t.is(sock, join(root, '.nanocoder', 'daemon.sock'));
});

test('getSocketPath on macOS uses project dir for short paths and temp dir for long paths', t => {
if (process.platform !== 'darwin') {
t.pass('skipped: macOS-only assertion');
return;
}
// Short path should use project-local socket
const shortRoot = '/tmp/short';
const shortSock = getSocketPath(shortRoot);
t.is(shortSock, join(shortRoot, '.nanocoder', 'daemon.sock'));
// Long path (>104 bytes) should use temp directory with hash
const longRoot = '/tmp/' + 'a'.repeat(200);
const longSock = getSocketPath(longRoot);
t.true(longSock.startsWith(tmpdir()));
t.regex(longSock, /nanocoder-daemon-[a-f0-9]{10}\.sock$/);
t.not(longSock, join(longRoot, '.nanocoder', 'daemon.sock'));
});

test('getSocketPath returns a named pipe path on Windows', t => {
if (process.platform !== 'win32') {
t.pass('skipped: Windows-only assertion');
Expand All @@ -146,4 +163,4 @@ test('getSocketPath returns a named pipe path on Windows', t => {
t.regex(a, /^\\\\\.\\pipe\\nanocoder-daemon-[a-f0-9]{10}$/);
t.regex(b, /^\\\\\.\\pipe\\nanocoder-daemon-[a-f0-9]{10}$/);
t.not(a, b, 'distinct project roots produce distinct pipes');
});
});
17 changes: 17 additions & 0 deletions source/daemon/lockfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import {createHash, randomBytes} from 'node:crypto';
import {existsSync, mkdirSync} from 'node:fs';
import {readFile, rename, unlink, writeFile} from 'node:fs/promises';
import {tmpdir} from 'node:os';
import {dirname, join} from 'node:path';

export interface DaemonLock {
Expand Down Expand Up @@ -45,6 +46,22 @@ export function getSocketPath(projectRoot: string): string {
.slice(0, 10);
return `\\\\.\\pipe\\nanocoder-daemon-${hash}`;
}

// On macOS, the socket path must be shorter than 104 bytes.
const DARWIN_SOCKET_PATH_CAPACITY = 104;
if (
process.platform === 'darwin' &&
Buffer.byteLength(projectRoot) > DARWIN_SOCKET_PATH_CAPACITY
) {
// Use a hash of the project root to keep and create a unique socket path in macOS temporary directory.
//
const hash = createHash('sha256')
.update(projectRoot)
.digest('hex')
.slice(0, 10)
.toLowerCase();
return join(tmpdir(), `nanocoder-daemon-${hash}.sock`);
}
return join(projectRoot, '.nanocoder', 'daemon.sock');
}

Expand Down
Loading