diff --git a/.changeset/macos-socket-path-length-limit.md b/.changeset/macos-socket-path-length-limit.md new file mode 100644 index 000000000..bdde4188c --- /dev/null +++ b/.changeset/macos-socket-path-length-limit.md @@ -0,0 +1,5 @@ +--- +"@nanocollective/nanocoder": patch +--- + +Handle macOS socket path length limit diff --git a/source/daemon/lockfile.spec.ts b/source/daemon/lockfile.spec.ts index 234647cf9..cfc51f323 100644 --- a/source/daemon/lockfile.spec.ts +++ b/source/daemon/lockfile.spec.ts @@ -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'); @@ -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'); -}); +}); \ No newline at end of file diff --git a/source/daemon/lockfile.ts b/source/daemon/lockfile.ts index 5e9de0fb3..c9538ee35 100644 --- a/source/daemon/lockfile.ts +++ b/source/daemon/lockfile.ts @@ -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 { @@ -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'); }