@@ -61,6 +61,20 @@ pub fn getArchiveExtension() []const u8 {
6161 };
6262}
6363
64+ /// Returns the platform-specific executable filename for a tool.
65+ /// Windows executables must keep their .exe suffix so shells and editors can find them.
66+ pub fn executableName (comptime base_name : []const u8 ) []const u8 {
67+ return switch (builtin .os .tag ) {
68+ .windows = > base_name ++ ".exe" ,
69+ else = > base_name ,
70+ };
71+ }
72+
73+ /// Returns true when the current target uses Windows executable semantics.
74+ pub fn isWindows () bool {
75+ return builtin .os .tag == .windows ;
76+ }
77+
6478/// Create a symbolic link at `link_path` pointing to `target`.
6579/// Removes any existing file/link at `link_path` before creation.
6680/// On Windows, creates a directory junction (no admin privileges required).
@@ -212,3 +226,160 @@ pub fn copyFile(io: std.Io, src_path: []const u8, dst_path: []const u8) !void {
212226 _ = src_reader .interface .streamRemaining (& dst_writer .interface ) catch return error .CopyFailed ;
213227 try dst_writer .interface .flush ();
214228}
229+
230+ // ─────────────────────────────────────────────────────────────────────────────
231+ // Windows PATH management — automatically add zvm bin directory to user PATH
232+ // ─────────────────────────────────────────────────────────────────────────────
233+
234+ /// Check if a directory is already in the Windows user PATH environment variable.
235+ /// Performs case-insensitive comparison (Windows paths are case-insensitive).
236+ /// Returns false on non-Windows platforms or on any error.
237+ pub fn isInUserPath (io : std.Io , dir_path : []const u8 ) bool {
238+ if (builtin .os .tag != .windows ) return false ;
239+
240+ // Normalize dir_path to use backslashes
241+ var dir_norm : [std .fs .max_path_bytes ]u8 = undefined ;
242+ if (dir_path .len > dir_norm .len ) return false ;
243+ @memcpy (dir_norm [0.. dir_path .len ], dir_path );
244+ std .mem .replaceScalar (u8 , dir_norm [0.. dir_path .len ], '/' , '\\ ' );
245+ const normalized = dir_norm [0.. dir_path .len ];
246+
247+ // Read current user PATH from registry
248+ const read_result = std .process .run (std .heap .page_allocator , io , .{
249+ .argv = &.{ "reg" , "query" , "HKCU\\ Environment" , "/v" , "PATH" },
250+ .stdout_limit = .limited (65536 ),
251+ .stderr_limit = .limited (4096 ),
252+ }) catch return false ;
253+ defer std .heap .page_allocator .free (read_result .stdout );
254+ defer std .heap .page_allocator .free (read_result .stderr );
255+
256+ if (read_result .term != .exited or read_result .term .exited != 0 ) return false ;
257+
258+ // Parse reg query output to find PATH value
259+ // Output format: " PATH REG_EXPAND_SZ C:\Users\..."
260+ const stdout = read_result .stdout ;
261+ var line_iter = std .mem .splitScalar (u8 , stdout , '\n ' );
262+ while (line_iter .next ()) | line | {
263+ const trimmed = std .mem .trim (u8 , line , " \r \t " );
264+ // Look for REG_EXPAND_SZ or REG_SZ and extract the value
265+ if (std .mem .indexOf (u8 , trimmed , "REG_EXPAND_SZ" )) | idx | {
266+ const value_start = idx + "REG_EXPAND_SZ" .len ;
267+ const value = std .mem .trim (u8 , trimmed [value_start .. ], " \t " );
268+ return containsPathEntry (value , normalized );
269+ }
270+ if (std .mem .indexOf (u8 , trimmed , "REG_SZ" )) | idx | {
271+ const value_start = idx + "REG_SZ" .len ;
272+ const value = std .mem .trim (u8 , trimmed [value_start .. ], " \t " );
273+ return containsPathEntry (value , normalized );
274+ }
275+ }
276+
277+ return false ;
278+ }
279+
280+ /// Add a directory to the Windows user PATH environment variable in the registry.
281+ /// Does nothing if the directory is already present.
282+ /// Returns true if PATH was updated, false if already present.
283+ /// No-op on non-Windows platforms (returns false).
284+ pub fn addToUserPath (io : std.Io , dir_path : []const u8 ) ! bool {
285+ if (builtin .os .tag != .windows ) return false ;
286+
287+ // Normalize dir_path to use backslashes
288+ var dir_norm : [std .fs .max_path_bytes ]u8 = undefined ;
289+ if (dir_path .len > dir_norm .len ) return error .PathTooLong ;
290+ @memcpy (dir_norm [0.. dir_path .len ], dir_path );
291+ std .mem .replaceScalar (u8 , dir_norm [0.. dir_path .len ], '/' , '\\ ' );
292+ const normalized = dir_norm [0.. dir_path .len ];
293+
294+ // Read current user PATH from registry
295+ const read_result = std .process .run (std .heap .page_allocator , io , .{
296+ .argv = &.{ "reg" , "query" , "HKCU\\ Environment" , "/v" , "PATH" },
297+ .stdout_limit = .limited (65536 ),
298+ .stderr_limit = .limited (4096 ),
299+ }) catch return error .PathUpdateFailed ;
300+ defer std .heap .page_allocator .free (read_result .stdout );
301+ defer std .heap .page_allocator .free (read_result .stderr );
302+
303+ // If reg query failed (e.g., PATH doesn't exist), create initial PATH
304+ if (read_result .term != .exited or read_result .term .exited != 0 ) {
305+ return try createInitialUserPath (io , normalized );
306+ }
307+
308+ // Parse reg query output to find current PATH value
309+ const stdout = read_result .stdout ;
310+ var current_path : []const u8 = "" ;
311+ var line_iter = std .mem .splitScalar (u8 , stdout , '\n ' );
312+ while (line_iter .next ()) | line | {
313+ const trimmed = std .mem .trim (u8 , line , " \r \t " );
314+ if (std .mem .indexOf (u8 , trimmed , "REG_EXPAND_SZ" )) | idx | {
315+ const value_start = idx + "REG_EXPAND_SZ" .len ;
316+ current_path = std .mem .trim (u8 , trimmed [value_start .. ], " \t " );
317+ break ;
318+ }
319+ if (std .mem .indexOf (u8 , trimmed , "REG_SZ" )) | idx | {
320+ const value_start = idx + "REG_SZ" .len ;
321+ current_path = std .mem .trim (u8 , trimmed [value_start .. ], " \t " );
322+ break ;
323+ }
324+ }
325+
326+ // Check if already in PATH
327+ if (containsPathEntry (current_path , normalized )) return false ;
328+
329+ // Build new PATH with the directory appended
330+ var new_path_buf : [65536 ]u8 = undefined ;
331+ const separator = if (current_path .len > 0 ) ";" else "" ;
332+ const new_path = std .fmt .bufPrint (& new_path_buf , "{s}{s}{s}" , .{ current_path , separator , normalized }) catch return error .PathTooLong ;
333+
334+ // Write updated PATH to registry using reg add
335+ try writeUserPath (io , new_path );
336+
337+ return true ;
338+ }
339+
340+ /// Create an initial user PATH entry in the registry with just the given directory.
341+ fn createInitialUserPath (io : std.Io , dir_path : []const u8 ) ! bool {
342+ try writeUserPath (io , dir_path );
343+ return true ;
344+ }
345+
346+ /// Write a value to the user PATH in the Windows registry.
347+ fn writeUserPath (io : std.Io , path_value : []const u8 ) ! void {
348+ const result = std .process .run (std .heap .page_allocator , io , .{
349+ .argv = &.{ "reg" , "add" , "HKCU\\ Environment" , "/v" , "PATH" , "/t" , "REG_EXPAND_SZ" , "/d" , path_value , "/f" },
350+ .stdout_limit = .limited (4096 ),
351+ .stderr_limit = .limited (4096 ),
352+ }) catch return error .PathUpdateFailed ;
353+ defer std .heap .page_allocator .free (result .stdout );
354+ defer std .heap .page_allocator .free (result .stderr );
355+
356+ if (result .term != .exited or result .term .exited != 0 )
357+ return error .PathUpdateFailed ;
358+ }
359+
360+ /// Check if a semicolon-separated PATH string contains a specific entry.
361+ /// Performs case-insensitive, slash-normalized comparison and trims trailing backslashes.
362+ fn containsPathEntry (path_str : []const u8 , entry : []const u8 ) bool {
363+ var iter = std .mem .splitScalar (u8 , path_str , ';' );
364+ while (iter .next ()) | part | {
365+ var p = std .mem .trim (u8 , part , " \t " );
366+ // Trim trailing backslashes for comparison
367+ while (p .len > 0 and p [p .len - 1 ] == '\\ ' ) {
368+ p = p [0 .. p .len - 1 ];
369+ }
370+ // Case-insensitive, slash-normalized comparison
371+ if (pathEqual (p , entry )) return true ;
372+ }
373+ return false ;
374+ }
375+
376+ /// Compare two path strings case-insensitively, treating '/' and '\\' as equal.
377+ fn pathEqual (a : []const u8 , b : []const u8 ) bool {
378+ if (a .len != b .len ) return false ;
379+ for (0.. a .len ) | i | {
380+ const ca : u8 = if (a [i ] == '/' ) '\\ ' else a [i ];
381+ const cb : u8 = if (b [i ] == '/' ) '\\ ' else b [i ];
382+ if (std .ascii .toLower (ca ) != std .ascii .toLower (cb )) return false ;
383+ }
384+ return true ;
385+ }
0 commit comments