port clide runtime to Windows (ConPTY, AF_UNIX, PATHEXT)

Bring the runtime up on Windows without disturbing the POSIX paths.

PTY: introduce a platform-neutral PtySession contract with a factory
that picks NativePty (posix_openpt/posix_spawn) or the new WindowsPty
(ConPTY via CreatePseudoConsole). The pane registry programs against
the interface; NativePty now implements it.

IPC: the per-workspace AF_UNIX socket lives under %LOCALAPPDATA% and
is hashed from a canonical workspace key (backslash + ASCII-folded
case) so the Dart server and the C client agree despite NTFS case-
insensitivity. The C client grows a Win32 shim (winsock afunix);
chmod is a no-op on Windows where the per-user ACL is the gate.

Toolchain: PATH probing splits on ';' and tries PATHEXT extensions;
the shell defaults to PowerShell (pwsh, then powershell); tmux is
treated as optional since it has no Windows build; dugite falls back
to PATH git for now.

Build: add `make build-windows`, a clide-cli MSVC build wrapped by
ci/build_cli_windows.sh, and a ConPTY smoke-test suite that self-
skips off-platform.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-06-14 17:42:55 +02:00
co-authored by Claude
parent 3a59f4f2e6
commit 03cc0603b0
19 changed files with 1257 additions and 103 deletions
+31 -5
View File
@@ -4,21 +4,47 @@ import 'dart:io';
/// Resolve the per-workspace Unix-domain socket path served by the
/// running clide app. Per D-70:
///
/// Linux: `$XDG_RUNTIME_DIR/clide/<hash>.sock`
/// macOS: `$HOME/Library/Caches/clide/<hash>.sock`
/// Linux: `$XDG_RUNTIME_DIR/clide/<hash>.sock`
/// macOS: `$HOME/Library/Caches/clide/<hash>.sock`
/// Windows: `%LOCALAPPDATA%\clide\<hash>.sock` (AF_UNIX — supported
/// by winsock since Windows 10 1803 and by dart:io)
///
/// The C `clide` client and any other consumer derive the same path
/// from the same workspace root, so server + client always agree
/// without configuration.
String workspaceSocketPath(String workspaceRoot) {
final dir = socketDirectory();
return '$dir/${_hash(workspaceRoot)}.sock';
return '$dir/${_hash(canonicalWorkspaceKey(workspaceRoot))}.sock';
}
/// Canonical form of the workspace root used as the FNV hash input.
///
/// On Windows one directory has many spellings — either slash kind,
/// any letter case (NTFS is case-insensitive and getcwd preserves
/// whatever the shell typed) — so the server and the C client could
/// derive different hashes for the same workspace. Backslash +
/// ASCII-lower-case is the canonical spelling; the C client applies
/// the same byte-level fold (which is why this is NOT Unicode
/// `toLowerCase()` — the fold must be reproducible over raw UTF-8
/// bytes in C). POSIX paths pass through untouched.
String canonicalWorkspaceKey(String workspaceRoot) {
if (!Platform.isWindows) return workspaceRoot;
final folded = workspaceRoot.replaceAll('/', r'\');
final units = folded.codeUnits.map((u) => (u >= 0x41 && u <= 0x5a) ? u + 0x20 : u).toList();
return String.fromCharCodes(units);
}
/// Parent directory that holds every per-workspace socket for this
/// user. Created with `0700` on bind (see D-71). Exposed separately
/// so the server can prepare/perm-fix the directory before binding.
/// user. Created with `0700` on bind (see D-71; on Windows the
/// per-user ACL on `%LOCALAPPDATA%` is the equivalent gate). Exposed
/// separately so the server can prepare/perm-fix the directory before
/// binding.
String socketDirectory() {
if (Platform.isWindows) {
final local = Platform.environment['LOCALAPPDATA'];
final base = (local != null && local.isNotEmpty) ? local : '${Platform.environment['USERPROFILE'] ?? r'C:\'}\\AppData\\Local';
return '$base\\clide';
}
if (Platform.isMacOS) {
final home = Platform.environment['HOME'] ?? '/tmp';
return '$home/Library/Caches/clide';
+5 -1
View File
@@ -410,8 +410,12 @@ class IpcServer {
// -- internals ------------------------------------------------------------
/// `chmod` via `chmod(1)` because dart:io doesn't expose the
/// syscall on unix. Cheap; only runs at start/stop.
/// syscall on unix. Cheap; only runs at start/stop. No-op on
/// Windows: POSIX modes don't exist there, and the socket lives
/// under `%LOCALAPPDATA%`, whose per-user ACL already provides the
/// user-only gate D-71 wants.
Future<void> _chmod(String path, int modeBits) async {
if (Platform.isWindows) return;
final octal = modeBits.toRadixString(8).padLeft(3, '0');
final r = await Process.run('chmod', [octal, path]);
if (r.exitCode != 0) {