mirror of
https://github.com/bitwarden/self-host.git
synced 2026-06-27 22:05:45 +00:00
Add auto complete for ZSH and Bash
This commit is contained in:
@@ -14,8 +14,7 @@ public static class ApplyCommand
|
||||
|
||||
var manifest = new Option<string>("--manifest", "-m")
|
||||
{ Description = "Path to the YAML manifest.", DefaultValueFactory = _ => "bitwarden.yaml" };
|
||||
var deployment = new Option<string?>("--deployment", "-d")
|
||||
{ Description = "standard | lite (defaults to the manifest)." };
|
||||
var deployment = Cli.DeploymentOption("standard | lite (defaults to the manifest).");
|
||||
var root = new Option<string>("--root")
|
||||
{ Description = "Data directory (bwdata).", DefaultValueFactory = _ => "./bwdata" };
|
||||
|
||||
@@ -38,10 +37,9 @@ public static class ApplyCommand
|
||||
var rootDir = parseResult.GetValue(root)!;
|
||||
var ctx = new InstallContext { Root = rootDir, Manifest = loaded };
|
||||
|
||||
// Re-render config/env from the manifest WITHOUT regenerating secrets (generation reuses
|
||||
// on-disk secrets), then recreate the stack so the new env takes effect. UpAsync force-
|
||||
// recreates every container, so the whole stack restarts briefly; data survives via the
|
||||
// named volume. Selective (only-changed) recreate is a future refinement.
|
||||
// Re-render from the manifest (secrets are reused, not regenerated), then recreate the
|
||||
// stack to pick up the changes. UpAsync recreates every container, so the stack restarts
|
||||
// briefly; data survives via the named volume.
|
||||
await dep.GenerateAssetsAsync(ctx, ct);
|
||||
var topology = dep.BuildTopology(ctx);
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ public static class BackupCommand
|
||||
{
|
||||
var cmd = new Command("backup", "Back up a deployment (config, secrets, certs, attachments + a DB dump) to a .tar.gz.");
|
||||
|
||||
var deployment = new Option<string?>("--deployment", "-d") { Description = "standard | lite." };
|
||||
var deployment = Cli.DeploymentOption();
|
||||
var root = new Option<string>("--root")
|
||||
{ Description = "Data directory (bwdata).", DefaultValueFactory = _ => "./bwdata" };
|
||||
var output = new Option<string?>("--out", "-o")
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Bit.SelfHost.Deployments;
|
||||
using System.CommandLine;
|
||||
using Bit.SelfHost.Deployments;
|
||||
using Bit.SelfHost.Engine;
|
||||
using Spectre.Console;
|
||||
|
||||
@@ -7,6 +8,21 @@ namespace Bit.SelfHost.Commands;
|
||||
/// <summary>Shared helpers for the command layer.</summary>
|
||||
public static class Cli
|
||||
{
|
||||
/// <summary>Service names across both deployments, for `logs <service>` tab completion.</summary>
|
||||
public static readonly string[] ServiceNames =
|
||||
[
|
||||
"mssql", "web", "attachments", "api", "identity", "sso", "admin", "icons",
|
||||
"notifications", "events", "nginx", "key-connector", "scim",
|
||||
];
|
||||
|
||||
/// <summary>The shared `--deployment` option, with standard|lite validation + tab completion.</summary>
|
||||
public static Option<string?> DeploymentOption(string description = "standard | lite.")
|
||||
{
|
||||
var option = new Option<string?>("--deployment", "-d") { Description = description };
|
||||
option.AcceptOnlyFromAmong("standard", "lite");
|
||||
return option;
|
||||
}
|
||||
|
||||
/// <summary>The image tag of a running container (e.g. "2026.4.1"), or null if it isn't present.</summary>
|
||||
public static async Task<string?> ImageTagAsync(IContainerEngine engine, string container, CancellationToken ct)
|
||||
{
|
||||
|
||||
69
POC/bwsh/Commands/CompletionsCommand.cs
Normal file
69
POC/bwsh/Commands/CompletionsCommand.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using System.CommandLine;
|
||||
|
||||
namespace Bit.SelfHost.Commands;
|
||||
|
||||
public static class CompletionsCommand
|
||||
{
|
||||
public static Command Build()
|
||||
{
|
||||
var cmd = new Command("completions", "Print a shell completion script (zsh or bash).");
|
||||
|
||||
var shell = new Argument<string>("shell") { Description = "Shell to generate completions for (zsh, bash)." };
|
||||
shell.AcceptOnlyFromAmong("zsh", "bash");
|
||||
cmd.Arguments.Add(shell);
|
||||
|
||||
cmd.SetAction(parseResult =>
|
||||
{
|
||||
Console.WriteLine(parseResult.GetValue(shell) == "bash" ? Bash : Zsh);
|
||||
return 0;
|
||||
});
|
||||
|
||||
return cmd;
|
||||
}
|
||||
|
||||
// Both scripts delegate back to bwsh's own `[suggest]` directive for suggestions, so they stay
|
||||
// correct as commands change and need no dotnet-suggest global tool.
|
||||
private const string Zsh = """
|
||||
#compdef bwsh
|
||||
# zsh completion for bwsh.
|
||||
#
|
||||
# Load in the current shell:
|
||||
# source <(bwsh completions zsh)
|
||||
# Or install persistently:
|
||||
# bwsh completions zsh > "${fpath[1]}/_bwsh" # then restart zsh
|
||||
|
||||
_bwsh() {
|
||||
local -a items
|
||||
# bwsh answers completion requests itself via the [suggest] directive.
|
||||
items=( ${(f)"$(command bwsh "[suggest:${CURSOR}]" "$BUFFER" 2>/dev/null)"} )
|
||||
if (( ${#items} )); then
|
||||
compadd -- $items
|
||||
else
|
||||
_files # fall back to paths (e.g. --manifest, restore <archive>, --root)
|
||||
fi
|
||||
}
|
||||
|
||||
compdef _bwsh bwsh
|
||||
""";
|
||||
|
||||
private const string Bash = """
|
||||
# bash completion for bwsh.
|
||||
#
|
||||
# Load in the current shell:
|
||||
# source <(bwsh completions bash)
|
||||
# Or install persistently (with bash-completion installed):
|
||||
# bwsh completions bash > /usr/local/etc/bash_completion.d/bwsh
|
||||
|
||||
_bwsh() {
|
||||
local IFS=$'\n'
|
||||
local cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
# bwsh answers completion requests itself via the [suggest] directive.
|
||||
local items
|
||||
items="$(command bwsh "[suggest:${COMP_POINT}]" "${COMP_LINE}" 2>/dev/null)"
|
||||
COMPREPLY=( $(compgen -W "${items}" -- "${cur}") )
|
||||
}
|
||||
|
||||
# -o default falls back to filenames when there are no matches (e.g. --manifest, restore).
|
||||
complete -o default -F _bwsh bwsh
|
||||
""";
|
||||
}
|
||||
@@ -11,7 +11,7 @@ public static class ConfigCommand
|
||||
|
||||
var assignment = new Argument<string?>("assignment")
|
||||
{ Description = "key=value to set, or key to get. Omit with --show to list.", Arity = ArgumentArity.ZeroOrOne };
|
||||
var deployment = new Option<string?>("--deployment", "-d") { Description = "standard | lite." };
|
||||
var deployment = Cli.DeploymentOption();
|
||||
var root = new Option<string>("--root")
|
||||
{ Description = "Data directory (bwdata).", DefaultValueFactory = _ => "./bwdata" };
|
||||
var show = new Option<bool>("--show") { Description = "Show resolved config files." };
|
||||
@@ -32,9 +32,9 @@ public static class ConfigCommand
|
||||
{
|
||||
Console.WriteLine($"{kind} deployment config files (under {rootDir}):");
|
||||
Console.WriteLine(kind == DeploymentKind.Standard
|
||||
? " config.yml (structural — `rebuild` to apply)\n" +
|
||||
" env/global.override.env (SMTP/admin/globalSettings — `restart` to apply)"
|
||||
: " settings.env (all BW_*/globalSettings__* — `restart` to apply)");
|
||||
? " config.yml (structural; run `bwsh apply`)\n" +
|
||||
" env/global.override.env (SMTP / admin / globalSettings; run `bwsh apply`)"
|
||||
: " settings.env (all BW_* / globalSettings__*; run `bwsh apply`)");
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -60,8 +60,8 @@ public static class ConfigCommand
|
||||
{
|
||||
Cli.UpsertEnv(target, key, value);
|
||||
Console.WriteLine($"Set {key} in {binding.File}. Run `bwsh apply` to apply.");
|
||||
// The manifest is the source of truth: `apply` re-renders from it, so add durable
|
||||
// keys to the manifest's `config:` block (a bare `config set` is a one-off tweak).
|
||||
// apply re-renders from the manifest, so add durable keys to its `config:` block;
|
||||
// a bare `bwsh config key=value` is a one-off tweak.
|
||||
Console.WriteLine("Add it to your manifest's `config:` block to persist across applies.");
|
||||
}
|
||||
else
|
||||
|
||||
@@ -11,8 +11,7 @@ public static class InstallCommand
|
||||
{
|
||||
var cmd = new Command("install", "Install a Bitwarden self-host deployment.");
|
||||
|
||||
var deployment = new Option<string?>("--deployment", "-d")
|
||||
{ Description = "Deployment type: standard | lite. Defaults to the manifest, else standard." };
|
||||
var deployment = Cli.DeploymentOption("Deployment type: standard | lite. Defaults to the manifest, else standard.");
|
||||
var manifest = new Option<string?>("--manifest", "-m")
|
||||
{ Description = "Path to a YAML install manifest for an unattended install." };
|
||||
var root = new Option<string>("--root")
|
||||
|
||||
@@ -18,7 +18,8 @@ public static class LogsCommand
|
||||
Description = "Service name (e.g. identity). Omit for the whole container.",
|
||||
Arity = ArgumentArity.ZeroOrOne,
|
||||
};
|
||||
var deployment = new Option<string?>("--deployment", "-d") { Description = "standard | lite." };
|
||||
service.CompletionSources.Add(Cli.ServiceNames);
|
||||
var deployment = Cli.DeploymentOption();
|
||||
var root = new Option<string>("--root")
|
||||
{ Description = "Data directory (bwdata).", DefaultValueFactory = _ => "./bwdata" };
|
||||
var tail = new Option<int>("--tail")
|
||||
|
||||
@@ -12,7 +12,7 @@ public static class MigrateCommand
|
||||
{
|
||||
var cmd = new Command("migrate", "Adopt an existing bash/compose install under CLI management (non-destructive).");
|
||||
|
||||
var deployment = new Option<string?>("--deployment", "-d") { Description = "standard | lite." };
|
||||
var deployment = Cli.DeploymentOption();
|
||||
var root = new Option<string>("--root")
|
||||
{ Description = "Data directory (bwdata).", DefaultValueFactory = _ => "./bwdata" };
|
||||
var yes = new Option<bool>("--yes", "-y") { Description = "Skip confirmation." };
|
||||
|
||||
@@ -13,7 +13,7 @@ public static class RestoreCommand
|
||||
var cmd = new Command("restore", "Restore a deployment from a backup .tar.gz.");
|
||||
|
||||
var archive = new Argument<string>("archive") { Description = "Path to a backup .tar.gz." };
|
||||
var deployment = new Option<string?>("--deployment", "-d") { Description = "standard | lite." };
|
||||
var deployment = Cli.DeploymentOption();
|
||||
var root = new Option<string>("--root")
|
||||
{ Description = "Target data directory (bwdata).", DefaultValueFactory = _ => "./bwdata" };
|
||||
var yes = new Option<bool>("--yes", "-y") { Description = "Skip confirmation." };
|
||||
|
||||
@@ -11,7 +11,7 @@ public static class StatusCommand
|
||||
{
|
||||
var cmd = new Command("status", "Show the running state of a deployment's services.");
|
||||
|
||||
var deployment = new Option<string?>("--deployment", "-d") { Description = "standard | lite." };
|
||||
var deployment = Cli.DeploymentOption();
|
||||
var root = new Option<string>("--root")
|
||||
{ Description = "Data directory (bwdata).", DefaultValueFactory = _ => "./bwdata" };
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ public static class UninstallCommand
|
||||
{
|
||||
var cmd = new Command("uninstall", "Stop and remove the deployment's containers.");
|
||||
|
||||
var deployment = new Option<string?>("--deployment", "-d") { Description = "standard | lite." };
|
||||
var deployment = Cli.DeploymentOption();
|
||||
var root = new Option<string>("--root")
|
||||
{ Description = "Data directory (bwdata).", DefaultValueFactory = _ => "./bwdata" };
|
||||
var purge = new Option<bool>("--purge")
|
||||
|
||||
@@ -10,7 +10,7 @@ public static class UpdateCommand
|
||||
{
|
||||
var cmd = new Command("update", "Pull target versions and recreate changed containers.");
|
||||
|
||||
var deployment = new Option<string?>("--deployment", "-d") { Description = "standard | lite." };
|
||||
var deployment = Cli.DeploymentOption();
|
||||
var root = new Option<string>("--root")
|
||||
{ Description = "Data directory (bwdata).", DefaultValueFactory = _ => "./bwdata" };
|
||||
var rebuild = new Option<bool>("--rebuild")
|
||||
|
||||
@@ -12,6 +12,7 @@ root.Subcommands.Add(LogsCommand.Build());
|
||||
root.Subcommands.Add(MigrateCommand.Build());
|
||||
root.Subcommands.Add(BackupCommand.Build());
|
||||
root.Subcommands.Add(RestoreCommand.Build());
|
||||
root.Subcommands.Add(CompletionsCommand.Build());
|
||||
|
||||
// ProcessTerminationTimeout (default ~2s) makes System.CommandLine cancel the action's token on
|
||||
// Ctrl+C/SIGTERM; we catch the resulting cancellation below and exit cleanly.
|
||||
|
||||
@@ -75,6 +75,21 @@ dotnet run -- uninstall # stop and remove; --purge also deletes dat
|
||||
|
||||
Run `dotnet run -- <command> --help` for options on any command.
|
||||
|
||||
## Shell completion
|
||||
|
||||
`bwsh` prints a completion script (zsh or bash) that tab-completes commands, options, deployment
|
||||
kinds (`standard`/`lite`), and service names. Load it with the `bwsh` binary on your PATH:
|
||||
|
||||
```bash
|
||||
source <(bwsh completions zsh) # zsh, current shell
|
||||
bwsh completions zsh > "${fpath[1]}/_bwsh" # zsh, persistent (restart zsh)
|
||||
|
||||
source <(bwsh completions bash) # bash, current shell
|
||||
bwsh completions bash > /usr/local/etc/bash_completion.d/bwsh # bash, persistent
|
||||
```
|
||||
|
||||
It delegates back to `bwsh` for suggestions, so no `dotnet-suggest` tool is needed.
|
||||
|
||||
## Migrate an existing bash install
|
||||
|
||||
Adopt a stack that was installed with `bitwarden.sh` under CLI management — non-destructive,
|
||||
|
||||
@@ -12,9 +12,8 @@ public static class StandardAssetBuilder
|
||||
IReadOnlyDictionary<string, string>? Config = null);
|
||||
|
||||
/// <summary>
|
||||
/// Secrets read back from an existing bwdata/ so re-rendering (`apply`, or a re-run `install`)
|
||||
/// reuses them instead of minting new ones — rotating the DB/identity passwords would mismatch
|
||||
/// the already-initialized mssql volume and break the install.
|
||||
/// Secrets read back from an existing bwdata/ so re-rendering (apply, or a re-run install) reuses
|
||||
/// them. Rotating the DB or identity password would break the already-initialized mssql volume.
|
||||
/// </summary>
|
||||
private sealed record ExistingSecrets(
|
||||
string? IdentityCertPassword, string? DbPassword,
|
||||
@@ -34,7 +33,7 @@ public static class StandardAssetBuilder
|
||||
var existing = ReadExistingSecrets(root);
|
||||
|
||||
// Order matches Setup's Install(): cert first (yields the password the env file needs).
|
||||
// Reuse the identity cert + its password when already present so `apply` doesn't rotate it.
|
||||
// Reuse the identity cert and password if present so apply doesn't rotate it.
|
||||
var identityCertPath = Path.Combine(root, "identity", "identity.pfx");
|
||||
var identityCertPassword = existing.IdentityCertPassword ?? SecureRandom.String(32);
|
||||
if (existing.IdentityCertPassword is null || !File.Exists(identityCertPath))
|
||||
@@ -131,8 +130,7 @@ public static class StandardAssetBuilder
|
||||
if (!config.PushNotifications)
|
||||
globalOverride["globalSettings__pushRelayBaseUri"] = "REPLACE";
|
||||
|
||||
// Manifest `config:` passthrough LAST so it overrides the defaults above (e.g. flips
|
||||
// mail__smtp__host from REPLACE to the operator's value) — mirrors LiteDeployment.
|
||||
// Manifest `config:` passthrough last so it overrides the defaults above (e.g. SMTP).
|
||||
if (install.Config is not null)
|
||||
foreach (var kv in install.Config)
|
||||
globalOverride[kv.Key] = kv.Value;
|
||||
|
||||
Reference in New Issue
Block a user