Skip to content

Commit 4e09683

Browse files
JohnCampionJrclaude
andcommitted
Add shell-init command emitting a changeset-net shell function
net-changesets and the Node @changesets CLI share the `changeset` command name, and a .NET tool can expose only one command (the SDK rejects a multi-command tool package), so there is no packaged way to add a second name. `changeset shell-init <shell>` prints a shell function (zsh/bash/pwsh) that points `changeset-net` at the net-changesets binary directly, so it always runs net-changesets regardless of PATH order. - New ShellInitCommand + registration; raw stdout so `eval "$(...)"` works. - E2E tests, changeset, and a README "Using alongside @changesets" section. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 713e05c commit 4e09683

6 files changed

Lines changed: 177 additions & 0 deletions

File tree

.changeset/shell-init.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"SolarWinds.Changesets": Minor
3+
---
4+
5+
Add a `shell-init` command that prints a shell snippet defining a `changeset-net` function (for zsh, bash, and pwsh). net-changesets and the Node `@changesets` CLI share the `changeset` command name, and a .NET tool can expose only one command, so there is no packaged way to add a second name. `changeset-net` points at the net-changesets binary directly, so it always runs net-changesets even when `changeset` resolves to the Node tool on your PATH. Enable it with `eval "$(changeset shell-init zsh)"` (or the pwsh equivalent).

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,23 @@ intentional gaps:
4444
`changelog-github` (PR links + commit links + author thanks) generators are not implemented. They may be
4545
added in a future update if needed or requested.
4646

47+
## Using alongside @changesets (Node)
48+
49+
net-changesets installs the `changeset` command — the same name as the Node `@changesets` CLI. When both are
50+
installed globally, whichever comes first on your `PATH` wins, and a .NET tool can expose only one command, so
51+
there is no built-in second name. To always reach net-changesets regardless of `PATH`, add its `changeset-net`
52+
shell function:
53+
54+
```bash
55+
# zsh (~/.zshrc) — bash and pwsh are also supported
56+
eval "$(changeset shell-init zsh)"
57+
```
58+
59+
`changeset shell-init <shell>` prints the snippet; run `changeset shell-init` for setup instructions. In a
60+
polyglot repo the smoothest setup is to let net-changesets own `changeset` (it leaves Node changesets untouched
61+
and guides you to the Node tool when there are no .NET projects), using `changeset-net` only when the Node tool
62+
owns `changeset`.
63+
4764
## Documentation
4865

