Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 20 additions & 1 deletion .agents/skills/seek-file-search/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@ Use `seek` for filesystem path search.
`seek report`
- Search from a specific root:
`seek report --root /path/to/root`
- Emit absolute paths instead of paths relative to `--root`:
`seek report --root /path/to/root --absolute`
- Regex path search:
`seek '.*\\.cs$' --regex`
- Match all eligible files:
`seek '' --files`
- Only files:
`seek report --files`
- Only directories:
Expand All @@ -28,6 +32,10 @@ Use `seek` for filesystem path search.
`seek cache --plain`
- NUL-delimited output for safe pipelines:
`seek cache --null | xargs -0 rm -rf`
- Preview matching deletes:
`seek delete cache --directories`
- Apply matching deletes:
`seek delete cache --directories --apply`

## Workflow

Expand All @@ -36,6 +44,7 @@ Use `seek` for filesystem path search.
- Content search: use `rg`.

2. Pick the smallest useful option set.
- Root-relative output is the default. Add `--absolute` when the consumer needs standalone paths.
- File-only matches: `--files`
- Directory-only matches: `--directories`
- Regex patterns: `--regex`
Expand All @@ -47,10 +56,18 @@ Use `seek` for filesystem path search.
- Plain line-delimited output: `--plain`
- Safe machine output for filenames with spaces/newlines: `--null` and a NUL-aware consumer such as `xargs -0`

4. For destructive work, preview before apply.
- Preview with `seek delete ...`
- Delete only with `seek delete ... --apply`

## Guardrails

- `seek` searches full paths, not file contents.
- `seek --null` is for machine consumption. Pair it with `xargs -0` or another NUL-aware reader.
- Search output is relative to `--root` by default.
- `seek --absolute` emits absolute paths.
- `seek --null` is for machine consumption. It emits plain absolute paths terminated by `\0`. Pair it with `xargs -0` or another NUL-aware reader.
- `seek ''` matches all eligible entries under the selected root.
- `seek delete` previews absolute candidate paths and only deletes when `--apply` is present.
- Do not pipe `seek --null` directly into commands like `rm`; `rm` expects argv, not stdin records.
- For destructive operations, prefer inspecting results first, or use safe batching:
`seek cache --directories --null | xargs -0 -n 100 rm -rf`
Expand All @@ -65,3 +82,5 @@ Use `seek` for filesystem path search.
`seek '\\.log$' --regex --files`
- `ls -R | grep report`
`seek report`
- `find . -type d -name '*cache*' -print0 | xargs -0 rm -rf`
`seek delete cache --directories --apply`
42 changes: 38 additions & 4 deletions .github/workflows/publish-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ concurrency:
env:
CLI_PROJECT: src/Seek.Cli/Seek.Cli.csproj
NUGET_SOURCE: https://api.nuget.org/v3/index.json
WINGET_INSTALLERS_REGEX: '^seek-win-(x64|arm64)\.exe$'

jobs:
metadata:
Expand Down Expand Up @@ -120,10 +121,6 @@ jobs:
fail-fast: false
matrix:
include:
- os: windows-latest
rid: win-x64
- os: windows-latest
rid: win-arm64
- os: ubuntu-latest
rid: linux-x64
- os: ubuntu-24.04-arm
Expand Down Expand Up @@ -323,6 +320,12 @@ jobs:
run: |
Compress-Archive -Path "publish\Seek.exe" -DestinationPath "seek-${{ matrix.rid }}.zip"

- name: Stage raw Windows binary
if: runner.os == 'Windows' && (matrix.rid == 'win-x64' || matrix.rid == 'win-arm64')
shell: pwsh
run: |
Copy-Item "publish\Seek.exe" "seek-${{ matrix.rid }}.exe"

- name: Generate artifact attestation
uses: actions/attest-build-provenance@v3
with:
Expand All @@ -342,6 +345,14 @@ jobs:
path: seek-${{ matrix.rid }}.zip
if-no-files-found: error

- name: Upload raw Windows binary artifact
if: runner.os == 'Windows' && (matrix.rid == 'win-x64' || matrix.rid == 'win-arm64')
uses: actions/upload-artifact@v6
with:
name: seek-${{ matrix.rid }}.exe
path: seek-${{ matrix.rid }}.exe
if-no-files-found: error

