Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
32b17d4
Add note, date created, and show subcommand
Trenly Apr 29, 2026
697ca1d
Add tests
Trenly Apr 29, 2026
b6502ef
Add pinning to COM APIs
Trenly Apr 29, 2026
ae483b0
Add IsPinned propery for Get-WingetPackage
Trenly Apr 29, 2026
fac4ec4
Add pinning to PowerShell Cmdlets
Trenly Apr 29, 2026
7ce3b27
Spelling
Trenly Apr 29, 2026
35322e7
Inherit from 1.0 Interface
Trenly Apr 29, 2026
236e667
Contract 29 is not released yet
Trenly Apr 29, 2026
1c89957
Adddress comments about resource strings
Trenly May 1, 2026
fa617a6
Update comment to indicate the appropriate contract version
Trenly May 1, 2026
7f33f56
Make methods public
Trenly May 1, 2026
3c7d4a6
Adjust how new schema is created
Trenly May 1, 2026
99a8760
Reduce duplicated code through inheritance
Trenly May 1, 2026
e336194
Use statement builder, savepoints, and tuples for ownership
Trenly May 1, 2026
0cb653b
Use a time point instead of strings
Trenly May 1, 2026
29d6203
Guard against non-existent source
Trenly May 1, 2026
184cc7a
Cache IsPinned
Trenly May 1, 2026
952e56e
Merge default pin add behavior into single test
Trenly May 1, 2026
e823b75
Ensure table is not created in a partial state
Trenly May 1, 2026
195846f
Add an alternate function for optional parms
Trenly May 1, 2026
0468b34
Move to PackageCatalogReference
Trenly May 1, 2026
4bfde99
Move functionality to converters
Trenly May 1, 2026
243798b
Remove version comment
Trenly May 1, 2026
4b7b8ef
Update filters so 1.1 schema is visible
Trenly May 1, 2026
e93e3d1
Create the correct interface and migrate only if newer
Trenly May 1, 2026
ae87ff1
Use correct version info
Trenly May 1, 2026
917db77
Move invariants outside try catch
Trenly May 1, 2026
34a727c
Add TryRemovePin for simplicity
Trenly May 1, 2026
01aad8e
Refactor to use -Blocking and -GatedVersion
Trenly May 1, 2026
ad9932f
Spelling
Trenly May 1, 2026
a96e43e
Add Pester Tests
Trenly May 1, 2026
fde7382
Tests should now be part of default creation test
Trenly May 1, 2026
d5f682c
Further abstract the interface
Trenly May 1, 2026
8f819c1
Update test name
Trenly May 1, 2026
91f532a
Add comment
Trenly May 1, 2026
595abca
Appease our AI overlords with an update to their tomes
Trenly May 1, 2026
7e83a83
Unify on AssignValue to avoid future confusion
Trenly May 1, 2026
615ddca
Ensure pin writes always record the last updated time
Trenly May 1, 2026
1f8932d
Use original symbol for creation
Trenly May 1, 2026
df55096
Add missing System namepace
Trenly May 1, 2026
6d085dd
Restore conditional setting of date
Trenly May 1, 2026
ea9f0d8
Ensure ShouldProcess is always respected
Trenly May 1, 2026
d207c77
Fix minor issue in copilot instructions
Trenly May 1, 2026
349a0a1
Validate provided pin types
Trenly May 1, 2026
840e5ae
Cache all pins and only fall back to get if set is empty
Trenly May 1, 2026
65f8373
Change file name to remove override
Trenly May 1, 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
2 changes: 2 additions & 0 deletions .github/actions/spelling/expect.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ appdata
appinstallertest
applic
appname
appone
appshutdown
APPTERMINATION
archs
Expand Down Expand Up @@ -339,6 +340,7 @@ megamorf
microsoftentraid
microsoftentraidforazureblobstorage
midl
migratepintable
minidump
MINORVERSION
missingdependency
Expand Down
2 changes: 2 additions & 0 deletions src/AppInstallerCLICore/Argument.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,8 @@ namespace AppInstaller::CLI
return { type, "blocking"_liv, ArgTypeCategory::None, ArgTypeExclusiveSet::PinType };
case Execution::Args::Type::PinInstalled:
return { type, "installed"_liv, ArgTypeCategory::None };
case Execution::Args::Type::PinNote:
return { type, "note"_liv, ArgTypeCategory::None };