4966
- [Implementation Details of net-changesets commands](https://github.com/solarwinds/net-changesets/blob/main/docs/commands-implementation-details.md)
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
using Spectre.Console.Cli;
2+
3+
namespace SolarWinds.Changesets.Commands.ShellInit;
4+
5+
/// <summary>
6+
/// Prints a shell snippet that defines a <c>changeset-net</c> command pointing at this net-changesets binary.
7+
/// Because net-changesets and the Node <c>@changesets</c> CLI share the command name <c>changeset</c> (and a
8+
/// .NET tool may expose only one command), there is no packaged way to add a second name. A shell function is
9+
/// the workaround: <c>changeset-net</c> always runs net-changesets, even when <c>changeset</c> resolves to the
10+
/// Node tool on the user's PATH. Use it via <c>eval "$(changeset shell-init zsh)"</c> (or the pwsh form).
11+
/// </summary>
12+
internal sealed class ShellInitCommand : Command<ShellInitCommandSettings>
13+
{
14+
private static readonly string[] s_shells = ["zsh", "bash", "pwsh"];
15+
16+
public static string Name { get; } = "shell-init";
17+
18+
public static string Description { get; } =
19+
"Print a shell snippet that defines a 'changeset-net' command for invoking net-changesets directly.";
20+
21+
protected override int Execute(CommandContext context, ShellInitCommandSettings settings, CancellationToken cancellationToken)
22+
{
23+
// The path to this binary, so the emitted command always reaches net-changesets regardless of PATH
24+
// order. For an installed global tool this is the tool shim (for example ~/.dotnet/tools/changeset).
25+
string net = Environment.ProcessPath ?? "changeset";
26+
27+
// Raw stdout throughout (Spectre1000 suppressed deliberately): the snippet is meant to be captured by
28+
// `eval "$(...)"`, so it must not be wrapped to terminal width or marked up, which AnsiConsole would do.
29+
#pragma warning disable Spectre1000 // Use AnsiConsole instead of System.Console
30+
// No shell → show how to enable it (this is what users run first).
31+
if (string.IsNullOrWhiteSpace(settings.Shell))
32+
{
33+
Console.WriteLine(Instructions(net));
34+
return 0;
35+
}
36+
37+
if (!s_shells.Contains(settings.Shell, StringComparer.OrdinalIgnoreCase))
38+
{
39+
Console.Error.WriteLine($"Unknown shell '{settings.Shell}'. Supported: {string.Join(", ", s_shells)}.");
40+
return 1;
41+
}
42+
43+
string snippet = string.Equals(settings.Shell, "pwsh", StringComparison.OrdinalIgnoreCase)
44+
? PwshSnippet(net)
45+
: string.Equals(settings.Shell, "bash", StringComparison.OrdinalIgnoreCase)
46+
? PosixSnippet(net, "~/.bashrc", "bash")
47+
: PosixSnippet(net, "~/.zshrc", "zsh");
48+
49+
Console.WriteLine(snippet);
50+
#pragma warning restore Spectre1000
51+
return 0;
52+
}
53+
54+
private static string Instructions(string net) =>
55+
$"""
56+
Define a `changeset-net` command that always runs net-changesets, even when the shared `changeset`
57+
command resolves to the Node @changesets CLI on your PATH.
58+
59+
Add the line for your shell to its startup file, then restart the shell:
60+
61+
zsh ~/.zshrc eval "$("{net}" shell-init zsh)"
62+
bash ~/.bashrc eval "$("{net}" shell-init bash)"
63+
pwsh $PROFILE Invoke-Expression (& "{net}" shell-init pwsh | Out-String)
64+
65+
Run `changeset shell-init <shell>` to print the snippet itself.
66+
""";
67+
68+
private static string PosixSnippet(string net, string rcFile, string shell) =>
69+
$$"""
70+
# net-changesets shell integration. Add this ONE line to {{rcFile}} (don't paste this script):
71+
# eval "$("{{net}}" shell-init {{shell}})"
72+
# It defines `changeset-net`, which always runs net-changesets even when the shared `changeset`
73+
# command resolves to the Node @changesets CLI on your PATH.
74+
changeset-net() {
75+
command "{{net}}" "$@"
76+
}
77+
""";
78+
79+
private static string PwshSnippet(string net) =>
80+
$$"""
81+
# net-changesets shell integration. Add this line to $PROFILE (don't paste this script):
82+
# Invoke-Expression (& "{{net}}" shell-init pwsh | Out-String)
83+
# It defines `changeset-net`, which always runs net-changesets even when the shared `changeset`
84+
# command resolves to the Node @changesets CLI on your PATH.
85+
function changeset-net {
86+
& "{{net}}" @args
87+
}
88+
""";
89+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using System.ComponentModel;
2+
using Spectre.Console.Cli;
3+
4+
namespace SolarWinds.Changesets.Commands.ShellInit;
5+
6+
internal sealed class ShellInitCommandSettings : CommandSettings
7+
{
8+
[CommandArgument(0, "[shell]")]
9+
[Description("The shell to emit the snippet for: 'zsh', 'bash', or 'pwsh'. Omit to print setup instructions.")]
10+
public string? Shell { get; init; }
11+
}

src/SolarWinds.Changesets/Program.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using SolarWinds.Changesets.Commands.Pre;
66
using SolarWinds.Changesets.Commands.Publish;
77
using SolarWinds.Changesets.Commands.Publish.Services;
8+
using SolarWinds.Changesets.Commands.ShellInit;
89
using SolarWinds.Changesets.Commands.Status;
910
using SolarWinds.Changesets.Commands.Tag;
1011
using SolarWinds.Changesets.Commands.Ui;
@@ -113,6 +114,12 @@
113114
.WithDescription(InfoChangesetCommand.Description)
114115
.WithExample("info")
115116
;
117+
118+
config
119+
.AddCommand<ShellInitCommand>(ShellInitCommand.Name)
120+
.WithDescription(ShellInitCommand.Description)
121+
.WithExample("shell-init", "zsh")
122+
;
116123
});
117124

118125
return app.Run(args);
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using AwesomeAssertions;
2+
3+
namespace SolarWinds.Changesets.Tests.E2E;
4+
5+
/// <summary>
6+
/// Covers the <c>shell-init</c> command, which emits the <c>changeset-net</c> shell function used to invoke
7+
/// net-changesets directly when the shared <c>changeset</c> command resolves to the Node tool on PATH.
8+
/// </summary>
9+
[TestFixture]
10+
[Category("E2E")]
11+
internal sealed class ShellInitTests
12+
{
13+
[Test]
14+
public void ShellInit_Zsh_EmitsTheChangesetNetFunction()
15+
{
16+
CliResult result = ChangesetCli.Run(Environment.CurrentDirectory, "shell-init", "zsh");
17+
18+
result.ExitCode.Should().Be(0, result.Output);
19+
result.Output.Should().Contain("changeset-net()").And.Contain("command ").And.Contain("\"$@\"");
20+
}
21+
22+
[Test]
23+
public void ShellInit_Pwsh_EmitsAFunction()
24+
{
25+
CliResult result = ChangesetCli.Run(Environment.CurrentDirectory, "shell-init", "pwsh");
26+
27+
result.ExitCode.Should().Be(0, result.Output);
28+
result.Output.Should().Contain("function changeset-net").And.Contain("@args");
29+
}
30+
31+
[Test]
32+
public void ShellInit_NoShell_PrintsInstructions()
33+
{
34+
CliResult result = ChangesetCli.Run(Environment.CurrentDirectory, "shell-init");
35+
36+
result.ExitCode.Should().Be(0, result.Output);
37+
result.Output.Should().Contain("changeset-net").And.Contain("startup file");
38+
}
39+
40+
[Test]
41+
public void ShellInit_UnknownShell_Fails()
42+
{
43+
CliResult result = ChangesetCli.Run(Environment.CurrentDirectory, "shell-init", "fish");
44+
45+
result.ExitCode.Should().NotBe(0);
46+
result.Output.Should().Contain("Unknown shell");
47+
}
48+
}

0 commit comments

Comments
 (0)