Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ This file provides focused, actionable information to help an AI coding agent be
Big picture
- Three main components:
- src/winapp-CLI (C#/.NET): the native CLI implemented with System.CommandLine. Key files: `src/winapp-CLI/WinApp.Cli/Program.cs`, `*Commands/*.cs` (e.g. `InitCommand.cs`, `RestoreCommand.cs`, `PackageCommand.cs`, `ToolCommand.cs`). Build with: `dotnet build src/winapp-CLI/winapp.sln`.
- src/winapp-npm (Node): a thin Node wrapper/SDK and CLI (`cli.js`) that forwards most commands to the native CLI. Key helpers: `winapp-cli-utils.js`, `msix-utils.js`, `addon-utils.js`. Install with `npm install` inside `src/winapp-npm` and test the CLI locally with `node cli.js <command>`.
- src/winapp-npm (Node): a thin Node wrapper/SDK and CLI (`cli.js`) that forwards most commands to the native CLI. Key helpers: `winapp-cli-utils.js`, `msix-utils.js`, `cpp-addon-utils.js`. Install with `npm install` inside `src/winapp-npm` and test the CLI locally with `node cli.js <command>`.
- src/winapp-vcpkg (vcpkg ports + sample): contains vcpkg port files and a CMake sample. Build the sample with CMake presets (see `src/winapp-vcpkg/vcpkg_sample/README.md`): `cmake . --preset x64-debug` then `cmake --build out/build/x64-debug`.

Developer workflows (concrete commands)
Expand Down
2 changes: 1 addition & 1 deletion src/winapp-CLI/WinApp.Cli/Services/NugetService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ public async Task<Dictionary<string, string>> InstallPackageAsync(DirectoryInfo
var psi = new ProcessStartInfo
{
FileName = nugetExe,
Arguments = $"install {EscapeArg(package)} -Version {EscapeArg(version)} -OutputDirectory {Quote(outputDir.FullName)} -NonInteractive -ForceEnglishOutput",
Arguments = $"install {EscapeArg(package)} -Version {EscapeArg(version)} -OutputDirectory {Quote(outputDir.FullName)} -Source https://api.nuget.org/v3/index.json -NonInteractive -ForceEnglishOutput",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
Expand Down
78 changes: 38 additions & 40 deletions src/winapp-npm/cli.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
#!/usr/bin/env node

const { generateAddonFiles } = require('./addon-utils');
const { generateCppAddonFiles } = require('./cpp-addon-utils');
const { generateCsAddonFiles } = require('./cs-addon-utils');
const { checkAndInstallDotnet10Sdk, checkAndInstallVisualStudioBuildTools, checkAndInstallPython } = require('./dependency-utils');
const { addElectronDebugIdentity } = require('./msix-utils');
const { getWinappCliPath, callWinappCli, WINAPP_CLI_CALLER_VALUE } = require('./winapp-cli-utils');
const { spawn } = require('child_process');
const { spawn, exec } = require('child_process');
const path = require('path');
const fs = require('fs');
const os = require('os');
Expand Down Expand Up @@ -169,7 +170,8 @@ if (require.main === module) {
module.exports = { main };

async function handleNode(args) {
if (args.length === 0) {
// Handle help flags
if (args.length === 0 || ['--help', '-h', 'help'].includes(args[0])) {
console.log(`Usage: ${CLI_NAME} node <subcommand> [options]`);
console.log('');
console.log('Node.js-specific commands');
Expand Down Expand Up @@ -260,58 +262,54 @@ async function handleCreateAddon(args) {
verbose: options.verbose
});

console.log(`✅ C# addon '${result.addonName}' created successfully!`);
console.log(`📁 ${result.addonPath}`);
console.log('');
console.log(`Next steps:`);
console.log(` 1. npx ${CLI_NAME} restore`);
console.log(` 2. npm run build-${result.addonName}`);
console.log(` 3. See ${result.addonName}/README.md for usage examples`);
} else {
console.log(`New addon at: ${result.addonPath}`);

await callWinappCli(['restore'], { verbose: options.verbose, exitOnError: true });

if (!await pythonExists()) {
console.error(`❌ Python is required to generate C++ addons but was not found in your PATH.`);
console.error(` Please install Python (version 3.10 or later) and ensure it is accessible from the command line.`);
process.exit(1);
console.log('');

if (result.needsTerminalRestart) {
printTerminalRestartInstructions();
}

console.log(`Next steps:`);
console.log(` 1. npm run build-${result.addonName}`);
console.log(` 2. See ${result.addonName}/README.md for usage examples`);

// Use C++ addon generator (existing)
result = await generateAddonFiles({
} else {
// Use C++ addon generator
result = await generateCppAddonFiles({
name: options.name,
verbose: options.verbose
});

console.log(`✅ Addon files generated successfully!`);
console.log(`📦 Addon name: ${result.addonName}`);
console.log(`📁 Addon path: ${result.addonPath}`);
console.log(`🔨 Build with: npm run build-${result.addonName}`);
console.log(`🔨 In your source, import the addon with:`);
console.log(` "const ${result.addonName} = require('./${result.addonName}/build/Release/${result.addonName}.node')";`);
console.log(`New addon at: ${result.addonPath}`);
console.log('');

if (result.needsTerminalRestart) {
printTerminalRestartInstructions();
}

console.log(`Next steps:`);
console.log(` 1. npm run build-${result.addonName}`);
console.log(` 2. In your source, import the addon with:`);
console.log(` "const ${result.addonName} = require('./${result.addonName}/build/Release/${result.addonName}.node')";`);
}
} catch (error) {
console.error(`❌ Failed to generate addon files: ${error.message}`);
process.exit(1);
}
}

function pythonExists() {
const commands = ["python --version", "python3 --version", "py --version"];
function printTerminalRestartInstructions() {
console.log('⚠️ IMPORTANT: You need to restart your terminal/command prompt for newly installed tools to be available in your PATH.');

return new Promise(resolve => {
let index = 0;

function tryNext() {
if (index >= commands.length) return resolve(false);

exec(commands[index], (err) => {
if (!err) return resolve(true);
index++;
tryNext();
});
}

tryNext();
});
// Simple check: This variable usually only exists if running inside PowerShell
if (process.env.PSModulePath) {
console.log('💡 To refresh current session, copy and run this line:');
console.log(' \x1b[36m$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")\x1b[0m');
}
console.log('');
}

async function handleAddonElectronDebugIdentity(args) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const fs = require('fs').promises;
const fsSync = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const { checkAndInstallPython, checkAndInstallVisualStudioBuildTools } = require('./dependency-utils');

/**
* Generates addon files for an Electron project
Expand All @@ -10,18 +11,28 @@ const { execSync } = require('child_process');
* @param {string} options.projectRoot - Root directory of the project (default: current working directory)
* @param {boolean} options.verbose - Enable verbose logging (default: true)
*/
async function generateAddonFiles(options = {}) {
async function generateCppAddonFiles(options = {}) {
const {
name = 'nativeWindowsAddon',
projectRoot = process.cwd(),
verbose = true
} = options;

if (verbose) {
console.log(`🔧 Generating addon files for: ${name}`);
}
let needsTerminalRestart = false;

try {
// Check for Python and offer to install if missing
const pythonInstalled = await checkAndInstallPython(false); // Don't show verbose Python info
if (pythonInstalled) needsTerminalRestart = true;

// Check for Visual Studio Build Tools and offer to install if missing
const vsInstalled = await checkAndInstallVisualStudioBuildTools(false); // Don't show verbose VS info
// VS tools are typically found without PATH restart, so we don't set needsTerminalRestart for it

if (verbose) {
console.log(`🔧 Generating addon files for: ${name}`);
}

// Find a unique addon directory name
const addonDirName = await findUniqueAddonName(name, projectRoot);
const addonDir = path.join(projectRoot, addonDirName);
Expand All @@ -46,6 +57,7 @@ async function generateAddonFiles(options = {}) {
success: true,
addonName: addonDirName,
addonPath: addonDir,
needsTerminalRestart: needsTerminalRestart,
files: [
path.join(addonDir, 'binding.gyp'),
path.join(addonDir, `${addonDirName}.cc`)
Expand Down Expand Up @@ -205,5 +217,5 @@ async function addBuildScript(addonName, projectRoot, verbose) {
}

module.exports = {
generateAddonFiles
generateCppAddonFiles: generateCppAddonFiles
};
41 changes: 10 additions & 31 deletions src/winapp-npm/cs-addon-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const fs = require('fs').promises;
const fsSync = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const { checkAndInstallDotnet10Sdk, checkAndInstallVisualStudioBuildTools } = require('./dependency-utils');

/**
* Generates C# addon files for an Electron project
Expand All @@ -17,12 +18,18 @@ async function generateCsAddonFiles(options = {}) {
verbose = true
} = options;

let needsTerminalRestart = false;

try {
// Validate addon name (should be a valid C# namespace/class name)
validateAddonName(name);

// Check if dotnet SDK is available
await checkDotnetSdk(false); // Don't show verbose SDK info
const vsInstalled = await checkAndInstallVisualStudioBuildTools(false); // Don't show verbose build tools info
// We don't set needsTerminalRestart for VS installation because so far the tools that need it know how to find it.

// Check if dotnet SDK is available and offer to install if missing
const dotnetInstalled = await checkAndInstallDotnet10Sdk(false); // Don't show verbose SDK info
if (dotnetInstalled) needsTerminalRestart = true;

// Check if addon already exists
const addonDir = path.join(projectRoot, name);
Expand Down Expand Up @@ -53,6 +60,7 @@ async function generateCsAddonFiles(options = {}) {
success: true,
addonName: name,
addonPath: addonDir,
needsTerminalRestart: needsTerminalRestart,
files: [
path.join(addonDir, `${name}.csproj`),
path.join(addonDir, 'addon.cs'),
Expand Down Expand Up @@ -101,35 +109,6 @@ function validateAddonName(name) {
}
}

/**
* Checks if dotnet SDK is installed and available
* @param {boolean} verbose - Enable verbose logging
*/
async function checkDotnetSdk(verbose) {
try {
const version = execSync('dotnet --version', {
encoding: 'utf8',
stdio: verbose ? ['pipe', 'pipe', 'inherit'] : 'pipe'
}).trim();

if (verbose) {
console.log(`✅ .NET SDK detected: ${version}`);
}

// Check if it's at least .NET 8.0
const majorVersion = parseInt(version.split('.')[0]);
if (majorVersion < 8) {
throw new Error(`.NET SDK version ${version} detected. Please install .NET 8.0 or later.`);
}

} catch (error) {
if (error.message.includes('not found') || error.message.includes('not recognized')) {
throw new Error('dotnet SDK is not installed or not in PATH. Please install .NET 8.0 SDK from https://dotnet.microsoft.com/download');
}
throw error;
}
}

/**
* Copies and processes template files to the addon directory and project root
* @param {string} addonName - Name of the addon
Expand Down
Loading