// Error command
case Execution::Args::Type::ErrorInput:
Expand Down
34 changes: 34 additions & 0 deletions src/AppInstallerCLICore/Commands/PinCommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ namespace AppInstaller::CLI
std::make_unique<PinRemoveCommand>(FullName()),
std::make_unique<PinListCommand>(FullName()),
std::make_unique<PinResetCommand>(FullName()),
std::make_unique<PinShowCommand>(FullName()),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer that we follow the model of package list and have --details on the existing list command. That also removes the need to create a whole new search mechanism and just makes the change in ReportPins.

Followup: Well it looks like it would need a whole new search mechanism to actually implement the arguments that it claims to support...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we would need a whole new search mechanism anyways. I do think that there is some overlap with winget list <query> --details, but to me it felt more akin to winget show <query>. The semantics of list vs show as a base command don't really apply to pins. Where show in the base command is for a remote package / manifest information, list is for installed applications.

I will think on this and probably end up refactoring to extend the behavior of list instead of adding show

});
}

Expand Down Expand Up @@ -65,6 +66,7 @@ namespace AppInstaller::CLI
Argument::ForType(Args::Type::Force),
Argument{ Args::Type::BlockingPin, Resource::String::PinAddBlockingArgumentDescription, ArgumentType::Flag },
Argument{ Args::Type::PinInstalled, Resource::String::PinInstalledArgumentDescription, ArgumentType::Flag },
Argument{ Args::Type::PinNote, Resource::String::PinNoteArgumentDescription, ArgumentType::Standard },
};
}

Expand Down Expand Up @@ -343,4 +345,36 @@ namespace AppInstaller::CLI
Workflow::ReportPins;
}
}

std::vector<Argument> PinShowCommand::GetArguments() const
{
return {
Argument::ForType(Args::Type::Query),
Argument::ForType(Args::Type::Id),
Argument::ForType(Args::Type::Name),
Argument::ForType(Args::Type::Exact),
};
}

Resource::LocString PinShowCommand::ShortDescription() const
{
return { Resource::String::PinShowCommandShortDescription };
}

Resource::LocString PinShowCommand::LongDescription() const
{
return { Resource::String::PinShowCommandLongDescription };
}

Utility::LocIndView PinShowCommand::HelpLink() const
{
return s_PinCommand_HelpLink;
}

void PinShowCommand::ExecuteInternal(Execution::Context& context) const
{
context <<
Workflow::OpenPinningIndex(/* readOnly */ true) <<
Workflow::ShowPinDetails;
}
}
15 changes: 15 additions & 0 deletions src/AppInstallerCLICore/Commands/PinCommand.h
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,19 @@ namespace AppInstaller::CLI
protected:
void ExecuteInternal(Execution::Context& context) const override;
};

struct PinShowCommand final : public Command
{
PinShowCommand(std::string_view parent) : Command("show", parent) {}

std::vector<Argument> GetArguments() const override;

Resource::LocString ShortDescription() const override;
Resource::LocString LongDescription() const override;

Utility::LocIndView HelpLink() const override;

protected:
void ExecuteInternal(Execution::Context& context) const override;
};
}
1 change: 1 addition & 0 deletions src/AppInstallerCLICore/ExecutionArgs.h
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ namespace AppInstaller::CLI::Execution
GatedVersion, // Differs from Version in that this supports wildcards
BlockingPin,
PinInstalled,
PinNote, // User-provided note to attach to a pin

