Skip to content

Commit 145dcfd

Browse files
refactor: extract private gallery services (#210)
* refactor: extract private gallery services * refactor: share private module workflow * refactor: extract nuget certificate export service * refactor: extract module test failure workflow * refactor: extract powershell compatibility workflow * refactor: extract scaffold helper services * refactor: extract repository release summary service * refactor: extract module build outcome service * refactor: extract project cleanup display service * refactor: extract project consistency display service * refactor: extract module test suite display service * refactor: extract more cmdlet display services * docs: clarify thin-cmdlet stopping rule * refactor: extract repository release display service * refactor: extract project build github display service * fix: address private gallery and path regressions * fix: restore private gallery fallback behavior * fix: normalize scaffold project paths cross-platform
1 parent 3230b39 commit 145dcfd

82 files changed

Lines changed: 4906 additions & 1946 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Agent Guide (PSPublishModule / PowerForge.Web + Websites)
22

3-
Last updated: 2026-03-01
3+
Last updated: 2026-03-11
44

55
This file is the "start here" context for any agent working on the PowerForge.Web engine and the three websites that use it.
66

@@ -74,13 +74,58 @@ need per-user global skill installs.
7474
## Working Agreements (Best Practices)
7575

7676
- Prefer engine fixes over theme hacks when the same issue can recur across sites.
77+
- Keep `PSPublishModule` cmdlets thin:
78+
- parameter binding
79+
- `ShouldProcess` / prompting / PowerShell UX
80+
- output mapping back to PowerShell-facing contract types
81+
- Move reusable logic into shared services first:
82+
- `PowerForge` for host-agnostic logic
83+
- `PowerForge.PowerShell` for logic that still needs PowerShell-host/runtime concepts
84+
- Do not add new business logic to `PSPublishModule\Cmdlets\` when the same behavior could be reused by `PowerForge.Cli`, PowerForge Studio, tests, or another C# host.
85+
- If a public PSPublishModule result type must stay stable, keep the reusable internal model in `PowerForge`/`PowerForge.PowerShell` and map it back in `PSPublishModule` instead of forcing cmdlet-specific types into shared layers.
7786
- CI/release should fail on regressions; dev should warn and summarize:
7887
- Verify: use baselines + `failOnNewWarnings:true` in CI.
7988
- Audit: use baselines + `failOnNewIssues:true` in CI.
8089
- Prefer stable theme helpers over ad-hoc rendering:
8190
- Scriban: use `pf.nav_links` / `pf.nav_actions` / `pf.menu_tree` (avoid `navigation.menus[0]`).
8291
- Commit frequently. Avoid "big bang" diffs that mix unrelated changes.
8392

93+
## Module Layering
94+
95+
When touching the PowerShell module stack, prefer this boundary:
96+
97+
- `PSPublishModule\Cmdlets\`
98+
- PowerShell-only surface area
99+
- minimal orchestration
100+
- no reusable build/publish/install rules unless they are truly cmdlet-specific
101+
- `PSPublishModule\Services\`
102+
- cmdlet host adapters or PowerShell-facing compatibility mappers only
103+
- `PowerForge\`
104+
- reusable domain logic, pipelines, models, filesystem/process/network orchestration
105+
- `PowerForge.PowerShell\`
106+
- reusable services that still depend on PowerShell-host concepts, module registration, manifest editing, or other SMA-adjacent behavior
107+
108+
Quick smell test before adding code to a cmdlet:
109+
110+
1. Could this be called from a test, CLI, Studio app, or another C# host?
111+
2. Could two cmdlets share it?
112+
3. Does it manipulate files, versions, dependencies, repositories, GitHub, NuGet, or build plans?
113+
114+
If the answer to any of those is yes, the code probably belongs in `PowerForge` or `PowerForge.PowerShell`, not directly in the cmdlet.
115+
116+
Stop extracting when the remaining code is only:
117+
118+
- PowerShell parameter binding and parameter-set branching
119+
- `ShouldProcess`, `WhatIf`, credential prompts, and PowerShell stream routing
120+
- host-only rendering such as `Host.UI.Write*`, Spectre.Console tables/rules, or pipeline-friendly `WriteObject` behavior
121+
- compatibility adapters that intentionally map shared models back to stable cmdlet-facing contracts
122+
123+
Preferred pattern for the last 10-20%:
124+
125+
- extract reusable workflow, validation, planning, summary-shaping, and display-line composition into `PowerForge` / `PowerForge.PowerShell`
126+
- keep the final host-specific rendering in the cmdlet when the rendering technology itself is PowerShell- or Spectre-specific
127+
- avoid creating fake abstractions just to move `AnsiConsole.Write`, `Host.UI.WriteLine`, or `WriteObject` calls out of cmdlets
128+
84129
## Quality Gates (Pattern)
85130

86131
Each website should have:

Module/Docs/Export-CertificateForNuGet.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable
114114

115115
## OUTPUTS
116116

117-
- `System.Object`
117+
- `PowerForge.NuGetCertificateExportResult`
118118

119119
## RELATED LINKS
120120

Module/en-US/PSPublishModule-help.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1213,7 +1213,7 @@ This cmdlet exports the selected certificate from the local certificate store.</
12131213
<command:returnValues>
12141214
<command:returnValue>
12151215
<dev:type>
1216-
<maml:name>System.Object</maml:name>
1216+
<maml:name>PowerForge.NuGetCertificateExportResult</maml:name>
12171217
</dev:type>
12181218
</command:returnValue>
12191219
</command:returnValues>

PSPublishModule/Cmdlets/ConnectModuleRepositoryCommand.cs

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -93,18 +93,19 @@ public sealed class ConnectModuleRepositoryCommand : PSCmdlet
9393
/// <summary>Executes the connect/login workflow.</summary>
9494
protected override void ProcessRecord()
9595
{
96-
PrivateGalleryCommandSupport.EnsureProviderSupported(Provider);
96+
var host = new CmdletPrivateGalleryHost(this);
97+
var service = new PrivateGalleryService(host);
98+
service.EnsureProviderSupported(Provider);
9799

98100
var endpoint = AzureArtifactsRepositoryEndpoints.Create(
99101
AzureDevOpsOrganization,
100102
AzureDevOpsProject,
101103
AzureArtifactsFeed,
102104
Name);
103-
var prerequisiteInstall = PrivateGalleryCommandSupport.EnsureBootstrapPrerequisites(this, InstallPrerequisites.IsPresent);
104-
var allowInteractivePrompt = !PrivateGalleryCommandSupport.IsWhatIfRequested(this);
105+
var prerequisiteInstall = service.EnsureBootstrapPrerequisites(InstallPrerequisites.IsPresent);
106+
var allowInteractivePrompt = !host.IsWhatIfRequested;
105107

106-
var credentialResolution = PrivateGalleryCommandSupport.ResolveCredential(
107-
this,
108+
var credentialResolution = service.ResolveCredential(
108109
endpoint.RepositoryName,
109110
BootstrapMode,
110111
CredentialUserName,
@@ -114,8 +115,7 @@ protected override void ProcessRecord()
114115
prerequisiteInstall.Status,
115116
allowInteractivePrompt);
116117

117-
var result = PrivateGalleryCommandSupport.EnsureAzureArtifactsRepositoryRegistered(
118-
this,
118+
var result = service.EnsureAzureArtifactsRepositoryRegistered(
119119
AzureDevOpsOrganization,
120120
AzureDevOpsProject,
121121
AzureArtifactsFeed,
@@ -136,18 +136,19 @@ protected override void ProcessRecord()
136136

137137
if (!result.RegistrationPerformed)
138138
{
139-
PrivateGalleryCommandSupport.WriteRegistrationSummary(this, result);
140-
WriteObject(result);
139+
service.WriteRegistrationSummary(result);
140+
WriteObject(ModuleRepositoryRegistrationResultMapper.ToCmdletResult(result));
141141
return;
142142
}
143143

144-
var probe = PrivateGalleryCommandSupport.ProbeRepositoryAccess(result, credentialResolution.Credential);
144+
var probe = service.ProbeRepositoryAccess(result, credentialResolution.Credential);
145145
result.AccessProbePerformed = true;
146146
result.AccessProbeSucceeded = probe.Succeeded;
147147
result.AccessProbeTool = probe.Tool;
148148
result.AccessProbeMessage = probe.Message;
149149

150-
PrivateGalleryCommandSupport.WriteRegistrationSummary(this, result);
150+
service.WriteRegistrationSummary(result);
151+
var cmdletResult = ModuleRepositoryRegistrationResultMapper.ToCmdletResult(result);
151152

152153
if (!probe.Succeeded)
153154
{
@@ -160,10 +161,10 @@ protected override void ProcessRecord()
160161
exception,
161162
"ConnectModuleRepositoryProbeFailed",
162163
ErrorCategory.OpenError,
163-
result.RepositoryName));
164+
cmdletResult.RepositoryName));
164165
return;
165166
}
166167

167-
WriteObject(result);
168+
WriteObject(cmdletResult);
168169
}
169170
}

PSPublishModule/Cmdlets/ConvertProjectConsistencyCommand.cs

Lines changed: 10 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using System;
22
using System.Collections;
3-
using System.Globalization;
4-
using System.IO;
3+
using System.Collections.Generic;
54
using System.Management.Automation;
65
using PowerForge;
76

@@ -166,7 +165,12 @@ protected override void ProcessRecord()
166165
if (shouldProcess)
167166
{
168167
var result = service.ConvertAndAnalyze(request);
169-
WriteSummary(result.RootPath, result.Report, result.EncodingConversion, result.LineEndingConversion);
168+
WriteDisplayLines(new ProjectConsistencyDisplayService().CreateSummary(
169+
result.RootPath,
170+
result.Report,
171+
result.EncodingConversion,
172+
result.LineEndingConversion,
173+
ExportPath));
170174
WriteObject(new ProjectConsistencyConversionResult(result.Report, result.EncodingConversion, result.LineEndingConversion));
171175
return;
172176
}
@@ -179,36 +183,10 @@ protected override void ProcessRecord()
179183
null));
180184
}
181185

182-
private void WriteSummary(string root, ProjectConsistencyReport report, ProjectConversionResult? encodingResult, ProjectConversionResult? lineEndingResult)
186+
private void WriteDisplayLines(IReadOnlyList<ProjectConsistencyDisplayLine> lines)
183187
{
184-
var s = report.Summary;
185-
HostWriteLineSafe("Project Consistency Conversion", ConsoleColor.Cyan);
186-
HostWriteLineSafe($"Project: {root}");
187-
HostWriteLineSafe($"Target encoding: {s.RecommendedEncoding}");
188-
HostWriteLineSafe($"Target line ending: {s.RecommendedLineEnding}");
189-
190-
if (encodingResult is not null)
191-
HostWriteLineSafe(
192-
$"Encoding conversion: {encodingResult.Converted}/{encodingResult.Total} converted, {encodingResult.Skipped} skipped, {encodingResult.Errors} errors",
193-
encodingResult.Errors == 0 ? ConsoleColor.Green : ConsoleColor.Red);
194-
195-
if (lineEndingResult is not null)
196-
HostWriteLineSafe(
197-
$"Line ending conversion: {lineEndingResult.Converted}/{lineEndingResult.Total} converted, {lineEndingResult.Skipped} skipped, {lineEndingResult.Errors} errors",
198-
lineEndingResult.Errors == 0 ? ConsoleColor.Green : ConsoleColor.Red);
199-
200-
HostWriteLineSafe("");
201-
HostWriteLineSafe("Consistency summary:", ConsoleColor.Cyan);
202-
HostWriteLineSafe(
203-
$" Files compliant: {s.FilesCompliant} ({s.CompliancePercentage.ToString("0.0", CultureInfo.InvariantCulture)}%)",
204-
s.CompliancePercentage >= 90 ? ConsoleColor.Green : s.CompliancePercentage >= 70 ? ConsoleColor.Yellow : ConsoleColor.Red);
205-
HostWriteLineSafe($" Files needing attention: {s.FilesWithIssues}", s.FilesWithIssues == 0 ? ConsoleColor.Green : ConsoleColor.Red);
206-
207-
if (!string.IsNullOrWhiteSpace(ExportPath) && File.Exists(ExportPath!))
208-
{
209-
HostWriteLineSafe("");
210-
HostWriteLineSafe($"Detailed report exported to: {ExportPath}", ConsoleColor.Green);
211-
}
188+
foreach (var line in lines)
189+
HostWriteLineSafe(line.Text, line.Color);
212190
}
213191

214192
private void HostWriteLineSafe(string text, ConsoleColor? fg = null)

0 commit comments

Comments
 (0)