Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
02e05a5
refactor: extract private gallery services
PrzemyslawKlys Mar 11, 2026
418462f
refactor: share private module workflow
PrzemyslawKlys Mar 11, 2026
341b72a
refactor: extract nuget certificate export service
PrzemyslawKlys Mar 11, 2026
0e55bd8
refactor: extract module test failure workflow
PrzemyslawKlys Mar 11, 2026
81e3cd1
refactor: extract powershell compatibility workflow
PrzemyslawKlys Mar 11, 2026
5309325
refactor: extract scaffold helper services
PrzemyslawKlys Mar 11, 2026
17cbcf1
refactor: extract repository release summary service
PrzemyslawKlys Mar 11, 2026
065494f
refactor: extract module build outcome service
PrzemyslawKlys Mar 11, 2026
06bb07c
refactor: extract project cleanup display service
PrzemyslawKlys Mar 11, 2026
8f3c2aa
refactor: extract project consistency display service
PrzemyslawKlys Mar 11, 2026
f18bef6
refactor: extract module test suite display service
PrzemyslawKlys Mar 11, 2026
2e760c2
refactor: extract more cmdlet display services
PrzemyslawKlys Mar 11, 2026
9759944
docs: clarify thin-cmdlet stopping rule
PrzemyslawKlys Mar 11, 2026
b2bc360
refactor: extract repository release display service
PrzemyslawKlys Mar 11, 2026
79ba65f
refactor: extract project build github display service
PrzemyslawKlys Mar 11, 2026
6d55b3e
fix: address private gallery and path regressions
PrzemyslawKlys Mar 11, 2026
ae96cfd
fix: restore private gallery fallback behavior
PrzemyslawKlys Mar 11, 2026
650a6d6
fix: normalize scaffold project paths cross-platform
PrzemyslawKlys Mar 11, 2026
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
47 changes: 46 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Agent Guide (PSPublishModule / PowerForge.Web + Websites)

Last updated: 2026-03-01
Last updated: 2026-03-11

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

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

- Prefer engine fixes over theme hacks when the same issue can recur across sites.
- Keep `PSPublishModule` cmdlets thin:
- parameter binding
- `ShouldProcess` / prompting / PowerShell UX
- output mapping back to PowerShell-facing contract types
- Move reusable logic into shared services first:
- `PowerForge` for host-agnostic logic
- `PowerForge.PowerShell` for logic that still needs PowerShell-host/runtime concepts
- 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.
- 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.
- CI/release should fail on regressions; dev should warn and summarize:
- Verify: use baselines + `failOnNewWarnings:true` in CI.
- Audit: use baselines + `failOnNewIssues:true` in CI.
- Prefer stable theme helpers over ad-hoc rendering:
- Scriban: use `pf.nav_links` / `pf.nav_actions` / `pf.menu_tree` (avoid `navigation.menus[0]`).
- Commit frequently. Avoid "big bang" diffs that mix unrelated changes.

## Module Layering

When touching the PowerShell module stack, prefer this boundary:

