-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathProgram.cs
More file actions
249 lines (213 loc) · 9.95 KB
/
Copy pathProgram.cs
File metadata and controls
249 lines (213 loc) · 9.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
using System;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.IO;
using Cimian.CLI.Cimipkg.Services;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging.Console;
namespace Cimian.CLI.Cimipkg;
/// <summary>
/// Custom console formatter that outputs clean messages like Go binaries.
/// </summary>
public sealed class CleanConsoleFormatter : ConsoleFormatter
{
public CleanConsoleFormatter() : base("clean") { }
public override void Write<TState>(
in LogEntry<TState> logEntry,
IExternalScopeProvider? scopeProvider,
TextWriter textWriter)
{
var message = logEntry.Formatter?.Invoke(logEntry.State, logEntry.Exception);
if (string.IsNullOrEmpty(message))
return;
textWriter.WriteLine(message);
}
}
class Program
{
private const string Version = "2.0.0";
static async Task<int> Main(string[] args)
{
// Root command
var rootCommand = new RootCommand("cimipkg - Cimian Package Builder")
{
Description = "Build .pkg and .nupkg packages for Cimian deployment"
};
// Global options
var verboseOption = new Option<bool>(
aliases: ["--verbose", "-v"],
description: "Enable verbose logging");
rootCommand.AddGlobalOption(verboseOption);
// Project directory argument
var projectDirArg = new Argument<string>(
name: "project-directory",
description: "Path to the project directory containing build-info.yaml")
{
Arity = ArgumentArity.ZeroOrOne
};
projectDirArg.SetDefaultValue(".");
// Build options
// TODO(pkg-sunset): Remove --pkg option and pkgOption variable
var pkgOption = new Option<bool>(
aliases: ["--pkg"],
description: "Build .pkg format (default is .msi)");
var nupkgOption = new Option<bool>(
aliases: ["--nupkg"],
description: "Build .nupkg format (Chocolatey compatible)");
var intunewinOption = new Option<bool>(
aliases: ["--intunewin"],
description: "Also generate .intunewin for Intune deployment (works with .msi and .nupkg)");
var envOption = new Option<string?>(
aliases: ["--env", "-e"],
description: "Path to .env file containing environment variables");
var signThumbprintOption = new Option<string?>(
aliases: ["--sign-thumbprint"],
description: "Certificate thumbprint for signing (overrides build-info.yaml)");
var signCertOption = new Option<string?>(
aliases: ["--sign-cert"],
description: "Certificate subject name for signing (overrides build-info.yaml)");
var skipImportOption = new Option<bool>(
aliases: ["--skip-import"],
description: "Skip the post-build prompt that offers to run cimiimport on the built package. Useful for CI/CD pipelines.");
// Create command
var createOption = new Option<string?>(
aliases: ["--create", "-c"],
description: "Create a new project structure at the specified path");
// TODO(pkg-sunset): Remove --resign, --resign-cert, --resign-thumbprint (pkg-only feature)
var resignOption = new Option<string?>(
aliases: ["--resign"],
description: "Re-sign an existing .pkg package without recompressing");
var resignCertOption = new Option<string?>(
aliases: ["--resign-cert"],
description: "Certificate name for re-signing");
var resignThumbprintOption = new Option<string?>(
aliases: ["--resign-thumbprint"],
description: "Certificate thumbprint for re-signing");
rootCommand.AddArgument(projectDirArg);
rootCommand.AddOption(pkgOption);
rootCommand.AddOption(nupkgOption);
rootCommand.AddOption(signThumbprintOption);
rootCommand.AddOption(signCertOption);
rootCommand.AddOption(skipImportOption);
rootCommand.AddOption(intunewinOption);
rootCommand.AddOption(envOption);
rootCommand.AddOption(createOption);
rootCommand.AddOption(resignOption);
rootCommand.AddOption(resignCertOption);
rootCommand.AddOption(resignThumbprintOption);
rootCommand.SetHandler((context) =>
{
var verbose = context.ParseResult.GetValueForOption(verboseOption);
var projectDir = context.ParseResult.GetValueForArgument(projectDirArg);
var buildPkg = context.ParseResult.GetValueForOption(pkgOption);
var buildNupkg = context.ParseResult.GetValueForOption(nupkgOption);
var buildIntunewin = context.ParseResult.GetValueForOption(intunewinOption);
var envFile = context.ParseResult.GetValueForOption(envOption);
var createPath = context.ParseResult.GetValueForOption(createOption);
var resignPath = context.ParseResult.GetValueForOption(resignOption);
var resignCert = context.ParseResult.GetValueForOption(resignCertOption);
var resignThumbprint = context.ParseResult.GetValueForOption(resignThumbprintOption);
var signThumbprint = context.ParseResult.GetValueForOption(signThumbprintOption);
var signCert = context.ParseResult.GetValueForOption(signCertOption);
var skipImport = context.ParseResult.GetValueForOption(skipImportOption);
// Set up logging with clean output (like Go binaries)
var logLevel = verbose ? LogLevel.Debug : LogLevel.Information;
using var loggerFactory = LoggerFactory.Create(builder =>
{
builder.SetMinimumLevel(logLevel);
builder.AddConsoleFormatter<CleanConsoleFormatter, ConsoleFormatterOptions>();
builder.AddConsole(options =>
{
options.FormatterName = "clean";
});
});
var logger = loggerFactory.CreateLogger<Program>();
var scriptProcessor = new ScriptProcessor(loggerFactory.CreateLogger<ScriptProcessor>());
var chocolateyGenerator = new ChocolateyGenerator(
loggerFactory.CreateLogger<ChocolateyGenerator>(), scriptProcessor);
var codeSigner = new CodeSigner(loggerFactory.CreateLogger<CodeSigner>());
var zipHelper = new ZipArchiveHelper(loggerFactory.CreateLogger<ZipArchiveHelper>());
var packageBuilder = new PackageBuilder(
loggerFactory.CreateLogger<PackageBuilder>(),
scriptProcessor, chocolateyGenerator, codeSigner, zipHelper);
try
{
// Handle --create
if (!string.IsNullOrEmpty(createPath))
{
logger.LogInformation("Creating new project at: {Path}", createPath);
packageBuilder.CreateNewProject(createPath);
WriteSuccess("New project created successfully!");
context.ExitCode = 0;
return;
}
// TODO(pkg-sunset): Remove --resign handler block (pkg-only feature)
if (!string.IsNullOrEmpty(resignPath))
{
logger.LogInformation("Re-signing package: {Path}", resignPath);
packageBuilder.ResignPackage(resignPath, resignCert, resignThumbprint);
WriteSuccess("Package re-signed successfully!");
context.ExitCode = 0;
return;
}
// Default: build package
if (string.IsNullOrEmpty(projectDir))
{
projectDir = ".";
}
projectDir = Path.GetFullPath(projectDir);
if (!Directory.Exists(projectDir))
{
WriteError($"Project directory not found: {projectDir}");
context.ExitCode = 1;
return;
}
var options = new PackageBuildOptions
{
BuildPkg = buildPkg,
BuildNupkg = buildNupkg,
BuildIntunewin = buildIntunewin,
EnvFilePath = envFile,
Verbose = verbose,
SigningThumbprint = signThumbprint,
SigningCertificate = signCert,
SkipImport = skipImport
};
var packagePath = packageBuilder.Build(projectDir, options);
WriteSuccess($"Package created: {packagePath}");
// Offer to run cimiimport on the built package. Honors
// --skip-import, non-TTY fast path, and a 60s timeout.
// Sync-over-async is fine here: this is a CLI one-shot and
// there's no synchronization context to deadlock on.
ImportPrompter.MaybeRunImportAsync(packagePath, skipImport, logger)
.GetAwaiter().GetResult();
context.ExitCode = 0;
}
catch (Exception ex)
{
WriteError($"Error: {ex.Message}");
if (verbose)
{
logger.LogError(ex, "Stack trace:");
}
context.ExitCode = 1;
}
});
return await rootCommand.InvokeAsync(args);
}
private static void WriteSuccess(string message)
{
var originalColor = Console.ForegroundColor;
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine($"✓ {message}");
Console.ForegroundColor = originalColor;
}
private static void WriteError(string message)
{
var originalColor = Console.ForegroundColor;
Console.ForegroundColor = ConsoleColor.Red;
Console.Error.WriteLine($"✗ {message}");
Console.ForegroundColor = originalColor;
}
}