- name: Upload Sigstore bundle artifact
uses: actions/upload-artifact@v6
with:
Expand Down Expand Up @@ -416,3 +427,26 @@ jobs:
for file in release-artifacts/*; do
gh release upload "${{ needs.metadata.outputs.version }}" "$file" --clobber
done

winget-release:
needs:
- metadata
- github-release
# winget-releaser updates an existing winget package entry.
# Bootstrap the first Seek submission manually, then enable this job by
# configuring WINGET_PACKAGE_IDENTIFIER and WINGET_TOKEN.
if: ${{ vars.WINGET_PACKAGE_IDENTIFIER != '' }}
runs-on: ubuntu-latest
env:
WINGET_TOKEN: ${{ secrets.WINGET_TOKEN }}

steps:
- name: Publish GitHub release to WinGet
if: ${{ env.WINGET_TOKEN != '' }}
uses: vedantmgoyal9/winget-releaser@v2
Comment on lines +444 to +446

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Fail the WinGet job when the PAT secret is missing

If WINGET_PACKAGE_IDENTIFIER is configured but WINGET_TOKEN is absent (for example after a secret rotation or in another release environment), this step is simply skipped and the job still finishes green because it has no other work. That leaves the GitHub release published but WinGet stale with no signal to the releaser that the install channel was not updated.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Initial winget release needs to be manual. As such the WINGET_PACKAGE_IDENTIFIER was removed on purpose to skip this part.
After the manual PR to winget packages was made, the variable was added to the repo to be used in the future.

with:
identifier: ${{ vars.WINGET_PACKAGE_IDENTIFIER }}
installers-regex: ${{ env.WINGET_INSTALLERS_REGEX }}
release-tag: ${{ needs.metadata.outputs.version }}
fork-user: ${{ vars.WINGET_FORK_USER }}
token: ${{ env.WINGET_TOKEN }}
14 changes: 10 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@
## Task behavior

- If a requested change needs unknown external source material, ask for it.
- If public command behavior, install flow, or package semantics change, ask whether to update `README.md` and `CHANGELOG.md`.
- If public command behavior, install flow, or package semantics change, ask whether to update `README.md`, `CHANGELOG.md`, and `.agents/skills/seek-file-search`.
- When discussing external dependencies, verify behavior against the exact version in use.
- When preparing a release or changing release-relevant behavior, keep `CHANGELOG.md` and `src/Seek.Cli/Seek.Cli.csproj` `<PackageReleaseNotes>` in sync.
- When preparing a release or changing release-relevant behavior, keep `CHANGELOG.md`, `src/Seek.Cli/Seek.Cli.csproj` `<PackageReleaseNotes>`, and `.agents/skills/seek-file-search` in sync when the shipped skill documents affected behavior.
- Write `CHANGELOG.md` in a human-readable release format aimed at GitHub release readers, not as a raw internal change inventory.
- `CHANGELOG.md` should only include public-facing changes. Do not include internal repo, process, test, or maintenance-only changes unless they materially affect users.
- Keep `<PackageReleaseNotes>` simpler and shorter than `CHANGELOG.md`, focused on NuGet users and package-facing changes.
- If one of `CHANGELOG.md` or `<PackageReleaseNotes>` changes for a release, review the other in the same pass so they do not drift.
- If one of `CHANGELOG.md`, `<PackageReleaseNotes>`, or `.agents/skills/seek-file-search` changes for a release or CLI behavior update, review the others in the same pass so they do not drift.

## Current architecture

Expand All @@ -34,12 +35,14 @@

- The CLI exposes a default search command wired in `Program.cs` via `app.Add("", Commands.SearchAsync)`.
- The CLI also exposes a named destructive subcommand wired via `app.Add("delete", Commands.DeleteAsync)`.
- The CLI also exposes a named utility subcommand wired via `app.Add("check-for-updates", Commands.CheckForUpdatesAsync)`.
- The default command positional argument is the search query.
- Current arguments and defaults from `Commands.SearchAsync`:
- `query`: required positional argument.
- `regex`: defaults to `false`.
- `caseSensitive`: defaults to `false`.
- `plain`: defaults to `false`.
- `absolute`: defaults to `false`.
- `null`: defaults to `false`.
- `hidden`: defaults to `false`.
- `system`: defaults to `false`.
Expand All @@ -66,11 +69,14 @@
- `-d` => `--directories`
- `-c` => `--highlight-color`
- If `plain` is `true`, the CLI writes the full path directly and does not emit PrettyConsole color/escape sequences for match sections.
- If `null` is `true`, the CLI emits plain NUL-terminated paths and bypasses highlight rendering.
- If `absolute` is `false`, search output is relative to `root`.
- If `null` is `true`, the CLI emits plain absolute NUL-terminated paths and bypasses highlight rendering.
- If both `files` and `directories` are `false`, both result kinds are emitted. If both are `true`, the current behavior is also to emit both result kinds.
- An empty search query matches all eligible entries under `root`.
- `seek delete` previews candidates by default, prints absolute candidate paths, and only deletes when `apply` is `true`.
- `seek delete` collapses descendants under matched directories before preview or apply.
- `seek delete` deletes sequentially, prints one `SUCCESS` or `FAIL` line per candidate in apply mode, and returns exit code `1` if any deletion fails.
- `seek check-for-updates` compares `ConsoleApp.Version` against the latest published NuGet package version and prints either update instructions or an up-to-date message.
- Worker count is not user-configurable today. `FileSystemSearch` computes it as `Math.Max(1, Environment.ProcessorCount - 1)`.
- Results are rendered to standard output through `PrettyConsole` when highlighting is enabled.
- Global exception handling preserves validation and argument parse failures, prints cancellations as a non-error, and prints unexpected exception messages in red while setting exit code `1`.
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 1.1.1

- Updated the shipped `seek-file-search` agent skill to reflect the 1.1.0 CLI behavior, including root-relative output by default, `--absolute`, `--null`, empty-query match-all searches, and `seek delete`.

## 1.1.0

- Search results are relative to `--root` by default. Use `--absolute` for standalone paths or `--null` for NUL-terminated absolute paths in scripts.
Expand Down
2 changes: 1 addition & 1 deletion src/Seek.Cli/Program.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using ConsoleAppFramework;

using Seek.Cli;
ConsoleApp.Version = "1.1.0";
ConsoleApp.Version = "1.1.1";

var app = ConsoleApp.Create();
app.UseFilter<GlobalExceptionHandler>();
Expand Down
11 changes: 5 additions & 6 deletions src/Seek.Cli/Seek.Cli.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
<GarbageCollectionAdaptationMode>0</GarbageCollectionAdaptationMode>
<InvariantGlobalization>true</InvariantGlobalization>
<StackTraceSupport>false</StackTraceSupport>
<Version>1.1.0</Version>
<Version>1.1.1</Version>
<StripSymbols>true</StripSymbols>
<RepositoryUrl>https://github.com/dusrdev/Seek</RepositoryUrl>
<RepositoryType>git</RepositoryType>
Expand All @@ -39,17 +39,16 @@
<PackageId>Seek</PackageId>
<PackAsTool>true</PackAsTool>
<PackageOutputPath>../.././nupkg</PackageOutputPath>
<!-- Keep Windows on the framework-dependent tool package so dotnet tool install
continues to expose an executable shim instead of a .cmd wrapper. -->
<ToolPackageRuntimeIdentifiers>
win-x64;win-arm64;linux-x64;linux-arm64;osx-x64;osx-arm64;any
linux-x64;linux-arm64;osx-x64;osx-arm64;any
</ToolPackageRuntimeIdentifiers>
</PropertyGroup>

<PropertyGroup>
<PackageReleaseNotes>
- Root-relative search output by default, with --absolute and --null for path-safe scripting.
- Added seek delete with preview-first behavior and --apply for actual deletion.
- Empty-query searches now match all eligible entries instead of hanging.
- Native AOT NuGet tool packages for common runtimes, plus a Seek.any fallback package.
- No CLI or package behavior changes in this patch release.
</PackageReleaseNotes>
</PropertyGroup>

Expand Down
93 changes: 93 additions & 0 deletions tests/Seek.Cli.Tests/ToolPackagingTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using System.Xml.Linq;

namespace Seek.Cli.Tests;

public sealed partial class ToolPackagingTests {
[Test]
public async Task CliProject_ToolPackageRuntimeIdentifiers_ExcludeWindowsRidPackages(CancellationToken cancellationToken) {
var projectContents = await File.ReadAllTextAsync(
Path.Combine(FindRepositoryRoot(), "src", "Seek.Cli", "Seek.Cli.csproj"),
cancellationToken);

var runtimeIdentifiers = ParseToolPackageRuntimeIdentifiers(projectContents);

await Assert.That(runtimeIdentifiers).Contains("linux-x64");
await Assert.That(runtimeIdentifiers).Contains("linux-arm64");
await Assert.That(runtimeIdentifiers).Contains("osx-x64");
await Assert.That(runtimeIdentifiers).Contains("osx-arm64");
await Assert.That(runtimeIdentifiers).Contains("any");
await Assert.That(runtimeIdentifiers).DoesNotContain("win-x64");
await Assert.That(runtimeIdentifiers).DoesNotContain("win-arm64");
}

[Test]
public async Task PublishReleaseWorkflow_NugetRidMatrix_SkipsWindowsRidPackagesButKeepsWindowsBinaryReleases(CancellationToken cancellationToken) {
var workflowContents = await File.ReadAllTextAsync(
Path.Combine(FindRepositoryRoot(), ".github", "workflows", "publish-release.yml"),
cancellationToken);

var nugetRidSection = ExtractSection(workflowContents, "nuget-rid", "nuget-publish");
var binariesSection = ExtractSection(workflowContents, "binaries", string.Empty);

await Assert.That(nugetRidSection).Contains("linux-x64");
await Assert.That(nugetRidSection).Contains("linux-arm64");
await Assert.That(nugetRidSection).Contains("osx-x64");
await Assert.That(nugetRidSection).Contains("osx-arm64");
await Assert.That(nugetRidSection).DoesNotContain("win-x64");
await Assert.That(nugetRidSection).DoesNotContain("win-arm64");

await Assert.That(binariesSection).Contains("win-x86");
await Assert.That(binariesSection).Contains("win-x64");
await Assert.That(binariesSection).Contains("win-arm64");
}

private static string FindRepositoryRoot() {
for (var current = new DirectoryInfo(AppContext.BaseDirectory); current is not null; current = current.Parent) {
var projectPath = Path.Combine(current.FullName, "src", "Seek.Cli", "Seek.Cli.csproj");
var workflowPath = Path.Combine(current.FullName, ".github", "workflows", "publish-release.yml");
if (File.Exists(projectPath) && File.Exists(workflowPath)) {
return current.FullName;
}
}

throw new DirectoryNotFoundException("Could not locate the repository root from the test output directory.");
}

private static string[] ParseToolPackageRuntimeIdentifiers(string projectContents) {
var project = XDocument.Parse(projectContents);
var runtimeIdentifiers = project.Root?
.Elements()
.Where(element => element.Name.LocalName == "PropertyGroup")
.Elements()
.FirstOrDefault(element => element.Name.LocalName == "ToolPackageRuntimeIdentifiers")
?.Value;

if (string.IsNullOrWhiteSpace(runtimeIdentifiers)) {
throw new InvalidOperationException("Could not find ToolPackageRuntimeIdentifiers in Seek.Cli.csproj.");
}

return runtimeIdentifiers
.Split(';', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
}

private static string ExtractSection(string contents, string sectionName, string nextSectionName) {
var sectionHeader = $"{sectionName}:";
var startIndex = contents.IndexOf(sectionHeader, StringComparison.Ordinal);
if (startIndex < 0) {
throw new InvalidOperationException($"Could not find the {sectionName} section in publish-release.yml.");
}

var sectionStart = startIndex + sectionHeader.Length;
if (nextSectionName.Length == 0) {
return contents[sectionStart..];
}

var nextSectionHeader = $"{nextSectionName}:";
var endIndex = contents.IndexOf(nextSectionHeader, sectionStart, StringComparison.Ordinal);
if (endIndex < 0) {
throw new InvalidOperationException($"Could not find the {nextSectionName} section in publish-release.yml.");
}

return contents[sectionStart..endIndex];
}
}