diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Constants/ErrorCodes.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Constants/ErrorCodes.cs
index 55f3df5..d820c44 100644
--- a/src/Microsoft.Agents.A365.DevTools.Cli/Constants/ErrorCodes.cs
+++ b/src/Microsoft.Agents.A365.DevTools.Cli/Constants/ErrorCodes.cs
@@ -16,7 +16,10 @@ public static class ErrorCodes
public const string DeploymentScopesFailed = "DEPLOYMENT_SCOPES_FAILED";
public const string DeploymentMcpFailed = "DEPLOYMENT_MCP_FAILED";
public const string HighPrivilegeScopeDetected = "HIGH_PRIVILEGE_SCOPE_DETECTED";
- public const string SetupValidationFailed = "SETUP_VALIDATION_FAILED";
+ public const string NodeBuildFailed = "NODE_BUILD_FAILED";
+ public const string NodeDependencyInstallFailed = "NODE_DEPENDENCY_INSTALL_FAILED";
+ public const string NodeProjectNotFound = "NODE_PROJECT_NOT_FOUND";
public const string RetryExhausted = "RETRY_EXHAUSTED";
+ public const string SetupValidationFailed = "SETUP_VALIDATION_FAILED";
}
}
diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Exceptions/NodeBuildFailedException.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Exceptions/NodeBuildFailedException.cs
new file mode 100644
index 0000000..fac3c97
--- /dev/null
+++ b/src/Microsoft.Agents.A365.DevTools.Cli/Exceptions/NodeBuildFailedException.cs
@@ -0,0 +1,57 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Microsoft.Agents.A365.DevTools.Cli.Constants;
+
+namespace Microsoft.Agents.A365.DevTools.Cli.Exceptions;
+
+///
+/// Thrown when the build process of a Node.js project fails.
+///
+public sealed class NodeBuildFailedException : Agent365Exception
+{
+ public override int ExitCode => 1;
+ public override bool IsUserError => true;
+
+ public NodeBuildFailedException(string projectDirectory, string? npmErrorOutput)
+ : base(
+ errorCode: ErrorCodes.NodeBuildFailed,
+ issueDescription: "Failed to build the Node.js project using 'npm run build'.",
+ errorDetails: BuildDetails(projectDirectory, npmErrorOutput),
+ mitigationSteps: new List
+ {
+ "Run 'npm run build' locally in the project directory and fix any TypeScript/webpack/build errors.",
+ "Verify that the 'build' script is defined correctly in package.json.",
+ "If the build depends on environment variables or private packages, ensure those are configured on the machine running 'a365 deploy'.",
+ "After resolving the build issues, rerun 'a365 deploy'."
+ },
+ context: new Dictionary
+ {
+ ["ProjectDirectory"] = projectDirectory
+ })
+ {
+ }
+
+ private static List BuildDetails(string projectDirectory, string? npmErrorOutput)
+ {
+ var details = new List
+ {
+ $"Project directory: {projectDirectory}",
+ };
+
+ if (!string.IsNullOrWhiteSpace(npmErrorOutput))
+ {
+ details.Add("npm build error output (truncated):");
+ details.Add($" {TrimError(npmErrorOutput)}");
+ }
+
+ return details;
+ }
+
+ private static string TrimError(string error)
+ {
+ const int maxLen = 400;
+ error = error.Trim();
+ return error.Length <= maxLen ? error : error[..maxLen] + " ...";
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Exceptions/NodeDependencyInstallException.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Exceptions/NodeDependencyInstallException.cs
new file mode 100644
index 0000000..a2ba9d5
--- /dev/null
+++ b/src/Microsoft.Agents.A365.DevTools.Cli/Exceptions/NodeDependencyInstallException.cs
@@ -0,0 +1,57 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Microsoft.Agents.A365.DevTools.Cli.Constants;
+
+namespace Microsoft.Agents.A365.DevTools.Cli.Exceptions;
+
+///
+/// Thrown when installation of Node.js dependencies fails.
+///
+public class NodeDependencyInstallException : Agent365Exception
+{
+ public override int ExitCode => 1;
+ public override bool IsUserError => true;
+
+ public NodeDependencyInstallException(string projectDirectory, string? npmErrorOutput)
+ : base(
+ errorCode: ErrorCodes.NodeDependencyInstallFailed,
+ issueDescription: "Failed to install Node.js dependencies for the project.",
+ errorDetails: BuildDetails(projectDirectory, npmErrorOutput),
+ mitigationSteps: new List
+ {
+ "Run 'npm install' (or 'npm ci') locally in the project directory and fix any errors.",
+ "Check that your internet connection and npm registry access are working.",
+ "If you use a private registry or npm auth, ensure those settings are configured on the machine running 'a365 deploy'.",
+ "After fixing the issue, rerun 'a365 deploy'."
+ },
+ context: new Dictionary
+ {
+ ["ProjectDirectory"] = projectDirectory
+ })
+ {
+ }
+
+ private static List BuildDetails(string projectDirectory, string? npmErrorOutput)
+ {
+ var details = new List
+ {
+ $"Project directory: {projectDirectory}",
+ };
+
+ if (!string.IsNullOrWhiteSpace(npmErrorOutput))
+ {
+ details.Add("npm error output (truncated):");
+ details.Add($" {TrimError(npmErrorOutput)}");
+ }
+
+ return details;
+ }
+
+ private static string TrimError(string error)
+ {
+ const int maxLen = 400;
+ error = error.Trim();
+ return error.Length <= maxLen ? error : error[..maxLen] + " ...";
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Exceptions/NodeProjectNotFoundException.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Exceptions/NodeProjectNotFoundException.cs
new file mode 100644
index 0000000..9ed5d7e
--- /dev/null
+++ b/src/Microsoft.Agents.A365.DevTools.Cli/Exceptions/NodeProjectNotFoundException.cs
@@ -0,0 +1,37 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Microsoft.Agents.A365.DevTools.Cli.Constants;
+
+namespace Microsoft.Agents.A365.DevTools.Cli.Exceptions;
+
+///
+/// Thrown when a Node.js deployment is requested from a directory that does not contain a package.json.
+///
+public class NodeProjectNotFoundException : Agent365Exception
+{
+ public override int ExitCode => 1;
+ public override bool IsUserError => true;
+
+ public NodeProjectNotFoundException(string projectDirectory)
+ : base(
+ errorCode: ErrorCodes.NodeProjectNotFound,
+ issueDescription: "No Node.js project was found in the specified directory.",
+ errorDetails: new List
+ {
+ "The deployment expects a package.json file to identify the Node.js project.",
+ $"Checked directory: {projectDirectory}"
+ },
+ mitigationSteps: new List
+ {
+ "Run this command from the root of your Node.js project (where package.json is located), or",
+ "Update the --project-path in your a365 config to point to the folder containing package.json.",
+ "Verify that package.json is checked into source control and not ignored or deleted."
+ },
+ context: new Dictionary
+ {
+ ["ProjectDirectory"] = projectDirectory
+ })
+ {
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Services/NodeBuilder.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Services/NodeBuilder.cs
index 05b6c55..46e9d0e 100644
--- a/src/Microsoft.Agents.A365.DevTools.Cli/Services/NodeBuilder.cs
+++ b/src/Microsoft.Agents.A365.DevTools.Cli/Services/NodeBuilder.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT License.
using System.Text.Json;
+using Microsoft.Agents.A365.DevTools.Cli.Exceptions;
using Microsoft.Agents.A365.DevTools.Cli.Models;
using Microsoft.Agents.A365.DevTools.Cli.Services.Helpers;
using Microsoft.Extensions.Logging;
@@ -77,7 +78,7 @@ public async Task BuildAsync(string projectDir, string outputPath, bool
var packageJsonPath = Path.Combine(projectDir, "package.json");
if (!File.Exists(packageJsonPath))
{
- throw new FileNotFoundException("package.json not found in project directory");
+ throw new NodeProjectNotFoundException(projectDir);
}
// Install dependencies
@@ -89,7 +90,7 @@ public async Task BuildAsync(string projectDir, string outputPath, bool
installResult = await _helper.ExecuteWithOutputAsync("npm", "install", projectDir, verbose);
if (!installResult.Success)
{
- throw new Exception($"npm install failed: {installResult.StandardError}");
+ throw new NodeDependencyInstallException(projectDir, installResult.StandardError);
}
}
@@ -103,7 +104,7 @@ public async Task BuildAsync(string projectDir, string outputPath, bool
var buildResult = await _helper.ExecuteWithOutputAsync("npm", "run build", projectDir, verbose);
if (!buildResult.Success)
{
- throw new Exception($"npm run build failed: {buildResult.StandardError}");
+ throw new NodeBuildFailedException(projectDir, buildResult.StandardError);
}
}
else
@@ -145,7 +146,7 @@ public async Task BuildAsync(string projectDir, string outputPath, bool
CopyDirectory(srcDir, Path.Combine(publishPath, "src"));
}
- // Copy server files (.js files in root)
+ // Copy server/entry files (.js/.ts files in root)
foreach (var jsFile in Directory.GetFiles(projectDir, "*.js"))
{
File.Copy(jsFile, Path.Combine(publishPath, Path.GetFileName(jsFile)));
@@ -155,6 +156,17 @@ public async Task BuildAsync(string projectDir, string outputPath, bool
File.Copy(tsFile, Path.Combine(publishPath, Path.GetFileName(tsFile)));
}
+ var distDir = Path.Combine(projectDir, "dist");
+ if (Directory.Exists(distDir))
+ {
+ _logger.LogInformation("Found dist folder, copying to publish output...");
+ CopyDirectory(distDir, Path.Combine(publishPath, "dist"));
+ }
+ else
+ {
+ _logger.LogInformation("No dist folder found in project; relying on Oryx build (if configured) to produce runtime output.");
+ }
+
// Create .deployment file to force Oryx build during Azure deployment
await CreateDeploymentFile(publishPath);
@@ -166,6 +178,11 @@ public async Task CreateManifestAsync(string projectDir, string pu
_logger.LogInformation("Creating Oryx manifest for Node.js...");
var packageJsonPath = Path.Combine(projectDir, "package.json");
+ if (!File.Exists(packageJsonPath))
+ {
+ throw new NodeProjectNotFoundException(projectDir);
+ }
+
var packageJson = await File.ReadAllTextAsync(packageJsonPath);
// Parse package.json to detect start command and version
@@ -177,8 +194,7 @@ public async Task CreateManifestAsync(string projectDir, string pu
if (root.TryGetProperty("engines", out var engines) &&
engines.TryGetProperty("node", out var nodeVersionProp))
{
- var versionString = nodeVersionProp.GetString() ?? "18";
- // Extract major version (e.g., "18.x" -> "18")
+ var versionString = nodeVersionProp.GetString() ?? "20";
var match = System.Text.RegularExpressions.Regex.Match(versionString, @"(\d+)");
if (match.Success)
{
@@ -192,7 +208,13 @@ public async Task CreateManifestAsync(string projectDir, string pu
if (root.TryGetProperty("scripts", out var scripts) &&
scripts.TryGetProperty("start", out var startScript))
{
- startCommand = startScript.GetString() ?? startCommand;
+ var scriptValue = startScript.GetString();
+ if (!string.IsNullOrWhiteSpace(scriptValue))
+ {
+ // Use the script literally, same as package.json
+ startCommand = scriptValue;
+ }
+
_logger.LogInformation("Detected start command from package.json: {Command}", startCommand);
}
else if (root.TryGetProperty("main", out var mainProp))
@@ -203,25 +225,38 @@ public async Task CreateManifestAsync(string projectDir, string pu
}
else
{
- // Look for common entry point files
+ // Look for common entry point files under publish path
var commonEntryPoints = new[] { "server.js", "app.js", "index.js", "main.js" };
foreach (var entryPoint in commonEntryPoints)
{
if (File.Exists(Path.Combine(publishPath, entryPoint)))
{
startCommand = $"node {entryPoint}";
- _logger.LogInformation("Detected entry point: {Command}", startCommand);
+ _logger.LogInformation("Detected entry point in publish folder: {Command}", startCommand);
break;
}
}
}
- var buildCommand = "npm run build";
- var hasBuildScript = scripts.TryGetProperty("build", out var buildScript);
- if (hasBuildScript)
+ // Detect build command – strictly Node
+ string buildCommand = "";
+ bool buildRequired = false;
+
+ if (root.TryGetProperty("scripts", out var scriptsForBuild) &&
+ scriptsForBuild.TryGetProperty("build", out var buildScript))
+ {
+ var buildValue = buildScript.GetString();
+ if (!string.IsNullOrWhiteSpace(buildValue))
+ {
+ // We always call through npm so it picks up the script from package.json
+ buildCommand = "npm run build";
+ buildRequired = true;
+ _logger.LogInformation("Detected build script; using Oryx build command: {Command}", buildCommand);
+ }
+ }
+ else
{
- buildCommand = buildScript.GetString() ?? buildCommand;
- _logger.LogInformation("Detected build command from package.json: {Command}", buildCommand);
+ _logger.LogInformation("No build script found; Oryx will only run npm install.");
}
return new OryxManifest
@@ -229,8 +264,8 @@ public async Task CreateManifestAsync(string projectDir, string pu
Platform = "nodejs",
Version = nodeVersion,
Command = startCommand,
- BuildCommand = hasBuildScript ? buildCommand : "",
- BuildRequired = hasBuildScript
+ BuildCommand = buildCommand,
+ BuildRequired = buildRequired
};
}