// Error command
ErrorInput,
Expand Down
12 changes: 12 additions & 0 deletions src/AppInstallerCLICore/Resources.h
Original file line number Diff line number Diff line change
Expand Up @@ -530,13 +530,16 @@ namespace AppInstaller::CLI::Resource
WINGET_DEFINE_RESOURCE_STRINGID(PinCannotOpenIndex);
WINGET_DEFINE_RESOURCE_STRINGID(PinCommandLongDescription);
WINGET_DEFINE_RESOURCE_STRINGID(PinCommandShortDescription);
WINGET_DEFINE_RESOURCE_STRINGID(PinDateAdded);
WINGET_DEFINE_RESOURCE_STRINGID(PinDoesNotExist);
WINGET_DEFINE_RESOURCE_STRINGID(PinExistsOverwriting);
WINGET_DEFINE_RESOURCE_STRINGID(PinExistsUseForceArg);
WINGET_DEFINE_RESOURCE_STRINGID(PinInstalledArgumentDescription);
WINGET_DEFINE_RESOURCE_STRINGID(PinInstalledSource);
WINGET_DEFINE_RESOURCE_STRINGID(PinListCommandLongDescription);
WINGET_DEFINE_RESOURCE_STRINGID(PinListCommandShortDescription);
WINGET_DEFINE_RESOURCE_STRINGID(PinNote);
WINGET_DEFINE_RESOURCE_STRINGID(PinNoteArgumentDescription);
WINGET_DEFINE_RESOURCE_STRINGID(PinNoPinsExist);
WINGET_DEFINE_RESOURCE_STRINGID(PinRemoveCommandLongDescription);
WINGET_DEFINE_RESOURCE_STRINGID(PinRemoveCommandShortDescription);
Expand All @@ -546,6 +549,15 @@ namespace AppInstaller::CLI::Resource
WINGET_DEFINE_RESOURCE_STRINGID(PinResetSuccessful);
WINGET_DEFINE_RESOURCE_STRINGID(PinResettingAll);
WINGET_DEFINE_RESOURCE_STRINGID(PinResetUseForceArg);
WINGET_DEFINE_RESOURCE_STRINGID(PinShowCommandLongDescription);
WINGET_DEFINE_RESOURCE_STRINGID(PinShowCommandShortDescription);
WINGET_DEFINE_RESOURCE_STRINGID(PinShowLabelDateAdded);
WINGET_DEFINE_RESOURCE_STRINGID(PinShowLabelId);
WINGET_DEFINE_RESOURCE_STRINGID(PinShowLabelNote);
WINGET_DEFINE_RESOURCE_STRINGID(PinShowLabelSource);
WINGET_DEFINE_RESOURCE_STRINGID(PinShowLabelType);
WINGET_DEFINE_RESOURCE_STRINGID(PinShowLabelVersion);
WINGET_DEFINE_RESOURCE_STRINGID(PinShowNoMatchFound);
WINGET_DEFINE_RESOURCE_STRINGID(PinType);
WINGET_DEFINE_RESOURCE_STRINGID(PinVersion);
WINGET_DEFINE_RESOURCE_STRINGID(PlatformArgumentDescription);
Expand Down
115 changes: 114 additions & 1 deletion src/AppInstallerCLICore/Workflows/PinFlow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
#include "pch.h"
#include "Resources.h"
#include "PinFlow.h"
#include "ShowFlow.h"
#include "TableOutput.h"
#include <AppInstallerDateTime.h>
#include <winget/PinningData.h>
#include <winget/RepositorySearch.h>
#include <winget/PackageVersionSelection.h>
Expand Down Expand Up @@ -197,9 +199,21 @@ namespace AppInstaller::CLI::Workflow

if (!pinsToAddOrUpdate.empty())
{
for (const auto& pin : pinsToAddOrUpdate)
std::string dateAdded = Utility::TimePointToString(
Comment thread
Trenly marked this conversation as resolved.
Outdated
std::chrono::system_clock::now(),
Utility::TimeFacet::Year | Utility::TimeFacet::Month | Utility::TimeFacet::Day |
Utility::TimeFacet::Hour | Utility::TimeFacet::Minute | Utility::TimeFacet::Second);

std::optional<std::string> note;
if (context.Args.Contains(Execution::Args::Type::PinNote))
{
note = std::string{ context.Args.GetArg(Execution::Args::Type::PinNote) };
}

for (auto& pin : pinsToAddOrUpdate)
{
pin.SetDateAdded(dateAdded);
Comment thread
Trenly marked this conversation as resolved.
Outdated
pin.SetNote(note);
pinningData.AddOrUpdatePin(pin);
}

Expand Down Expand Up @@ -335,4 +349,103 @@ namespace AppInstaller::CLI::Workflow
context.Reporter.Info() << Resource::String::PinNoPinsExist << std::endl;
}
}

