plusui-native 0.2.100 → 0.2.103

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plusui-native",
3
- "version": "0.2.100",
3
+ "version": "0.2.103",
4
4
  "description": "PlusUI CLI - Build C++ desktop apps modern UI ",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -27,11 +27,11 @@
27
27
  "semver": "^7.6.0",
28
28
  "which": "^4.0.0",
29
29
  "execa": "^8.0.1",
30
- "plusui-native-builder": "^0.1.98",
31
- "plusui-native-connect": "^0.1.98"
30
+ "plusui-native-builder": "^0.1.101",
31
+ "plusui-native-connect": "^0.1.101"
32
32
  },
33
33
  "peerDependencies": {
34
- "plusui-native-connect": "^0.1.98"
34
+ "plusui-native-connect": "^0.1.101"
35
35
  },
36
36
  "publishConfig": {
37
37
  "access": "public"
@@ -1,4 +1,6 @@
1
1
  import { execSync } from 'child_process';
2
+ import { existsSync } from 'fs';
3
+ import { join, dirname } from 'path';
2
4
  import semver from 'semver';
3
5
 
4
6
  const REQUIRED_JUST_VERSION = '1.0.0';
@@ -16,30 +18,71 @@ async function tryCommand(command) {
16
18
  }
17
19
  }
18
20
 
