1export interface WslUncPath {
2 readonly distro: string;
3 readonly linuxPath: string;
4}
5
6export interface WslEnvironmentCandidate<TEnvironmentId extends string = string> {
7 readonly environmentId: TEnvironmentId;
8 readonly backendId: string;
9 readonly runningDistro: string | null;
10}
11
12export interface WslEnvironmentConfiguration {
13 readonly enabled: boolean;
14 readonly wslOnly: boolean;
15 readonly distro: string | null;
16 readonly distros: ReadonlyArray<{
17 readonly name: string;
18 readonly isDefault: boolean;
19 }>;
20}
21
22export interface WslProjectSelection<TEnvironmentId extends string = string> extends WslUncPath {
23 readonly environmentId: TEnvironmentId;
24}
25
26const WSL_UNC_PREFIXES = ["\\\\wsl.localhost\\", "\\\\wsl$\\"] as const;
27const WSL_DISTRO_NAME_PATTERN = /^[A-Za-z0-9._-]+$/;
28const WSL_DEFAULT_BACKEND_ID = "wsl:default";
29
30export function parseWslUncPath(input: string): WslUncPath | null {
31 const normalized = input.trim().replaceAll("/", "\\");
32 const prefix = WSL_UNC_PREFIXES.find((candidate) =>
33 normalized.toLowerCase().startsWith(candidate.toLowerCase()),
34 );
35 if (!prefix) {
36 return null;
37 }
38
39 const rest = normalized.slice(prefix.length);
40 const segments = rest.split("\\").filter((segment) => segment.length > 0);
41 const distro = segments.shift();
42 if (!distro || !WSL_DISTRO_NAME_PATTERN.test(distro)) {
43 return null;
44 }
45
46 return {
47 distro,
48 linuxPath: segments.length === 0 ? "/" : `/${segments.join("/")}`,
49 };
50}
51
52export function resolveWslProjectSelection<TEnvironmentId extends string>(
53 input: string,
54 candidates: ReadonlyArray<WslEnvironmentCandidate<TEnvironmentId>>,
55): WslProjectSelection<TEnvironmentId> | null {
56 const parsed = parseWslUncPath(input);
57 if (!parsed) {
58 return null;
59 }
60
61 const exact = candidates.find((candidate) => {
62 if (!candidate.backendId.startsWith("wsl:")) {
63 return false;
64 }
65
66 const backendDistro = candidate.backendId.slice("wsl:".length);
67 const runningDistro =
68 candidate.runningDistro ??
69 (backendDistro.length > 0 && backendDistro.toLowerCase() !== "default"
70 ? backendDistro
71 : null);
72 return runningDistro?.toLowerCase() === parsed.distro.toLowerCase();
73 });
74 return exact ? { ...parsed, environmentId: exact.environmentId } : null;
75}
76
77interface ConfiguredWslBackend {
78 readonly backendId: string;
79 readonly runningDistro: string | null;
80}
81
82function resolveConfiguredWslBackend(
83 configuration: WslEnvironmentConfiguration | null,
84): ConfiguredWslBackend | null {
85 if (!configuration) {
86 return null;
87 }
88
89 if (configuration.distro === null) {
90 return { backendId: WSL_DEFAULT_BACKEND_ID, runningDistro: null };
91 }
92
93 const configuredDistro = configuration.distro.trim();
94 if (configuredDistro.length === 0) {
95 return null;
96 }
97
98 const installedDistro = configuration.distros.find(
99 (distro) => distro.name.toLowerCase() === configuredDistro.toLowerCase(),
100 );
101 const resolvedDistro = installedDistro?.name ?? configuredDistro;
102 return { backendId: `wsl:${resolvedDistro}`, runningDistro: resolvedDistro };
103}
104
105export function resolveProjectPickerTarget<TEnvironmentId extends string>(input: {
106 readonly browseEnvironmentId: TEnvironmentId | null;
107 readonly primaryEnvironmentId: TEnvironmentId | null;
108 readonly desktopInstanceId: string | null;
109 readonly wslConfiguration: WslEnvironmentConfiguration | null;
110}): string | null {
111 if (input.desktopInstanceId !== null) {
112 return input.desktopInstanceId;
113 }
114
115 if (
116 input.browseEnvironmentId === null ||
117 input.browseEnvironmentId !== input.primaryEnvironmentId ||
118 !input.wslConfiguration?.enabled ||
119 !input.wslConfiguration.wslOnly
120 ) {
121 return null;
122 }
123
124 return resolveConfiguredWslBackend(input.wslConfiguration)?.backendId ?? null;
125}
126
127export function applyWslEnvironmentConfiguration<TEnvironmentId extends string>(
128 candidates: ReadonlyArray<WslEnvironmentCandidate<TEnvironmentId>>,
129 primaryEnvironmentId: TEnvironmentId | null,
130 configuration: WslEnvironmentConfiguration | null,
131 primaryRunningDistro: string | null = null,
132): ReadonlyArray<WslEnvironmentCandidate<TEnvironmentId>> {
133 if (!configuration) {
134 return candidates;
135 }
136
137 const configuredBackend = resolveConfiguredWslBackend(configuration);
138 if (!configuredBackend) {
139 return candidates;
140 }
141
142 if (
143 configuration.enabled &&
144 configuration.wslOnly &&
145 primaryEnvironmentId !== null &&
146 !candidates.some((candidate) => candidate.environmentId === primaryEnvironmentId)
147 ) {
148 return [
149 ...candidates,
150 {
151 environmentId: primaryEnvironmentId,
152 ...configuredBackend,
153 runningDistro: primaryRunningDistro ?? configuredBackend.runningDistro,
154 },
155 ];
156 }
157
158 return candidates;
159}