void ShowPinDetails(Execution::Context& context)
{
auto& pinningData = context.Get<Execution::Data::PinningData>();
auto allPins = pinningData.GetAllPins();

// Apply filtering based on provided arguments
bool hasId = context.Args.Contains(Execution::Args::Type::Id);
bool hasName = context.Args.Contains(Execution::Args::Type::Name);
bool hasQuery = context.Args.Contains(Execution::Args::Type::Query);
bool exactMatch = context.Args.Contains(Execution::Args::Type::Exact);

Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

winget pin show currently allows running with no query/id/name arguments, in which case it will match every pin and dump verbose details for all of them. This seems inconsistent with the intent of a "show" command (and the comment above suggests at least one filter is expected). Consider enforcing that at least one of --id/--name/--query is provided and terminating with an appropriate error message when none are specified.

Suggested change
if (!hasId && !hasName && !hasQuery)
{
context.Reporter.Error() << "The 'pin show' command requires at least one of --id, --name, or --query." << std::endl;
AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INVALID_CL_ARGUMENTS);
}

Copilot uses AI. Check for mistakes.
std::vector<Pinning::Pin> matchingPins;
for (const auto& pin : allPins)
{
const auto& packageId = pin.GetKey().PackageId;

if (hasId)
{
std::string_view idArg = context.Args.GetArg(Execution::Args::Type::Id);
bool match = exactMatch
? Utility::CaseInsensitiveEquals(packageId, idArg)
: Utility::CaseInsensitiveContainsSubstring(packageId, idArg);
if (!match)
{
continue;
}
}
else if (hasName || hasQuery)
{
// Without an open source, we can only match against PackageId
std::string_view queryArg = hasName
? context.Args.GetArg(Execution::Args::Type::Name)
: context.Args.GetArg(Execution::Args::Type::Query);
bool match = exactMatch
? Utility::CaseInsensitiveEquals(packageId, queryArg)
: Utility::CaseInsensitiveContainsSubstring(packageId, queryArg);
if (!match)
{
continue;
}
}

matchingPins.push_back(pin);
}

if (matchingPins.empty())
{
context.Reporter.Info() << Resource::String::PinShowNoMatchFound << std::endl;
AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PIN_DOES_NOT_EXIST);
}

auto info = context.Reporter.Info();
bool firstPin = true;
for (const auto& pin : matchingPins)
{
if (!firstPin)
{
info << std::endl;
}
firstPin = false;

const auto& pinKey = pin.GetKey();

// ID
ShowSingleLineField(info, Resource::String::PinShowLabelId, Utility::LocIndView{ pinKey.PackageId });

// Source
if (!pinKey.SourceId.empty() && !pinKey.IsForInstalled())
{
ShowSingleLineField(info, Resource::String::PinShowLabelSource, Utility::LocIndView{ pinKey.SourceId });
}

// Type
std::string pinTypeStr{ ToString(pin.GetType()) };
ShowSingleLineField(info, Resource::String::PinShowLabelType, Utility::LocIndView{ pinTypeStr });

// Version (gated version string; empty for pinning/blocking pins)
std::string gatedVersionStr = pin.GetGatedVersion().ToString();
if (!gatedVersionStr.empty())
{
ShowSingleLineField(info, Resource::String::PinShowLabelVersion, Utility::LocIndView{ gatedVersionStr });
}

// Date Added
const auto& dateAdded = pin.GetDateAdded();
if (!dateAdded.empty())
{
ShowSingleLineField(info, Resource::String::PinShowLabelDateAdded, Utility::LocIndView{ dateAdded });
}

// Note (only shown if present)
const auto& note = pin.GetNote();
if (note.has_value() && !note->empty())
{
ShowSingleLineField(info, Resource::String::PinShowLabelNote, Utility::LocIndView{ *note });
}
}
}
}
8 changes: 8 additions & 0 deletions src/AppInstallerCLICore/Workflows/PinFlow.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ namespace AppInstaller::CLI::Workflow
// Outputs: None
void ReportPins(Execution::Context& context);

// Shows details for a single matching pin (for `winget pin show`).
// Filters the pinning index by query/name/id/exact args and outputs
// detailed field-by-field info (Name, ID, Version, Source, Type, Date Added, Note).
// Required Args: None (at least one of --query/--name/--id expected)
// Inputs: PinningIndex
// Outputs: None
void ShowPinDetails(Execution::Context& context);

