local-commander-agent 0.3.47 → 0.3.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,223 +1,223 @@
1
- import fs from "node:fs/promises";
2
- import os from "node:os";
3
- import path from "node:path";
4
- import { spawnSync } from "node:child_process";
5
-
6
- const SHIM_MARKER = "@rem Local Commander managed command\r\n";
7
- const WRAPPER_MARKER = "# Local Commander managed command\r\n";
8
- const MONITOR_WRAPPER = "localcommander-monitor.ps1";
9
- const SHORTCUT_NAME = "Local Commander Monitor";
10
- const CLI_FILES = ["package.json", "bin/localcommander.mjs", "bin/monitor.mjs", "bin/monitor-agents.mjs", "bin/monitor-context.mjs", "bin/monitor-mcp.mjs", "bin/monitor-savings.mjs", "bin/command-path.mjs", "bin/network-package.mjs"];
11
-
12
- function validVersion(version) {
13
- if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(version)) throw Error("PACKAGE_VERSION_INVALID");
14
- return version;
15
- }
16
-
17
- export function commandShim(version) {
18
- return SHIM_MARKER + '@echo off\r\nsetlocal DisableDelayedExpansion\r\nnode "%~dp0..\\Versions\\' + validVersion(version) +
19
- '\\bin\\localcommander.mjs" %*\r\nexit /b %errorlevel%\r\n';
20
- }
21
-
22
- // Visible launcher for the Start menu shortcut. PowerShell hosts node directly so Ctrl+C
23
- // ends the monitor without cmd's "Terminate batch job" prompt; the window stays open with
24
- // local guidance only when the monitor could not start or ended with an error.
25
- export function monitorWrapper(version) {
26
- return WRAPPER_MARKER + [
27
- "# 'Continue' keeps monitor stderr lines (for example unavailable snapshots) from ending the view when output is redirected.",
28
- "$ErrorActionPreference = 'Continue'",
29
- "try { $Host.UI.RawUI.WindowTitle = '" + SHORTCUT_NAME + "' } catch {}",
30
- "$cli = [IO.Path]::Combine([IO.Path]::GetDirectoryName($PSScriptRoot), 'Versions', '" + validVersion(version) + "', 'bin', 'localcommander.mjs')",
31
- "$code = 1",
32
- "try {",
33
- " if (-not (Test-Path -LiteralPath $cli -PathType Leaf)) { throw 'LOCAL_COMMANDER_CLI_MISSING' }",
34
- " & node $cli monitor --watch",
35
- " $code = $LASTEXITCODE",
36
- "} catch { Write-Host ('' + $_) }",
37
- "if ($code -ne 0) {",
38
- " Write-Host ''",
39
- " Write-Host 'Local Commander Monitor could not start or ended with an error.'",
40
- " Write-Host 'Node.js 20+ must be installed and Local Commander must be installed for this Windows user.'",
41
- " Write-Host 'Check the installation in a terminal: npx local-commander-agent@latest status'",
42
- " Write-Host 'Reinstall or update: npx local-commander-agent@latest update'",
43
- " Write-Host 'Help: https://localcmd.app/support'",
44
- " Write-Host ''",
45
- " Read-Host 'Press Enter to close this window' | Out-Null",
46
- "}",
47
- "exit $code",
48
- ""
49
- ].join("\r\n");
50
- }
51
-
52
- async function plainDirectory(directory) {
53
- await fs.mkdir(directory, { recursive: true, mode: 0o700 });
54
- const stat = await fs.lstat(directory);
55
- if (!stat.isDirectory() || stat.isSymbolicLink()) throw Error("INSTALL_DIRECTORY_INVALID");
56
- }
57
- async function regularOrMissing(file) {
58
- const stat = await fs.lstat(file).catch(error => { if (error.code !== "ENOENT") throw error; });
59
- if (stat && (!stat.isFile() || stat.isSymbolicLink())) throw Error("INSTALL_STATE_INVALID");
60
- }
61
-
62
- export async function installCommandFiles(packageRoot, installRoot, version) {
63
- const shim = commandShim(version);
64
- const runtime = path.join(installRoot, "Versions", version);
65
- const bin = path.join(installRoot, "bin");
66
- for (const directory of [installRoot, path.join(installRoot, "Versions"), runtime, path.join(runtime, "bin"), bin]) await plainDirectory(directory);
67
- for (const file of CLI_FILES) {
68
- const source = path.join(packageRoot, file), target = path.join(runtime, file);
69
- await regularOrMissing(source);
70
- await regularOrMissing(target);
71
- if (path.resolve(source).toLowerCase() !== path.resolve(target).toLowerCase()) await fs.copyFile(source, target);
72
- }
73
- await writeManaged(path.join(bin, "localcommander.cmd"), SHIM_MARKER, shim);
74
- await writeManaged(path.join(bin, MONITOR_WRAPPER), WRAPPER_MARKER, monitorWrapper(version));
75
- return bin;
76
- }
77
-
78
- // Only files that still start with their Local Commander marker are replaced; a user-owned
79
- // file with the same name is left untouched and reported.
80
- async function writeManaged(target, marker, content) {
81
- await regularOrMissing(target);
82
- const previous = await fs.readFile(target, "utf8").catch(error => { if (error.code !== "ENOENT") throw error; });
83
- if (previous !== undefined && !previous.startsWith(marker)) throw Error("COMMAND_SHIM_CONFLICT");
84
- await fs.writeFile(target, content, { mode: 0o600 });
85
- }
86
-
87
- export async function removeCommandFile(installRoot) {
88
- for (const directory of [installRoot, path.join(installRoot, "bin")]) {
89
- const stat = await fs.lstat(directory).catch(error => { if (error.code !== "ENOENT") throw error; });
90
- if (!stat) return;
91
- if (!stat.isDirectory() || stat.isSymbolicLink()) throw Error("INSTALL_DIRECTORY_INVALID");
92
- }
93
- for (const [name, marker] of [["localcommander.cmd", SHIM_MARKER], [MONITOR_WRAPPER, WRAPPER_MARKER]]) {
94
- const file = path.join(installRoot, "bin", name);
95
- await regularOrMissing(file);
96
- const content = await fs.readFile(file, "utf8").catch(error => { if (error.code !== "ENOENT") throw error; });
97
- if (content?.startsWith(marker)) await fs.unlink(file);
98
- }
99
- }
100
-
101
- // The per-user Start menu entry targets the stable managed wrapper, so it survives version
102
- // updates and npm cache removal. Ownership is recorded in the shortcut description; an
103
- // unrelated shortcut with the same display name is never overwritten or removed.
104
- export const START_MENU_SCRIPT = String.raw`
105
- $ErrorActionPreference = 'Stop'
106
- $marker = 'Local Commander managed shortcut'
107
- $folderName = if ($env:LC_SHORTCUT_FOLDER) { $env:LC_SHORTCUT_FOLDER } else { 'Programs' }
108
- $programs = [Environment]::GetFolderPath($folderName)
109
- if (-not $programs) { throw 'START_MENU_UNAVAILABLE' }
110
- $link = Join-Path $programs ($env:LC_SHORTCUT_NAME + '.lnk')
111
- $shell = New-Object -ComObject WScript.Shell
112
- $exists = Test-Path -LiteralPath $link
113
- if ($exists) {
114
- $item = Get-Item -LiteralPath $link -Force
115
- if ($item.PSIsContainer -or ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)) { throw 'SHORTCUT_CONFLICT' }
116
- if ($shell.CreateShortcut($link).Description -ne $marker) { throw 'SHORTCUT_CONFLICT' }
117
- }
118
- if ($env:LC_SHORTCUT_REMOVE -eq '1') {
119
- if ($exists) { Remove-Item -LiteralPath $link -Force }
120
- } else {
121
- $shortcut = $shell.CreateShortcut($link)
122
- $shortcut.TargetPath = $env:LC_SHORTCUT_TARGET
123
- $shortcut.Arguments = $env:LC_SHORTCUT_ARGUMENTS
124
- $shortcut.WorkingDirectory = $env:LC_SHORTCUT_DIRECTORY
125
- $shortcut.Description = $marker
126
- $shortcut.WindowStyle = 1
127
- $shortcut.Save()
128
- }
129
- Write-Output 'SHORTCUT_READY'
130
- `;
131
-
132
- function powershellPath() {
133
- return path.join(process.env.SystemRoot || "C:\\Windows", "System32/WindowsPowerShell/v1.0/powershell.exe");
134
- }
135
-
136
- function runStartMenuScript(env) {
137
- const result = spawnSync(powershellPath(),
138
- ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(START_MENU_SCRIPT, "utf16le").toString("base64")],
139
- { windowsHide: true, encoding: "utf8", timeout: 15_000, env: { ...process.env, LC_SHORTCUT_NAME: SHORTCUT_NAME, ...env } });
140
- if (result.error) throw Error("SHORTCUT_UPDATE_FAILED");
141
- if (result.status === 0 && result.stdout.trim() === "SHORTCUT_READY") return;
142
- throw Error(/SHORTCUT_CONFLICT/.test(result.stderr) ? "SHORTCUT_CONFLICT" : "SHORTCUT_UPDATE_FAILED");
143
- }
144
-
145
- export function installStartMenuShortcut(bin, workingDirectory = os.homedir()) {
146
- const wrapper = path.join(bin, MONITOR_WRAPPER);
147
- if (!path.isAbsolute(bin) || /["\r\n\0]/.test(bin) || !path.isAbsolute(workingDirectory) || /["\r\n\0]/.test(workingDirectory)) throw Error("COMMAND_PATH_INVALID");
148
- runStartMenuScript({
149
- LC_SHORTCUT_REMOVE: "0",
150
- LC_SHORTCUT_TARGET: powershellPath(),
151
- LC_SHORTCUT_ARGUMENTS: '-NoLogo -NoProfile -ExecutionPolicy Bypass -File "' + wrapper + '"',
152
- LC_SHORTCUT_DIRECTORY: workingDirectory
153
- });
154
- return SHORTCUT_NAME;
155
- }
156
-
157
- export function removeStartMenuShortcut() {
158
- runStartMenuScript({ LC_SHORTCUT_REMOVE: "1" });
159
- }
160
-
161
- // A named shortcut (Start menu + Desktop) that launches a browser already routed through the proxy.
162
- export function installBrowserShortcut(name, target, args, workingDirectory = os.homedir()) {
163
- if (!name || /["\r\n\0\\/]/.test(name) || !path.isAbsolute(target) || /["\r\n\0]/.test(target) || /[\r\n\0]/.test(args)) throw Error("SHORTCUT_PARAM_INVALID");
164
- for (const folder of ["Programs", "Desktop"]) runStartMenuScript({
165
- LC_SHORTCUT_FOLDER: folder, LC_SHORTCUT_NAME: name, LC_SHORTCUT_REMOVE: "0",
166
- LC_SHORTCUT_TARGET: target, LC_SHORTCUT_ARGUMENTS: args, LC_SHORTCUT_DIRECTORY: workingDirectory
167
- });
168
- return name;
169
- }
170
-
171
- export function removeBrowserShortcut(name) {
172
- for (const folder of ["Programs", "Desktop"]) { try { runStartMenuScript({ LC_SHORTCUT_FOLDER: folder, LC_SHORTCUT_NAME: name, LC_SHORTCUT_REMOVE: "1" }); } catch { /* best effort */ } }
173
- }
174
-
175
- // Read the registry's raw value so %VARIABLE% entries and its value type survive.
176
- export const USER_PATH_SCRIPT = String.raw`
177
- $ErrorActionPreference = 'Stop'
178
- $target = $env:LC_COMMAND_BIN
179
- $remove = $env:LC_COMMAND_PATH_REMOVE -eq '1'
180
- $key = [Microsoft.Win32.Registry]::CurrentUser.CreateSubKey('Environment')
181
- try {
182
- $raw = $key.GetValue('Path', '', [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
183
- if ($raw -isnot [string]) { throw 'USER_PATH_INVALID' }
184
- $kind = [Microsoft.Win32.RegistryValueKind]::ExpandString
185
- if ($key.GetValueNames() -contains 'Path') { $kind = $key.GetValueKind('Path') }
186
- if ($kind -ne [Microsoft.Win32.RegistryValueKind]::String -and $kind -ne [Microsoft.Win32.RegistryValueKind]::ExpandString) { throw 'USER_PATH_INVALID' }
187
- $entries = $raw.Split(';')
188
- $kept = @($entries | Where-Object {
189
- $expanded = [Environment]::ExpandEnvironmentVariables($_.Trim().Trim('"')).Replace('/', '\').TrimEnd('\')
190
- -not [string]::Equals($expanded, $target.TrimEnd('\'), [StringComparison]::OrdinalIgnoreCase)
191
- })
192
- $next = $raw
193
- if ($remove) { $next = [string]::Join(';', $kept) }
194
- elseif ($kept.Count -eq $entries.Count) {
195
- $separator = if ($raw.Length -gt 0 -and -not $raw.EndsWith(';')) { ';' } else { '' }
196
- $next = $raw + $separator + $target
197
- }
198
- if ($next.Length -gt 32766) { throw 'USER_PATH_TOO_LONG' }
199
- if ($next -cne $raw) { $key.SetValue('Path', $next, $kind) }
200
- } finally { $key.Dispose() }
201
- Add-Type -TypeDefinition @'
202
- using System;
203
- using System.Runtime.InteropServices;
204
- public static class LocalCommanderEnvironment {
205
- [DllImport("user32.dll", CharSet=CharSet.Unicode, SetLastError=true)]
206
- public static extern IntPtr SendMessageTimeout(IntPtr window, uint message, IntPtr word, string value, uint flags, uint timeout, out UIntPtr result);
207
- }
208
- '@
209
- $result = [UIntPtr]::Zero
210
- [void][LocalCommanderEnvironment]::SendMessageTimeout([IntPtr]0xffff, 0x1a, [IntPtr]::Zero, 'Environment', 2, 1000, [ref]$result)
211
- Write-Output 'USER_PATH_READY'
212
- `;
213
-
214
- export function updateUserPath(bin, remove = false) {
215
- if (!path.isAbsolute(bin) || /[;"\r\n\0]/.test(bin)) throw Error("COMMAND_PATH_INVALID");
216
- // The settings broadcast waits up to 1 s per top-level window; a busy desktop took 19 s (2026-10-17).
217
- const result = spawnSync(powershellPath(),
218
- ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(USER_PATH_SCRIPT, "utf16le").toString("base64")], {
219
- windowsHide: true, encoding: "utf8", timeout: 60_000,
220
- env: { ...process.env, LC_COMMAND_BIN: bin, LC_COMMAND_PATH_REMOVE: remove ? "1" : "0" }
221
- });
222
- if (result.error || result.status !== 0 || result.stdout.trim() !== "USER_PATH_READY") throw Error("USER_PATH_UPDATE_FAILED");
223
- }
1
+ import fs from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { spawnSync } from "node:child_process";
5
+
6
+ const SHIM_MARKER = "@rem Local Commander managed command\r\n";
7
+ const WRAPPER_MARKER = "# Local Commander managed command\r\n";
8
+ const MONITOR_WRAPPER = "localcommander-monitor.ps1";
9
+ const SHORTCUT_NAME = "Local Commander Monitor";
10
+ const CLI_FILES = ["package.json", "bin/localcommander.mjs", "bin/monitor.mjs", "bin/monitor-agents.mjs", "bin/monitor-context.mjs", "bin/monitor-mcp.mjs", "bin/monitor-savings.mjs", "bin/command-path.mjs", "bin/network-package.mjs"];
11
+
12
+ function validVersion(version) {
13
+ if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(version)) throw Error("PACKAGE_VERSION_INVALID");
14
+ return version;
15
+ }
16
+
17
+ export function commandShim(version) {
18
+ return SHIM_MARKER + '@echo off\r\nsetlocal DisableDelayedExpansion\r\nnode "%~dp0..\\Versions\\' + validVersion(version) +
19
+ '\\bin\\localcommander.mjs" %*\r\nexit /b %errorlevel%\r\n';
20
+ }
21
+
22
+ // Visible launcher for the Start menu shortcut. PowerShell hosts node directly so Ctrl+C
23
+ // ends the monitor without cmd's "Terminate batch job" prompt; the window stays open with
24
+ // local guidance only when the monitor could not start or ended with an error.
25
+ export function monitorWrapper(version) {
26
+ return WRAPPER_MARKER + [
27
+ "# 'Continue' keeps monitor stderr lines (for example unavailable snapshots) from ending the view when output is redirected.",
28
+ "$ErrorActionPreference = 'Continue'",
29
+ "try { $Host.UI.RawUI.WindowTitle = '" + SHORTCUT_NAME + "' } catch {}",
30
+ "$cli = [IO.Path]::Combine([IO.Path]::GetDirectoryName($PSScriptRoot), 'Versions', '" + validVersion(version) + "', 'bin', 'localcommander.mjs')",
31
+ "$code = 1",
32
+ "try {",
33
+ " if (-not (Test-Path -LiteralPath $cli -PathType Leaf)) { throw 'LOCAL_COMMANDER_CLI_MISSING' }",
34
+ " & node $cli monitor --watch",
35
+ " $code = $LASTEXITCODE",
36
+ "} catch { Write-Host ('' + $_) }",
37
+ "if ($code -ne 0) {",
38
+ " Write-Host ''",
39
+ " Write-Host 'Local Commander Monitor could not start or ended with an error.'",
40
+ " Write-Host 'Node.js 20+ must be installed and Local Commander must be installed for this Windows user.'",
41
+ " Write-Host 'Check the installation in a terminal: npx local-commander-agent@latest status'",
42
+ " Write-Host 'Reinstall or update: npx local-commander-agent@latest update'",
43
+ " Write-Host 'Help: https://localcmd.app/support'",
44
+ " Write-Host ''",
45
+ " Read-Host 'Press Enter to close this window' | Out-Null",
46
+ "}",
47
+ "exit $code",
48
+ ""
49
+ ].join("\r\n");
50
+ }
51
+
52
+ async function plainDirectory(directory) {
53
+ await fs.mkdir(directory, { recursive: true, mode: 0o700 });
54
+ const stat = await fs.lstat(directory);
55
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw Error("INSTALL_DIRECTORY_INVALID");
56
+ }
57
+ async function regularOrMissing(file) {
58
+ const stat = await fs.lstat(file).catch(error => { if (error.code !== "ENOENT") throw error; });
59
+ if (stat && (!stat.isFile() || stat.isSymbolicLink())) throw Error("INSTALL_STATE_INVALID");
60
+ }
61
+
62
+ export async function installCommandFiles(packageRoot, installRoot, version) {
63
+ const shim = commandShim(version);
64
+ const runtime = path.join(installRoot, "Versions", version);
65
+ const bin = path.join(installRoot, "bin");
66
+ for (const directory of [installRoot, path.join(installRoot, "Versions"), runtime, path.join(runtime, "bin"), bin]) await plainDirectory(directory);
67
+ for (const file of CLI_FILES) {
68
+ const source = path.join(packageRoot, file), target = path.join(runtime, file);
69
+ await regularOrMissing(source);
70
+ await regularOrMissing(target);
71
+ if (path.resolve(source).toLowerCase() !== path.resolve(target).toLowerCase()) await fs.copyFile(source, target);
72
+ }
73
+ await writeManaged(path.join(bin, "localcommander.cmd"), SHIM_MARKER, shim);
74
+ await writeManaged(path.join(bin, MONITOR_WRAPPER), WRAPPER_MARKER, monitorWrapper(version));
75
+ return bin;
76
+ }
77
+
78
+ // Only files that still start with their Local Commander marker are replaced; a user-owned
79
+ // file with the same name is left untouched and reported.
80
+ async function writeManaged(target, marker, content) {
81
+ await regularOrMissing(target);
82
+ const previous = await fs.readFile(target, "utf8").catch(error => { if (error.code !== "ENOENT") throw error; });
83
+ if (previous !== undefined && !previous.startsWith(marker)) throw Error("COMMAND_SHIM_CONFLICT");
84
+ await fs.writeFile(target, content, { mode: 0o600 });
85
+ }
86
+
87
+ export async function removeCommandFile(installRoot) {
88
+ for (const directory of [installRoot, path.join(installRoot, "bin")]) {
89
+ const stat = await fs.lstat(directory).catch(error => { if (error.code !== "ENOENT") throw error; });
90
+ if (!stat) return;
91
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw Error("INSTALL_DIRECTORY_INVALID");
92
+ }
93
+ for (const [name, marker] of [["localcommander.cmd", SHIM_MARKER], [MONITOR_WRAPPER, WRAPPER_MARKER]]) {
94
+ const file = path.join(installRoot, "bin", name);
95
+ await regularOrMissing(file);
96
+ const content = await fs.readFile(file, "utf8").catch(error => { if (error.code !== "ENOENT") throw error; });
97
+ if (content?.startsWith(marker)) await fs.unlink(file);
98
+ }
99
+ }
100
+
101
+ // The per-user Start menu entry targets the stable managed wrapper, so it survives version
102
+ // updates and npm cache removal. Ownership is recorded in the shortcut description; an
103
+ // unrelated shortcut with the same display name is never overwritten or removed.
104
+ export const START_MENU_SCRIPT = String.raw`
105
+ $ErrorActionPreference = 'Stop'
106
+ $marker = 'Local Commander managed shortcut'
107
+ $folderName = if ($env:LC_SHORTCUT_FOLDER) { $env:LC_SHORTCUT_FOLDER } else { 'Programs' }
108
+ $programs = [Environment]::GetFolderPath($folderName)
109
+ if (-not $programs) { throw 'START_MENU_UNAVAILABLE' }
110
+ $link = Join-Path $programs ($env:LC_SHORTCUT_NAME + '.lnk')
111
+ $shell = New-Object -ComObject WScript.Shell
112
+ $exists = Test-Path -LiteralPath $link
113
+ if ($exists) {
114
+ $item = Get-Item -LiteralPath $link -Force
115
+ if ($item.PSIsContainer -or ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)) { throw 'SHORTCUT_CONFLICT' }
116
+ if ($shell.CreateShortcut($link).Description -ne $marker) { throw 'SHORTCUT_CONFLICT' }
117
+ }
118
+ if ($env:LC_SHORTCUT_REMOVE -eq '1') {
119
+ if ($exists) { Remove-Item -LiteralPath $link -Force }
120
+ } else {
121
+ $shortcut = $shell.CreateShortcut($link)
122
+ $shortcut.TargetPath = $env:LC_SHORTCUT_TARGET
123
+ $shortcut.Arguments = $env:LC_SHORTCUT_ARGUMENTS
124
+ $shortcut.WorkingDirectory = $env:LC_SHORTCUT_DIRECTORY
125
+ $shortcut.Description = $marker
126
+ $shortcut.WindowStyle = 1
127
+ $shortcut.Save()
128
+ }
129
+ Write-Output 'SHORTCUT_READY'
130
+ `;
131
+
132
+ function powershellPath() {
133
+ return path.join(process.env.SystemRoot || "C:\\Windows", "System32/WindowsPowerShell/v1.0/powershell.exe");
134
+ }
135
+
136
+ function runStartMenuScript(env) {
137
+ const result = spawnSync(powershellPath(),
138
+ ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(START_MENU_SCRIPT, "utf16le").toString("base64")],
139
+ { windowsHide: true, encoding: "utf8", timeout: 15_000, env: { ...process.env, LC_SHORTCUT_NAME: SHORTCUT_NAME, ...env } });
140
+ if (result.error) throw Error("SHORTCUT_UPDATE_FAILED");
141
+ if (result.status === 0 && result.stdout.trim() === "SHORTCUT_READY") return;
142
+ throw Error(/SHORTCUT_CONFLICT/.test(result.stderr) ? "SHORTCUT_CONFLICT" : "SHORTCUT_UPDATE_FAILED");
143
+ }
144
+
145
+ export function installStartMenuShortcut(bin, workingDirectory = os.homedir()) {
146
+ const wrapper = path.join(bin, MONITOR_WRAPPER);
147
+ if (!path.isAbsolute(bin) || /["\r\n\0]/.test(bin) || !path.isAbsolute(workingDirectory) || /["\r\n\0]/.test(workingDirectory)) throw Error("COMMAND_PATH_INVALID");
148
+ runStartMenuScript({
149
+ LC_SHORTCUT_REMOVE: "0",
150
+ LC_SHORTCUT_TARGET: powershellPath(),
151
+ LC_SHORTCUT_ARGUMENTS: '-NoLogo -NoProfile -ExecutionPolicy Bypass -File "' + wrapper + '"',
152
+ LC_SHORTCUT_DIRECTORY: workingDirectory
153
+ });
154
+ return SHORTCUT_NAME;
155
+ }
156
+
157
+ export function removeStartMenuShortcut() {
158
+ runStartMenuScript({ LC_SHORTCUT_REMOVE: "1" });
159
+ }
160
+
161
+ // A named shortcut (Start menu + Desktop) that launches a browser already routed through the proxy.
162
+ export function installBrowserShortcut(name, target, args, workingDirectory = os.homedir()) {
163
+ if (!name || /["\r\n\0\\/]/.test(name) || !path.isAbsolute(target) || /["\r\n\0]/.test(target) || /[\r\n\0]/.test(args)) throw Error("SHORTCUT_PARAM_INVALID");
164
+ for (const folder of ["Programs", "Desktop"]) runStartMenuScript({
165
+ LC_SHORTCUT_FOLDER: folder, LC_SHORTCUT_NAME: name, LC_SHORTCUT_REMOVE: "0",
166
+ LC_SHORTCUT_TARGET: target, LC_SHORTCUT_ARGUMENTS: args, LC_SHORTCUT_DIRECTORY: workingDirectory
167
+ });
168
+ return name;
169
+ }
170
+
171
+ export function removeBrowserShortcut(name) {
172
+ for (const folder of ["Programs", "Desktop"]) { try { runStartMenuScript({ LC_SHORTCUT_FOLDER: folder, LC_SHORTCUT_NAME: name, LC_SHORTCUT_REMOVE: "1" }); } catch { /* best effort */ } }
173
+ }
174
+
175
+ // Read the registry's raw value so %VARIABLE% entries and its value type survive.
176
+ export const USER_PATH_SCRIPT = String.raw`
177
+ $ErrorActionPreference = 'Stop'
178
+ $target = $env:LC_COMMAND_BIN
179
+ $remove = $env:LC_COMMAND_PATH_REMOVE -eq '1'
180
+ $key = [Microsoft.Win32.Registry]::CurrentUser.CreateSubKey('Environment')
181
+ try {
182
+ $raw = $key.GetValue('Path', '', [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
183
+ if ($raw -isnot [string]) { throw 'USER_PATH_INVALID' }
184
+ $kind = [Microsoft.Win32.RegistryValueKind]::ExpandString
185
+ if ($key.GetValueNames() -contains 'Path') { $kind = $key.GetValueKind('Path') }
186
+ if ($kind -ne [Microsoft.Win32.RegistryValueKind]::String -and $kind -ne [Microsoft.Win32.RegistryValueKind]::ExpandString) { throw 'USER_PATH_INVALID' }
187
+ $entries = $raw.Split(';')
188
+ $kept = @($entries | Where-Object {
189
+ $expanded = [Environment]::ExpandEnvironmentVariables($_.Trim().Trim('"')).Replace('/', '\').TrimEnd('\')
190
+ -not [string]::Equals($expanded, $target.TrimEnd('\'), [StringComparison]::OrdinalIgnoreCase)
191
+ })
192
+ $next = $raw
193
+ if ($remove) { $next = [string]::Join(';', $kept) }
194
+ elseif ($kept.Count -eq $entries.Count) {
195
+ $separator = if ($raw.Length -gt 0 -and -not $raw.EndsWith(';')) { ';' } else { '' }
196
+ $next = $raw + $separator + $target
197
+ }
198
+ if ($next.Length -gt 32766) { throw 'USER_PATH_TOO_LONG' }
199
+ if ($next -cne $raw) { $key.SetValue('Path', $next, $kind) }
200
+ } finally { $key.Dispose() }
201
+ Add-Type -TypeDefinition @'
202
+ using System;
203
+ using System.Runtime.InteropServices;
204
+ public static class LocalCommanderEnvironment {
205
+ [DllImport("user32.dll", CharSet=CharSet.Unicode, SetLastError=true)]
206
+ public static extern IntPtr SendMessageTimeout(IntPtr window, uint message, IntPtr word, string value, uint flags, uint timeout, out UIntPtr result);
207
+ }
208
+ '@
209
+ $result = [UIntPtr]::Zero
210
+ [void][LocalCommanderEnvironment]::SendMessageTimeout([IntPtr]0xffff, 0x1a, [IntPtr]::Zero, 'Environment', 2, 1000, [ref]$result)
211
+ Write-Output 'USER_PATH_READY'
212
+ `;
213
+
214
+ export function updateUserPath(bin, remove = false) {
215
+ if (!path.isAbsolute(bin) || /[;"\r\n\0]/.test(bin)) throw Error("COMMAND_PATH_INVALID");
216
+ // The settings broadcast waits up to 1 s per top-level window; a busy desktop took 19 s (2026-10-17).
217
+ const result = spawnSync(powershellPath(),
218
+ ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(USER_PATH_SCRIPT, "utf16le").toString("base64")], {
219
+ windowsHide: true, encoding: "utf8", timeout: 60_000,
220
+ env: { ...process.env, LC_COMMAND_BIN: bin, LC_COMMAND_PATH_REMOVE: remove ? "1" : "0" }
221
+ });
222
+ if (result.error || result.status !== 0 || result.stdout.trim() !== "USER_PATH_READY") throw Error("USER_PATH_UPDATE_FAILED");
223
+ }