- `PSPublishModule\Cmdlets\`
- PowerShell-only surface area
- minimal orchestration
- no reusable build/publish/install rules unless they are truly cmdlet-specific
- `PSPublishModule\Services\`
- cmdlet host adapters or PowerShell-facing compatibility mappers only
- `PowerForge\`
- reusable domain logic, pipelines, models, filesystem/process/network orchestration
- `PowerForge.PowerShell\`
- reusable services that still depend on PowerShell-host concepts, module registration, manifest editing, or other SMA-adjacent behavior

Quick smell test before adding code to a cmdlet:

1. Could this be called from a test, CLI, Studio app, or another C# host?
2. Could two cmdlets share it?
3. Does it manipulate files, versions, dependencies, repositories, GitHub, NuGet, or build plans?

If the answer to any of those is yes, the code probably belongs in `PowerForge` or `PowerForge.PowerShell`, not directly in the cmdlet.

Stop extracting when the remaining code is only:

- PowerShell parameter binding and parameter-set branching
- `ShouldProcess`, `WhatIf`, credential prompts, and PowerShell stream routing
- host-only rendering such as `Host.UI.Write*`, Spectre.Console tables/rules, or pipeline-friendly `WriteObject` behavior
- compatibility adapters that intentionally map shared models back to stable cmdlet-facing contracts

Preferred pattern for the last 10-20%:

- extract reusable workflow, validation, planning, summary-shaping, and display-line composition into `PowerForge` / `PowerForge.PowerShell`
- keep the final host-specific rendering in the cmdlet when the rendering technology itself is PowerShell- or Spectre-specific
- avoid creating fake abstractions just to move `AnsiConsole.Write`, `Host.UI.WriteLine`, or `WriteObject` calls out of cmdlets

## Quality Gates (Pattern)

Each website should have:
Expand Down
2 changes: 1 addition & 1 deletion Module/Docs/Export-CertificateForNuGet.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable

## OUTPUTS

- `System.Object`
- `PowerForge.NuGetCertificateExportResult`

## RELATED LINKS

Expand Down
2 changes: 1 addition & 1 deletion Module/en-US/PSPublishModule-help.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1213,7 +1213,7 @@ This cmdlet exports the selected certificate from the local certificate store.</
<command:returnValues>
<command:returnValue>
<dev:type>
<maml:name>System.Object</maml:name>
<maml:name>PowerForge.NuGetCertificateExportResult</maml:name>
</dev:type>
</command:returnValue>
</command:returnValues>
Expand Down
27 changes: 14 additions & 13 deletions PSPublishModule/Cmdlets/ConnectModuleRepositoryCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,18 +93,19 @@ public sealed class ConnectModuleRepositoryCommand : PSCmdlet
/// <summary>Executes the connect/login workflow.</summary>
protected override void ProcessRecord()
{
PrivateGalleryCommandSupport.EnsureProviderSupported(Provider);
var host = new CmdletPrivateGalleryHost(this);
var service = new PrivateGalleryService(host);
service.EnsureProviderSupported(Provider);

var endpoint = AzureArtifactsRepositoryEndpoints.Create(
AzureDevOpsOrganization,
AzureDevOpsProject,
AzureArtifactsFeed,
Name);
var prerequisiteInstall = PrivateGalleryCommandSupport.EnsureBootstrapPrerequisites(this, InstallPrerequisites.IsPresent);
var allowInteractivePrompt = !PrivateGalleryCommandSupport.IsWhatIfRequested(this);
var prerequisiteInstall = service.EnsureBootstrapPrerequisites(InstallPrerequisites.IsPresent);
var allowInteractivePrompt = !host.IsWhatIfRequested;

var credentialResolution = PrivateGalleryCommandSupport.ResolveCredential(
this,
var credentialResolution = service.ResolveCredential(
endpoint.RepositoryName,
BootstrapMode,
CredentialUserName,
Expand All @@ -114,8 +115,7 @@ protected override void ProcessRecord()
prerequisiteInstall.Status,
allowInteractivePrompt);

var result = PrivateGalleryCommandSupport.EnsureAzureArtifactsRepositoryRegistered(
this,
var result = service.EnsureAzureArtifactsRepositoryRegistered(
AzureDevOpsOrganization,
AzureDevOpsProject,
AzureArtifactsFeed,
Expand All @@ -136,18 +136,19 @@ protected override void ProcessRecord()

if (!result.RegistrationPerformed)
{
PrivateGalleryCommandSupport.WriteRegistrationSummary(this, result);
WriteObject(result);
service.WriteRegistrationSummary(result);
WriteObject(ModuleRepositoryRegistrationResultMapper.ToCmdletResult(result));
return;
}

var probe = PrivateGalleryCommandSupport.ProbeRepositoryAccess(result, credentialResolution.Credential);
var probe = service.ProbeRepositoryAccess(result, credentialResolution.Credential);
result.AccessProbePerformed = true;
result.AccessProbeSucceeded = probe.Succeeded;
result.AccessProbeTool = probe.Tool;
result.AccessProbeMessage = probe.Message;

PrivateGalleryCommandSupport.WriteRegistrationSummary(this, result);
service.WriteRegistrationSummary(result);
var cmdletResult = ModuleRepositoryRegistrationResultMapper.ToCmdletResult(result);

if (!probe.Succeeded)
{
Expand All @@ -160,10 +161,10 @@ protected override void ProcessRecord()
exception,
"ConnectModuleRepositoryProbeFailed",
ErrorCategory.OpenError,
result.RepositoryName));
cmdletResult.RepositoryName));
return;
}

WriteObject(result);
WriteObject(cmdletResult);
}
}
42 changes: 10 additions & 32 deletions PSPublishModule/Cmdlets/ConvertProjectConsistencyCommand.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using System;
using System.Collections;
using System.Globalization;
using System.IO;
using System.Collections.Generic;
using System.Management.Automation;
using PowerForge;

Expand Down Expand Up @@ -166,7 +165,12 @@ protected override void ProcessRecord()
if (shouldProcess)
{
var result = service.ConvertAndAnalyze(request);
WriteSummary(result.RootPath, result.Report, result.EncodingConversion, result.LineEndingConversion);
WriteDisplayLines(new ProjectConsistencyDisplayService().CreateSummary(
result.RootPath,
result.Report,
result.EncodingConversion,
result.LineEndingConversion,
ExportPath));
WriteObject(new ProjectConsistencyConversionResult(result.Report, result.EncodingConversion, result.LineEndingConversion));
return;
}
Expand All @@ -179,36 +183,10 @@ protected override void ProcessRecord()
null));
}

private void WriteSummary(string root, ProjectConsistencyReport report, ProjectConversionResult? encodingResult, ProjectConversionResult? lineEndingResult)
private void WriteDisplayLines(IReadOnlyList<ProjectConsistencyDisplayLine> lines)
{
var s = report.Summary;
HostWriteLineSafe("Project Consistency Conversion", ConsoleColor.Cyan);
HostWriteLineSafe($"Project: {root}");
HostWriteLineSafe($"Target encoding: {s.RecommendedEncoding}");
HostWriteLineSafe($"Target line ending: {s.RecommendedLineEnding}");

if (encodingResult is not null)
HostWriteLineSafe(
$"Encoding conversion: {encodingResult.Converted}/{encodingResult.Total} converted, {encodingResult.Skipped} skipped, {encodingResult.Errors} errors",
encodingResult.Errors == 0 ? ConsoleColor.Green : ConsoleColor.Red);

if (lineEndingResult is not null)
HostWriteLineSafe(
$"Line ending conversion: {lineEndingResult.Converted}/{lineEndingResult.Total} converted, {lineEndingResult.Skipped} skipped, {lineEndingResult.Errors} errors",
lineEndingResult.Errors == 0 ? ConsoleColor.Green : ConsoleColor.Red);

HostWriteLineSafe("");
HostWriteLineSafe("Consistency summary:", ConsoleColor.Cyan);
HostWriteLineSafe(
$" Files compliant: {s.FilesCompliant} ({s.CompliancePercentage.ToString("0.0", CultureInfo.InvariantCulture)}%)",
s.CompliancePercentage >= 90 ? ConsoleColor.Green : s.CompliancePercentage >= 70 ? ConsoleColor.Yellow : ConsoleColor.Red);
HostWriteLineSafe($" Files needing attention: {s.FilesWithIssues}", s.FilesWithIssues == 0 ? ConsoleColor.Green : ConsoleColor.Red);

if (!string.IsNullOrWhiteSpace(ExportPath) && File.Exists(ExportPath!))
{
HostWriteLineSafe("");
HostWriteLineSafe($"Detailed report exported to: {ExportPath}", ConsoleColor.Green);
}
foreach (var line in lines)
HostWriteLineSafe(line.Text, line.Color);
}

private void HostWriteLineSafe(string text, ConsoleColor? fg = null)
Expand Down
Loading
Loading