// Resets all the existing pins.
// Required Args: None
// Inputs: None
Expand Down
2 changes: 2 additions & 0 deletions src/AppInstallerCLIPackage/Package.appxmanifest
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@
</com:Class>
<com:Class Id="29B19238-81AD-4A8E-A2FC-ADF17C38CAEB" DisplayName="EditPackageCatalogOptions Server">
</com:Class>
<com:Class Id="B3A61CCB-A3D0-497D-B300-A904904EEA56" DisplayName="PinPackageOptions Server">
</com:Class>
</com:ExeServer>
</com:ComServer>
</com:Extension>
Expand Down
46 changes: 46 additions & 0 deletions src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw
Original file line number Diff line number Diff line change
Expand Up @@ -1648,6 +1648,10 @@ Please specify one of them using the --source option to proceed.</value>
<data name="PinInstalledArgumentDescription" xml:space="preserve">
<value>Pin a specific installed version</value>
</data>
<data name="PinNoteArgumentDescription" xml:space="preserve">
<value>Optional note to store with the pin</value>
<comment>Description for the --note argument used with `winget pin add`</comment>
</data>
<data name="PinInstalledSource" xml:space="preserve">
<value>Installed</value>
<comment>Value used in a table to indicate that a package comes from the list of packages installed in the machine</comment>
Expand Down Expand Up @@ -1913,6 +1917,48 @@ Please specify one of them using the --source option to proceed.</value>
<value>Pinned version</value>
<comment>Table header for the version to which a package is pinned; meaning it should not update from that version.</comment>
</data>
<data name="PinDateAdded" xml:space="preserve">
<value>Date added</value>
<comment>Label shown in the pin show output for when the pin was added or last updated.</comment>
Comment thread
Trenly marked this conversation as resolved.
Outdated
</data>
<data name="PinNote" xml:space="preserve">
<value>Note</value>
<comment>Label shown in the pin show output for the user-provided note stored with the pin.</comment>
</data>
<data name="PinShowCommandShortDescription" xml:space="preserve">
<value>Show details about a pin</value>
Comment thread
Trenly marked this conversation as resolved.
</data>
<data name="PinShowCommandLongDescription" xml:space="preserve">
<value>Show detailed information about a specific pin, including the package ID, version, type, date added, and any note stored with the pin.</value>
Comment thread
Trenly marked this conversation as resolved.
Outdated
</data>
<data name="PinShowLabelDateAdded" xml:space="preserve">
<value>Date added:</value>
Comment thread
Trenly marked this conversation as resolved.
<comment>Label shown in the `winget pin show` output for when the pin was added or last updated.</comment>
</data>
<data name="PinShowLabelId" xml:space="preserve">
<value>Id:</value>
<comment>Label shown in the `winget pin show` output for the package identifier.</comment>
</data>
<data name="PinShowLabelNote" xml:space="preserve">
<value>Note:</value>
<comment>Label shown in the `winget pin show` output for the user-provided note stored with the pin.</comment>
</data>
<data name="PinShowLabelSource" xml:space="preserve">
<value>Source:</value>
<comment>Label shown in the `winget pin show` output for the package source.</comment>
</data>
<data name="PinShowLabelType" xml:space="preserve">
<value>Pin type:</value>
<comment>Label shown in the `winget pin show` output for the type of pin (e.g., Pinning, Gating, Blocking).</comment>
</data>
<data name="PinShowLabelVersion" xml:space="preserve">
<value>Pinned version:</value>
<comment>Label shown in the `winget pin show` output for the version string of a gating pin.</comment>
</data>
<data name="PinShowNoMatchFound" xml:space="preserve">
<value>No pin found matching the specified criteria.</value>
<comment>Shown when `winget pin show` finds no matching pin.</comment>
</data>
<data name="ConfigurationDescriptionWasTruncated" xml:space="preserve">
<value>&lt;See the log file for additional details&gt;</value>
<comment>The brackets are intended to make the value stand out from other text which it will follow. Any locale appropriate mechanism that achieves this is acceptable.</comment>
Expand Down
Loading