diff --git a/core/README.md b/core/README.md index ea07a91cf..d44344c94 100644 --- a/core/README.md +++ b/core/README.md @@ -155,18 +155,27 @@ curl -fsSL https://install.danklinux.com | sh **Headless (unattended):** -Headless mode requires cached sudo credentials. Run `sudo -v` first: +Headless mode requires cached credentials or passwordless privilege escalation (sudo/doas/run0): ```bash -sudo -v && curl -fsSL https://install.danklinux.com | sh -s -- -c niri -t ghostty -y -sudo -v && curl -fsSL https://install.danklinux.com | sh -s -- -c hyprland -t kitty --include-deps dms-greeter -y +curl -fsSL https://install.danklinux.com | sh -s -- -c niri -t ghostty -p sudo -y +curl -fsSL https://install.danklinux.com | sh -s -- -c hyprland -t kitty --git-deps niri,quickshell -a -y +curl -fsSL https://install.danklinux.com | sh -s -- -c mango -t alacritty --git --danksearch --dankcalendar -y ``` | Flag | Short | Description | |------|-------|-------------| -| `--compositor ` | `-c` | Compositor/WM to install (required for headless) | -| `--term ` | `-t` | Terminal emulator (required for headless) | -| `--include-deps ` | | Enable optional dependencies (e.g. `dms-greeter`) | +| `--compositor ` | `-c` | Compositor/WM to install (required for headless) | +| `--term ` | `-t` | Terminal emulator (required for headless) | +| `--privesc ` | `-p` | Explicit privilege escalation tool | +| `--git` / `--git-all` | | Use git/development versions for all supported components | +| `--git-deps ` | | Specify dependencies to install git versions of (e.g. `niri,quickshell`) | +| `--all` / `--all-features` | `-a` | Install all optional features and dependencies | +| `--no-features` | | Skip all optional features and dependencies | +| `--dms-greeter` | | Install dms-greeter optional package | +| `--danksearch` | | Install danksearch and enable user indexing service | +| `--dankcalendar` | | Install dankcalendar package | +| `--include-deps ` | | Enable specific optional dependencies | | `--exclude-deps ` | | Skip specific dependencies | | `--replace-configs ` | | Replace specific configuration files (mutually exclusive with `--replace-configs-all`) | | `--replace-configs-all` | | Replace all configuration files (mutually exclusive with `--replace-configs`) | @@ -174,7 +183,7 @@ sudo -v && curl -fsSL https://install.danklinux.com | sh -s -- -c hyprland -t ki Headless mode requires `--yes` to proceed; without it, the installer exits with an error. Configuration files are not replaced by default unless `--replace-configs` or `--replace-configs-all` is specified. -`dms-greeter` is disabled by default; use `--include-deps dms-greeter` to enable it. +Use `--all` / `-a` to automatically enable all optional packages (`dms-greeter`, `danksearch`, `dankcalendar`). When no flags are provided, `dankinstall` launches the interactive TUI. diff --git a/core/cmd/dankinstall/main.go b/core/cmd/dankinstall/main.go index 389992ab1..f527c0f3c 100644 --- a/core/cmd/dankinstall/main.go +++ b/core/cmd/dankinstall/main.go @@ -18,6 +18,12 @@ var Version = "dev" var ( compositor string term string + privescTool string + gitAll bool + gitDeps []string + allFeatures bool + noFeatures bool + dmsGreeter bool includeDeps []string excludeDeps []string replaceConfigs []string @@ -35,8 +41,7 @@ var rootCmd = &cobra.Command{ Without flags, it launches an interactive TUI. Providing either --compositor or --term activates headless (unattended) mode, which requires both flags. -Headless mode requires cached sudo credentials. Run 'sudo -v' beforehand, or -configure passwordless sudo for your user.`, +Headless mode requires cached credentials or passwordless privilege escalation.`, Args: cobra.NoArgs, RunE: runDankinstall, SilenceErrors: true, @@ -46,7 +51,15 @@ configure passwordless sudo for your user.`, func init() { rootCmd.Flags().StringVarP(&compositor, "compositor", "c", "", "Compositor/WM to install: niri, hyprland, or mango (enables headless mode)") rootCmd.Flags().StringVarP(&term, "term", "t", "", "Terminal emulator to install: ghostty, kitty, or alacritty (enables headless mode)") - rootCmd.Flags().StringSliceVar(&includeDeps, "include-deps", []string{}, "Optional deps to enable (e.g. dms-greeter)") + rootCmd.Flags().StringVarP(&privescTool, "privesc", "p", "", "Privilege escalation tool: sudo, doas, or run0") + rootCmd.Flags().BoolVar(&gitAll, "git-all", false, "Use git/development versions for all supported components") + rootCmd.Flags().BoolVar(&gitAll, "git", false, "Use git/development versions for all supported components (alias for --git-all)") + rootCmd.Flags().StringSliceVar(&gitDeps, "git-deps", []string{}, "Comma-separated list of dependencies to use git versions for") + rootCmd.Flags().BoolVarP(&allFeatures, "all-features", "a", false, "Install all optional features and dependencies") + rootCmd.Flags().BoolVar(&allFeatures, "all", false, "Install all optional features and dependencies (alias for --all-features)") + rootCmd.Flags().BoolVar(&noFeatures, "no-features", false, "Skip all optional features and dependencies") + rootCmd.Flags().BoolVar(&dmsGreeter, "dms-greeter", false, "Install dms-greeter optional package") + rootCmd.Flags().StringSliceVar(&includeDeps, "include-deps", []string{}, "Optional deps to enable (e.g. dms-greeter, danksearch)") rootCmd.Flags().StringSliceVar(&excludeDeps, "exclude-deps", []string{}, "Deps to skip during installation") rootCmd.Flags().StringSliceVar(&replaceConfigs, "replace-configs", []string{}, "Deploy only named configs (e.g. niri,ghostty)") rootCmd.Flags().BoolVar(&replaceConfigsAll, "replace-configs-all", false, "Deploy and replace all configurations") @@ -73,6 +86,14 @@ func runDankinstall(cmd *cobra.Command, args []string) error { if !headlessMode { // Reject headless-only flags when running in TUI mode. headlessOnly := []string{ + "privesc", + "git-all", + "git", + "git-deps", + "all-features", + "all", + "no-features", + "dms-greeter", "include-deps", "exclude-deps", "replace-configs", @@ -110,6 +131,12 @@ func runHeadless() error { cfg := headless.Config{ Compositor: compositor, Terminal: term, + PrivescTool: privescTool, + GitAll: gitAll, + GitDeps: gitDeps, + AllFeatures: allFeatures, + NoFeatures: noFeatures, + DmsGreeter: dmsGreeter, IncludeDeps: includeDeps, ExcludeDeps: excludeDeps, ReplaceConfigs: replaceConfigs, diff --git a/core/install.sh b/core/install.sh index baca5881b..0863d0d44 100755 --- a/core/install.sh +++ b/core/install.sh @@ -35,11 +35,13 @@ aarch64) ;; esac +REPO_SLUG="${DMS_REPO:-AvengeMedia/DankMaterialShell}" + # Get the latest release version -LATEST_VERSION=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') +LATEST_VERSION=$(curl -s "https://api.github.com/repos/${REPO_SLUG}/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') if [ -z "$LATEST_VERSION" ]; then - printf "%bError: Could not fetch latest version%b\n" "$RED" "$NC" + printf "%bError: Could not fetch latest version from %s%b\n" "$RED" "$REPO_SLUG" "$NC" exit 1 fi @@ -51,8 +53,8 @@ cd "$TEMP_DIR" || exit 1 # Download the gzipped binary and its checksum printf "%bDownloading installer...%b\n" "$GREEN" "$NC" -curl -L "https://github.com/AvengeMedia/DankMaterialShell/releases/download/$LATEST_VERSION/dankinstall-$ARCH.gz" -o "installer.gz" -curl -L "https://github.com/AvengeMedia/DankMaterialShell/releases/download/$LATEST_VERSION/dankinstall-$ARCH.gz.sha256" -o "expected.sha256" +curl -L "https://github.com/${REPO_SLUG}/releases/download/$LATEST_VERSION/dankinstall-$ARCH.gz" -o "installer.gz" +curl -L "https://github.com/${REPO_SLUG}/releases/download/$LATEST_VERSION/dankinstall-$ARCH.gz.sha256" -o "expected.sha256" # Get the expected checksum EXPECTED_CHECKSUM=$(awk '{print $1}' expected.sha256) @@ -79,7 +81,7 @@ chmod +x installer # Execute the installer printf "%bRunning installer...%b\n" "$GREEN" "$NC" -./installer +./installer "$@" # Cleanup cd - >/dev/null diff --git a/core/internal/headless/runner.go b/core/internal/headless/runner.go index e5f664c3e..b7c8fbc75 100644 --- a/core/internal/headless/runner.go +++ b/core/internal/headless/runner.go @@ -34,8 +34,14 @@ var orderedConfigNames = []string{"niri", "hyprland", "ghostty", "kitty", "alacr // Config holds all CLI parameters for unattended installation. type Config struct { - Compositor string // "niri" or "hyprland" - Terminal string // "ghostty", "kitty", or "alacritty" + Compositor string // "niri", "hyprland", or "mango" + Terminal string // "ghostty", "kitty", or "alacritty" + PrivescTool string // "sudo", "doas", or "run0" + GitAll bool // install git version of all supported components + GitDeps []string // specific dependencies to force git version + AllFeatures bool // enable all optional features + NoFeatures bool // disable all optional features + DmsGreeter bool // install dms-greeter IncludeDeps []string ExcludeDeps []string ReplaceConfigs []string // specific configs to deploy (e.g. "niri", "ghostty") @@ -68,6 +74,12 @@ func (r *Runner) GetLogChan() <-chan string { func (r *Runner) Run() error { r.log("Starting headless installation") + if r.cfg.PrivescTool != "" { + if err := privesc.SetTool(privesc.Tool(strings.ToLower(r.cfg.PrivescTool))); err != nil { + return fmt.Errorf("failed to set privilege escalation tool %q: %w", r.cfg.PrivescTool, err) + } + } + // 1. Parse compositor and terminal selections wm, err := r.parseWindowManager() if err != nil { @@ -112,6 +124,10 @@ func (r *Runner) Run() error { return fmt.Errorf("dependency detection failed: %w", err) } + if err := r.applyGitVariants(dependencies); err != nil { + return err + } + // 5. Apply include/exclude filters and build the disabled-items map. // Headless mode does not currently collect any explicit reinstall selections, // so keep reinstallItems nil instead of constructing an always-empty map. @@ -283,18 +299,68 @@ func (r *Runner) Run() error { return nil } +// applyGitVariants sets VariantGit on dependencies matching git flags or --git-deps. +func (r *Runner) applyGitVariants(dependencies []deps.Dependency) error { + gitDepsMap := make(map[string]bool) + for _, raw := range r.cfg.GitDeps { + for _, item := range strings.Split(raw, ",") { + item = strings.TrimSpace(strings.ToLower(item)) + if item != "" { + gitDepsMap[item] = true + } + } + } + + for target := range gitDepsMap { + found := false + for i := range dependencies { + depNameLower := strings.ToLower(dependencies[i].Name) + if depNameLower == target || (target == "dms" && depNameLower == "dms (dankmaterialshell)") { + found = true + if !dependencies[i].CanToggle { + return fmt.Errorf("--git-deps: dependency %q does not support git variant", dependencies[i].Name) + } + dependencies[i].Variant = deps.VariantGit + } + } + if !found { + return fmt.Errorf("--git-deps: unknown dependency %q", target) + } + } + + if r.cfg.GitAll { + for i := range dependencies { + if dependencies[i].CanToggle { + dependencies[i].Variant = deps.VariantGit + } + } + } + + return nil +} + // buildDisabledItems computes the set of dependencies that should be skipped // during installation. Optional components are opt-in (disabled by default), // then re-enabled by the dedicated flags and --include-deps. func (r *Runner) buildDisabledItems(dependencies []deps.Dependency) (map[string]bool, error) { + if r.cfg.AllFeatures && r.cfg.NoFeatures { + return nil, fmt.Errorf("cannot specify both --all-features/--all and --no-features") + } + disabledItems := make(map[string]bool) - for i := range dependencies { - if !dependencies[i].Required { - disabledItems[dependencies[i].Name] = true + if !r.cfg.AllFeatures { + for i := range dependencies { + if !dependencies[i].Required { + disabledItems[dependencies[i].Name] = true + } } } + if r.cfg.NoFeatures { + return disabledItems, nil + } + // Dedicated flags resolve before include/exclude if r.cfg.DankSearch { if !r.depExists(dependencies, "danksearch") { @@ -308,6 +374,12 @@ func (r *Runner) buildDisabledItems(dependencies []deps.Dependency) (map[string] } delete(disabledItems, "dankcalendar") } + if r.cfg.DmsGreeter { + if !r.depExists(dependencies, "dms-greeter") { + return nil, fmt.Errorf("--dms-greeter: not available on this distribution") + } + delete(disabledItems, "dms-greeter") + } // Process --include-deps (enable items that are disabled by default) for _, name := range r.cfg.IncludeDeps { diff --git a/core/internal/headless/runner_test.go b/core/internal/headless/runner_test.go index d667644e5..6cd568f83 100644 --- a/core/internal/headless/runner_test.go +++ b/core/internal/headless/runner_test.go @@ -442,6 +442,23 @@ func TestBuildDisabledItems(t *testing.T) { wantErr: true, errContains: "--dankcalendar", }, + { + name: "allFeatures enables all optional features", + allFeatures: true, + wantEnabled: []string{"dms-greeter", "danksearch", "dankcalendar"}, + }, + { + name: "noFeatures disables all optional features", + noFeatures: true, + wantDisabled: []string{"dms-greeter", "danksearch", "dankcalendar"}, + }, + { + name: "both allFeatures and noFeatures set returns error", + allFeatures: true, + noFeatures: true, + wantErr: true, + errContains: "cannot specify both --all-features/--all and --no-features", + }, } for _, tt := range tests { @@ -451,6 +468,8 @@ func TestBuildDisabledItems(t *testing.T) { ExcludeDeps: tt.excludeDeps, DankSearch: tt.dankSearch, DankCalendar: tt.dankCalendar, + AllFeatures: tt.allFeatures, + NoFeatures: tt.noFeatures, }) d := tt.deps if d == nil { @@ -483,11 +502,49 @@ func TestBuildDisabledItems(t *testing.T) { t.Errorf("expected %q to NOT be disabled, but it is", name) } } - - // If wantDisabled is empty, the map should have length 0 - if len(tt.wantDisabled) == 0 && len(got) != 0 { - t.Errorf("expected empty disabledItems map, got %v", got) - } }) } } + +func TestApplyGitVariants(t *testing.T) { + r := NewRunner(Config{ + Compositor: "niri", + GitDeps: []string{"niri", "quickshell", "matugen"}, + }) + + dependencies := []deps.Dependency{ + {Name: "niri", Variant: deps.VariantStable, CanToggle: true}, + {Name: "quickshell", Variant: deps.VariantStable, CanToggle: true}, + {Name: "matugen", Variant: deps.VariantStable, CanToggle: true}, + {Name: "ghostty", Variant: deps.VariantStable, CanToggle: false}, + } + + if err := r.applyGitVariants(dependencies); err != nil { + t.Fatalf("applyGitVariants unexpected error: %v", err) + } + + if dependencies[0].Variant != deps.VariantGit { + t.Errorf("expected niri variant to be VariantGit, got %v", dependencies[0].Variant) + } + if dependencies[1].Variant != deps.VariantGit { + t.Errorf("expected quickshell variant to be VariantGit, got %v", dependencies[1].Variant) + } + if dependencies[2].Variant != deps.VariantGit { + t.Errorf("expected matugen variant to be VariantGit, got %v", dependencies[2].Variant) + } + if dependencies[3].Variant != deps.VariantStable { + t.Errorf("expected ghostty variant to stay VariantStable (CanToggle=false), got %v", dependencies[3].Variant) + } + + // Test unknown dependency in --git-deps returns error + rErr := NewRunner(Config{GitDeps: []string{"nonexistent"}}) + if err := rErr.applyGitVariants(dependencies); err == nil { + t.Error("expected error for unknown git dependency, got nil") + } + + // Test non-togglable dependency in --git-deps returns error + rCantToggle := NewRunner(Config{GitDeps: []string{"ghostty"}}) + if err := rCantToggle.applyGitVariants(dependencies); err == nil { + t.Error("expected error for non-togglable git dependency, got nil") + } +}