Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
77 changes: 37 additions & 40 deletions src/winapp-npm/cli.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
#!/usr/bin/env node

const { generateAddonFiles } = require('./addon-utils');
const { generateCppAddonFiles } = require('./cpp-addon-utils');
const { generateCsAddonFiles } = require('./cs-addon-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 +169,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 +261,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 { checkAndInstallDotnetSdk, 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 checkAndInstallDotnetSdk("10", 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