19
- export async function detectJust() {
20
- const result = await tryCommand('just --version');
21
+ // Known install locations for just.exe on Windows.
22
+ function getJustCandidatePaths() {
23
+ if (process.platform !== 'win32') return [];
24
+ const home = process.env.USERPROFILE || process.env.HOME || '';
25
+ const localApp = process.env.LOCALAPPDATA || join(home, 'AppData', 'Local');
26
+ return [
27
+ join(localApp, 'Microsoft', 'WinGet', 'Links', 'just.exe'),
28
+ join(localApp, 'Microsoft', 'WinGet', 'Packages', 'Casey.Just_Microsoft.Winget.Source_8wekyb3d8bbwe', 'just.exe'),
29
+ 'C:\\Program Files\\just\\just.exe',
30
+ join(home, '.plusui', 'bin', 'just.exe'),
31
+ join(home, '.cargo', 'bin', 'just.exe'),
32
+ join(home, 'scoop', 'shims', 'just.exe'),
33
+ join(home, 'scoop', 'apps', 'just', 'current', 'just.exe'),
34
+ ];
35
+ }
21
36
 
22
- if (!result.success) {
37
+ export async function detectJust() {
38
+ // 1. Try PATH first
39
+ const pathResult = await tryCommand('just --version');
40
+ if (pathResult.success) {
41
+ const versionMatch = pathResult.output.match(/just (\d+\.\d+\.\d+)/);
42
+ const version = versionMatch ? versionMatch[1] : null;
23
43
  return {
24
- found: false,
25
- version: null,
26
- valid: false,
44
+ found: true,
45
+ inPath: true,
46
+ version: version || 'unknown',
47
+ valid: version ? semver.gte(version, REQUIRED_JUST_VERSION) : true,
27
48
  requiredVersion: REQUIRED_JUST_VERSION
28
49
  };
29
50
  }
30
51
 
31
- // Parse version (output format: "just 1.34.0")
32
- const versionMatch = result.output.match(/just (\d+\.\d+\.\d+)/);
33
- const version = versionMatch ? versionMatch[1] : null;
52
+ // 2. On Windows, probe known install locations
53
+ if (process.platform === 'win32') {
54
+ for (const candidate of getJustCandidatePaths()) {
55
+ if (existsSync(candidate)) {
56
+ const result = await tryCommand(`"${candidate}" --version`);
57
+ if (result.success) {
58
+ const versionMatch = result.output.match(/just (\d+\.\d+\.\d+)/);
59
+ const version = versionMatch ? versionMatch[1] : null;
34
60
 
35
- // If we can't parse the version but the command succeeded, assume it's valid enough
36
- // but ideally we check version. Just is usually pretty standard.
37
- const valid = version ? semver.gte(version, REQUIRED_JUST_VERSION) : true;
61
+ // Inject the directory into the current process PATH so subsequent
62
+ // calls (e.g. the Justfile runner) can find just without a new terminal.
63
+ const dir = dirname(candidate);
64
+ if (!process.env.PATH.includes(dir)) {
65
+ process.env.PATH = dir + ';' + process.env.PATH;
66
+ }
67
+
68
+ return {
69
+ found: true,
70
+ inPath: false, // found on disk but not originally in PATH
71
+ foundPath: candidate,
72
+ version: version || 'unknown',
73
+ valid: version ? semver.gte(version, REQUIRED_JUST_VERSION) : true,
74
+ requiredVersion: REQUIRED_JUST_VERSION
75
+ };
76
+ }
77
+ }
78
+ }
79
+ }
38
80
 
39
81
  return {
40
- found: true,
41
- version: version || 'unknown',
42
- valid,
82
+ found: false,
83
+ inPath: false,
84
+ version: null,
85
+ valid: false,
43
86
  requiredVersion: REQUIRED_JUST_VERSION
44
87
  };
45
88
  }
@@ -102,11 +102,16 @@ export class EnvironmentDoctor {
102
102
  console.log(chalk.bold('\nInstallation Results\n'));
103
103
  console.log('====================\n');
104
104
 
105
+ let anyPathRefreshNeeded = false;
106
+
105
107
  for (const result of fixResults) {
106
108
  if (result.success) {
107
- console.log(chalk.green(`✓ ${result.name} installed successfully`));
108
- if (result.pathRefreshNeeded) {
109
- console.log(chalk.gray(' Note: open a new terminal for the updated PATH to take effect'));
109
+ const alreadyHad = result.message && /already installed/i.test(result.message);
110
+ if (alreadyHad) {
111
+ console.log(chalk.green(`✓ ${result.name} already installed and up to date`));
112
+ } else {
113
+ console.log(chalk.green(`✓ ${result.name} installed successfully`));
114
+ if (result.pathRefreshNeeded) anyPathRefreshNeeded = true;
110
115
  }
111
116
  } else {
112
117
  console.log(chalk.red(`✗ ${result.name} installation failed`));
@@ -128,8 +133,18 @@ export class EnvironmentDoctor {
128
133
  }
129
134
  }
130
135
 
136
+ if (anyPathRefreshNeeded) {
137
+ console.log(chalk.yellow('\n ACTION REQUIRED: Refresh your PATH to use the installed tools.'));
138
+ if (this.platform === 'win32') {
139
+ console.log(chalk.white(' Run this in your current terminal to reload PATH without reopening it:'));
140
+ console.log(chalk.cyan('\n $env:PATH = [System.Environment]::GetEnvironmentVariable("PATH","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("PATH","User")\n'));
141
+ } else {
142
+ console.log(chalk.white(' Run: source ~/.bashrc (or open a new terminal)\n'));
143
+ }
144
+ }
145
+
131
146
  // Re-run diagnosis to verify
132
- console.log(chalk.bold('\n\nVerifying installation...\n'));
147
+ console.log(chalk.bold('\nVerifying installation...\n'));
133
148
  const newResults = await this.diagnose();
134
149
 
135
150
  return newResults;
@@ -67,6 +67,35 @@ const TOOL_NAMES = {
67
67
 
68
68
  export async function installTool(toolName) {
69
69
  const name = TOOL_NAMES[toolName] || toolName;
70
+
71
+ // just: use the official install script with prebuilt binaries
72
+ if (toolName === 'just') {
73
+ console.log(`Installing ${name} from prebuilt binaries...`);
74
+ const destDir = `${process.env.HOME || '~'}/.plusui/bin`;
75
+ await tryCommand(`mkdir -p "${destDir}"`);
76
+ const result = await tryCommand(
77
+ `curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to "${destDir}"`,
78
+ 120000
79
+ );
80
+ if (result.success) {
81
+ // Add to PATH in shell profiles
82
+ const addToPath = `grep -qF '.plusui/bin' "$HOME/.bashrc" 2>/dev/null || echo 'export PATH="$HOME/.plusui/bin:$PATH"' >> "$HOME/.bashrc"`;
83
+ await tryCommand(addToPath, 5000);
84
+ // Inject into current process PATH immediately
85
+ if (!process.env.PATH.includes(destDir)) {
86
+ process.env.PATH = destDir + ':' + process.env.PATH;
87
+ }
88
+ return {
89
+ success: true,
90
+ message: `${name} installed successfully`,
91
+ output: result.output,
92
+ pathRefreshNeeded: true
93
+ };
94
+ }
95
+ // Fall through to package manager if script failed
96
+ console.log(` Direct install failed, trying package manager...`);
97
+ }
98
+
70
99
  const pm = await detectPackageManager();
71
100
 
72
101
  if (!pm) {
@@ -90,8 +119,6 @@ export async function installTool(toolName) {
90
119
  }
91
120
 
92
121
  console.log(`Installing ${name} via ${pm}...`);
93
- console.log(`Running: ${command}`);
94
-
95
122
  const result = await tryCommand(command);
96
123
 
97
124
  if (result.success) {
@@ -30,6 +30,9 @@ const INSTALL_COMMANDS = {
30
30
  name: 'Node.js'
31
31
  },
32
32
  just: {
33
+ // Use the official install script with prebuilt binaries as primary method
34
+ installScript: `curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to "$HOME/.plusui/bin"`,
35
+ addToPath: `grep -qF '$HOME/.plusui/bin' "$HOME/.zshrc" 2>/dev/null || echo 'export PATH="$HOME/.plusui/bin:$PATH"' >> "$HOME/.zshrc"; grep -qF '$HOME/.plusui/bin' "$HOME/.bash_profile" 2>/dev/null || echo 'export PATH="$HOME/.plusui/bin:$PATH"' >> "$HOME/.bash_profile"`,
33
36
  command: 'brew install just',
34
37
  manual: 'https://github.com/casey/just/releases',
35
38
  name: 'Just Command Runner'
@@ -72,6 +75,33 @@ export async function installTool(toolName) {
72
75
  };
73
76
  }
74
77
 
78
+ // just: use the official install script with prebuilt binaries
79
+ if (toolName === 'just' && tool.installScript) {
80
+ console.log(`Installing ${tool.name} from prebuilt binaries...`);
81
+ const destDir = `${process.env.HOME || '~'}/.plusui/bin`;
82
+ const mkdirResult = await tryCommand(`mkdir -p "${destDir}"`);
83
+ const result = await tryCommand(
84
+ `curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to "${destDir}"`,
85
+ 120000
86
+ );
87
+ if (result.success) {
88
+ // Add to PATH in shell profiles
89
+ await tryCommand(tool.addToPath, 5000);
90
+ // Inject into current process PATH immediately
91
+ if (!process.env.PATH.includes(destDir)) {
92
+ process.env.PATH = destDir + ':' + process.env.PATH;
93
+ }
94
+ return {
95
+ success: true,
96
+ message: `${tool.name} installed successfully`,
97
+ output: result.output,
98
+ pathRefreshNeeded: true
99
+ };
100
+ }
101
+ // Fall through to Homebrew if script failed
102
+ console.log(` Direct install failed, trying Homebrew...`);
103
+ }
104
+
75
105
  // Check if Homebrew is available
76
106
  const hasHomebrew = await checkHomebrew();
77
107
 
@@ -91,7 +121,7 @@ export async function installTool(toolName) {
91
121
  };
92
122
  }
93
123
 
94
- // Attempt auto-installation
124
+ // Attempt auto-installation via Homebrew
95
125
  console.log(`Installing ${tool.name} via Homebrew...`);
96
126
  const result = await tryCommand(tool.command);
97
127
 
@@ -1,4 +1,7 @@
1
1
  import { execSync } from 'child_process';
2
+ import { existsSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { homedir } from 'os';
2
5
 
3
6
  async function tryCommand(command, timeout = 30000) {
4
7
  try {
@@ -20,40 +23,110 @@ async function checkWinget() {
20
23
  return result.success;
21
24
  }
22
25
 
26
+ // Run winget via cmd shell with output redirected to a temp file so it works
27
+ // in non-interactive (piped) Node child processes where winget goes silent.
28
+ async function tryWinget(args, timeout = 300000) {
29
+ const tmp = join(process.env.TEMP || 'C:\\Temp', `plusui_winget_${Date.now()}.txt`);
30
+ let exitOk = false;
31
+ try {
32
+ execSync(`cmd /c "winget ${args} > "${tmp}" 2>&1"`, { stdio: 'ignore', timeout, shell: false });
33
+ exitOk = true;
34
+ } catch (_) {}
35
+ let output = '';
36
+ try { output = execSync(`type "${tmp}"`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim(); } catch (_) {}
37
+ try { execSync(`del /f /q "${tmp}"`, { stdio: 'ignore' }); } catch (_) {}
38
+ return { success: exitOk, output };
39
+ }
40
+
41
+ // Install just using the official prebuilt binaries from GitHub releases.
42
+ // Uses PowerShell to download and extract — no bash required on Windows.
43
+ async function installJustFromGitHub(destDir) {
44
+ const justExe = join(destDir, 'just.exe');
45
+
46
+ // Ensure destination directory exists
47
+ try { execSync(`cmd /c "mkdir "${destDir}" 2>nul"`, { stdio: 'ignore' }); } catch (_) {}
48
+
49
+ const psLines = [
50
+ `$ErrorActionPreference = 'Stop'`,
51
+ `$dest = '${destDir.replace(/\\/g, '\\\\')}'`,
52
+ `$exe = '${justExe.replace(/\\/g, '\\\\')}'`,
53
+ // Fetch latest release metadata
54
+ `$rel = Invoke-RestMethod -Uri 'https://api.github.com/repos/casey/just/releases/latest' -UseBasicParsing`,
55
+ // Pick the Windows x86_64 MSVC zip
56
+ `$asset = $rel.assets | Where-Object { $_.name -match 'x86_64-pc-windows-msvc\\.zip$' } | Select-Object -First 1`,
57
+ `if (-not $asset) { throw 'No Windows asset found in latest just release' }`,
58
+ `$zip = Join-Path $env:TEMP 'just_install.zip'`,
59
+ `Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $zip -UseBasicParsing`,
60
+ `Expand-Archive -Path $zip -DestinationPath $dest -Force`,
61
+ `Remove-Item $zip -Force`,
62
+ `Write-Host "just installed to $exe"`,
63
+ ];
64
+
65
+ const result = await tryCommand(
66
+ `powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "${psLines.join('; ')}"`,
67
+ 120000
68
+ );
69
+
70
+ if (!result.success || !existsSync(justExe)) {
71
+ return { success: false, output: result.stdout + result.stderr };
72
+ }
73
+
74
+ // Add destDir to user PATH permanently (registry)
75
+ const addToPath = [
76
+ `$p = [Environment]::GetEnvironmentVariable('PATH','User')`,
77
+ `$d = '${destDir.replace(/\\/g, '\\\\')}' `,
78
+ `if ($p -notlike "*$d*") { [Environment]::SetEnvironmentVariable('PATH', ($p.TrimEnd(';') + ';' + $d), 'User') }`,
79
+ ];
80
+ await tryCommand(
81
+ `powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "${addToPath.join('; ')}"`,
82
+ 10000
83
+ );
84
+
85
+ // Also inject into the current process PATH so tools work immediately
86
+ if (!process.env.PATH.includes(destDir)) {
87
+ process.env.PATH = destDir + ';' + process.env.PATH;
88
+ }
89
+
90
+ return { success: true, justExe };
91
+ }
92
+
23
93
  const INSTALL_COMMANDS = {
24
94
  cmake: {
25
- // NOTE: Kitware.CMake does NOT support --scope user; omit it so winget
26
- // uses the package's default (machine) scope. winget will request UAC
27
- // elevation automatically when needed.
28
- command: 'winget install -e --id Kitware.CMake --accept-package-agreements --accept-source-agreements --disable-interactivity',
95
+ wingetId: 'Kitware.CMake',
96
+ wingetArgs: 'install -e --id Kitware.CMake --accept-package-agreements --accept-source-agreements --disable-interactivity',
29
97
  manual: 'https://cmake.org/download/',
30
98
  name: 'CMake',
31
99
  timeout: 300000,
32
100
  requiresElevation: true
33
101
  },
34
102
  nodejs: {
35
- command: 'winget install -e --id OpenJS.NodeJS --scope user --accept-package-agreements --accept-source-agreements --disable-interactivity',
103
+ wingetId: 'OpenJS.NodeJS',
104
+ wingetArgs: 'install -e --id OpenJS.NodeJS --scope user --accept-package-agreements --accept-source-agreements --disable-interactivity',
36
105
  manual: 'https://nodejs.org/',
37
106
  name: 'Node.js',
38
107
  timeout: 300000
39
108
  },
40
109
  just: {
41
- command: 'winget install -e --id Casey.Just --scope user --accept-package-agreements --accept-source-agreements --disable-interactivity',
110
+ wingetId: 'Casey.Just',
111
+ wingetArgs: 'install -e --id Casey.Just --scope user --accept-package-agreements --accept-source-agreements --disable-interactivity',
42
112
  manual: 'https://github.com/casey/just/releases',
43
113
  name: 'Just Command Runner',
44
114
  timeout: 300000
45
115
  },
46
116
  webview2: {
47
- command: 'winget install -e --id Microsoft.EdgeWebView2Runtime --scope user --accept-package-agreements --accept-source-agreements --disable-interactivity',
117
+ wingetId: 'Microsoft.EdgeWebView2Runtime',
118
+ wingetArgs: 'install -e --id Microsoft.EdgeWebView2Runtime --scope user --accept-package-agreements --accept-source-agreements --disable-interactivity',
48
119
  manual: 'https://developer.microsoft.com/en-us/microsoft-edge/webview2/#download-section',
49
120
  name: 'WebView2 Runtime',
50
121
  timeout: 300000
51
122
  },
52
123
  visualstudio: {
53
- command: 'winget install -e --id Microsoft.VisualStudio.2022.BuildTools --accept-package-agreements --accept-source-agreements --disable-interactivity --override "--wait --quiet --norestart --add Microsoft.VisualStudio.Workload.VCTools --add Microsoft.VisualStudio.Component.VC.CMake.Project --add Microsoft.VisualStudio.Component.Windows11SDK.22621"',
124
+ wingetId: 'Microsoft.VisualStudio.2022.BuildTools',
125
+ wingetArgs: 'install -e --id Microsoft.VisualStudio.2022.BuildTools --accept-package-agreements --accept-source-agreements --disable-interactivity --override "--wait --quiet --norestart --add Microsoft.VisualStudio.Workload.VCTools --add Microsoft.VisualStudio.Component.VC.CMake.Project --add Microsoft.VisualStudio.Component.Windows11SDK.22621"',
54
126
  manual: 'https://visualstudio.microsoft.com/downloads/',
55
127
  name: 'Visual Studio 2022',
56
128
  timeout: 3600000,
129
+ requiresElevation: true,
57
130
  instructions: [
58
131
  '1. Download Visual Studio 2022 Build Tools or Community (free)',
59
132
  '2. Run the installer',
@@ -70,61 +143,59 @@ export async function installTool(toolName) {
70
143
  const tool = INSTALL_COMMANDS[toolName];
71
144
 
72
145
  if (!tool) {
73
- return {
74
- success: false,
75
- error: `Unknown tool: ${toolName}`
76
- };
146
+ return { success: false, error: `Unknown tool: ${toolName}` };
77
147
  }
78
148
 
79
- // Check if winget is available
80
- const hasWinget = await checkWinget();
149
+ // just gets its own install path using the official prebuilt binaries
150
+ if (toolName === 'just') {
151
+ return installJust(tool);
152
+ }
81
153
 
82
- if (!hasWinget && tool.command) {
154
+ // All other tools go through winget
155
+ const hasWinget = await checkWinget();
156
+ if (!hasWinget) {
83
157
  return {
84
158
  success: false,
85
- autoInstallAvailable: false,
86
- manual: tool.manual,
87
159
  message: 'winget is not available. Please install manually.',
88
160
  downloadUrl: tool.manual
89
161
  };
90
162
  }
91
163
 
92
- // Attempt auto-installation
93
164
  console.log(`Installing ${tool.name} via winget...`);
94
- const result = await tryCommand(tool.command, tool.timeout || 300000);
165
+ const result = await tryWinget(tool.wingetArgs, tool.timeout || 300000);
95
166
 
96
167
  if (result.success) {
97
- return {
98
- success: true,
99
- message: `${tool.name} installed successfully`,
100
- output: result.output,
101
- // Remind callers that the current process PATH won't include the new
102
- // install – they should re-spawn or check known filesystem paths.
103
- pathRefreshNeeded: true
104
- };
168
+ return { success: true, message: `${tool.name} installed successfully`, output: result.output, pathRefreshNeeded: true };
105
169
  }
106
170
 
107
- // Build a human-readable reason from winget's own output
108
- const reason = result.stderr || result.stdout ||
109
- (result.error && result.error.message) ||
110
- 'Unknown error';
171
+ const alreadyInstalled =
172
+ /No newer package versions are available/i.test(result.output) ||
173
+ /No available upgrade found/i.test(result.output) ||
174
+ /already installed/i.test(result.output);
175
+
176
+ if (alreadyInstalled) {
177
+ const upgradeResult = await tryWinget(
178
+ `upgrade -e --id ${tool.wingetId} --accept-package-agreements --accept-source-agreements --disable-interactivity`,
179
+ tool.timeout || 300000
180
+ );
181
+ if (upgradeResult.success) {
182
+ return { success: true, message: `${tool.name} installed successfully`, output: upgradeResult.output, pathRefreshNeeded: true };
183
+ }
184
+ return { success: true, message: `${tool.name} is already installed and up to date`, output: result.output, pathRefreshNeeded: true };
185
+ }
111
186
 
112
187
  const failResult = {
113
188
  success: false,
114
- autoInstallAvailable: true,
115
- error: result.error,
116
189
  message: `Failed to install ${tool.name} automatically`,
117
- reason,
118
- manual: tool.manual,
190
+ reason: result.output || 'Unknown error',
119
191
  downloadUrl: tool.manual
120
192
  };
121
193
 
122
194
  if (tool.requiresElevation) {
123
195
  failResult.instructions = [
124
196
  'This tool requires administrator rights to install system-wide.',
125
- 'Option 1: Re-run from an elevated (Run as Administrator) terminal:',
126
- ' plusui doctor --fix',
127
- `Option 2: Install manually via winget (elevated): ${tool.command}`,
197
+ 'Option 1: Re-run from an elevated (Run as Administrator) terminal: plusui doctor --fix',
198
+ `Option 2: winget ${tool.wingetArgs}`,
128
199
  `Option 3: Download from: ${tool.manual}`
129
200
  ];
130
201
  }
@@ -132,15 +203,65 @@ export async function installTool(toolName) {
132
203
  return failResult;
133
204
  }
134
205
 
206
+ async function installJust(tool) {
207
+ const destDir = join(homedir(), '.plusui', 'bin');
208
+ console.log(`Installing ${tool.name} from prebuilt binaries...`);
209
+
210
+ // Use the official prebuilt binary from GitHub releases
211
+ const ghResult = await installJustFromGitHub(destDir);
212
+ if (ghResult.success) {
213
+ return {
214
+ success: true,
215
+ message: `${tool.name} installed successfully`,
216
+ output: `Installed to ${ghResult.justExe}`,
217
+ pathRefreshNeeded: true
218
+ };
219
+ }
220
+
221
+ // Fallback: try winget
222
+ console.log(` Direct download failed, trying winget...`);
223
+ const wingetResult = await tryWinget(tool.wingetArgs, tool.timeout || 300000);
224
+ if (wingetResult.success) {
225
+ return { success: true, message: `${tool.name} installed successfully`, output: wingetResult.output, pathRefreshNeeded: true };
226
+ }
227
+
228
+ const wingetSaysInstalled =
229
+ /No newer package versions are available/i.test(wingetResult.output) ||
230
+ /No available upgrade found/i.test(wingetResult.output) ||
231
+ /already installed/i.test(wingetResult.output);
232
+
233
+ if (wingetSaysInstalled) {
234
+ const upgradeResult = await tryWinget(
235
+ `upgrade -e --id ${tool.wingetId} --scope user --accept-package-agreements --accept-source-agreements --disable-interactivity`,
236
+ tool.timeout || 300000
237
+ );
238
+ if (upgradeResult.success) {
239
+ return { success: true, message: `${tool.name} installed successfully`, output: upgradeResult.output, pathRefreshNeeded: true };
240
+ }
241
+ return { success: true, message: `${tool.name} is already installed and up to date`, output: wingetResult.output, pathRefreshNeeded: true };
242
+ }
243
+
244
+ return {
245
+ success: false,
246
+ message: `Failed to install ${tool.name} automatically`,
247
+ reason: ghResult.output || wingetResult.output || 'All install methods failed',
248
+ downloadUrl: tool.manual,
249
+ instructions: [
250
+ `winget install -e --id Casey.Just`,
251
+ `Or download from: ${tool.manual}`,
252
+ `Or via cargo: cargo install just`,
253
+ ]
254
+ };
255
+ }
256
+
135
257
  export function getInstallInstructions(toolName) {
136
258
  const tool = INSTALL_COMMANDS[toolName];
137
259
  if (!tool) return null;
138
-
139
260
  return {
140
261
  name: tool.name,
141
262
  manual: tool.manual,
142
263
  instructions: tool.instructions || [
143
- `Install via winget: ${tool.command}`,
264
+ `Install via winget: winget ${tool.wingetArgs}`,
144
265
  `Or download from: ${tool.manual}`
145
266
  ]
146
267
  };
@@ -72,7 +72,12 @@ export class DoctorReporter {
72
72
  // Just
73
73
  if (results.just.found) {
74
74
  if (results.just.valid) {
75
- output += chalk.green(`✓ Just v${results.just.version}\n`);
75
+ if (results.just.inPath === false && results.just.foundPath) {
76
+ output += chalk.yellow(`⚠ Just v${results.just.version} (installed but not in PATH yet — open a new terminal)\n`);
77
+ output += chalk.gray(` Found at: ${results.just.foundPath}\n`);
78
+ } else {
79
+ output += chalk.green(`✓ Just v${results.just.version}\n`);
80
+ }
76
81
  } else {
77
82
  output += chalk.yellow(`⚠ Just v${results.just.version} (requires >= ${results.just.requiredVersion})\n`);
78
83
  }
@@ -126,8 +131,19 @@ export class DoctorReporter {
126
131
  if (missingTools.length === 0) {
127
132
  output += chalk.green('Status: ✓ Ready to build PlusUI apps!\n');
128
133
  } else {
134
+ const missingNames = missingTools.map(t => {
135
+ switch (t.name) {
136
+ case 'nodejs': return 'Node.js';
137
+ case 'cmake': return 'CMake';
138
+ case 'compiler': return t.info.name || 'C++ Compiler';
139
+ case 'just': return 'Just';
140
+ case 'webview2': return 'WebView2';
141
+ default: return t.name;
142
+ }
143
+ });
129
144
  output += chalk.red(`Status: ✗ Needs Setup\n`);
130
- output += `Missing: ${missingTools.length} required tool(s)\n`;
145
+ output += `Missing: ${missingNames.join(', ')}\n`;
146
+ output += chalk.yellow(`Run: plusui doctor --fix to install automatically\n`);
131
147
  }
132
148
 
133
149
  console.log(output);