#!/usr/bin/env sh
:; # -*- mode: emacs-lisp; lexical-binding: t; no-byte-compile: t -*-
:; case "$EMACS" in *term*) EMACS=emacs ;; *) EMACS="${EMACS:-emacs}" ;; esac
:; [ "x$EMACS" = xemacs ] && { type emacs >/dev/null 2>&1 || err=1; }
:; [ "x$err" = x ] || { echo "Error: failed to run Emacs with command '$EMACS'"; echo; echo "Are you sure Emacs is installed and in your \$PATH?"; exit 1; } >&2
:; emacs="$EMACS ${DEBUG:+--debug-init} -q --no-site-file --batch"
:; export EMACS_PLUS_NO_PATH_INJECTION=1
:; export __DOOMPID="${__DOOMPID:-$$}"
:; export __DOOMSTEP="${__DOOMSTEP:-0}"
:; export __DOOMGEOM="${__DOOMGEOM:-`tput cols 2>/dev/null`x`tput lines 2>/dev/null`}"
:; export __DOOMGPIPE=${__DOOMGPIPE:-$__DOOMPIPE}
:; export __DOOMPIPE=; [ -t 0 ] || __DOOMPIPE="${__DOOMPIPE}0"; [ -t 1 ] || __DOOMPIPE="${__DOOMPIPE}1"
:; eval "$emacs --eval \"(setq warning-inhibit-types '((files missing-lexbind-cookie)))\" --load \"$0\"" -- "$@" || exit=$?
:; [ "${exit:-0}" -eq 254 ] && { export TMPDIR="${TMPDIR:-${TMP:-${TEMP:-`eval "$emacs -Q --eval '(princ temporary-file-directory)'" 2>/dev/null`}}}"; sh "${TMPDIR}/doom.${__DOOMPID}.${__DOOMSTEP}.sh" "$0" "$@" && true; exit="$?"; }
:; exit $exit

;; This magical mess of a shebang is necessary for any script that relies on
;; Doom's CLI framework, because Emacs' tty libraries and capabilities are too
;; immature (borderline non-existent). This shebang sets out to accomplish these
;; three goals:
;;
;; 1. To produce a more helpful error if Emacs isn't installed or broken. It
;;    must do so without assuming whether $EMACS is a shell command (e.g. 'snap
;;    run emacs') or an absolute path (to an emacs executable). I've avoided
;;    'command -v $EMACS' for this reason.
;;
;; 2. To allow this Emacs session to "exit into" a child process (since Elisp
;;    lacks an analogue for exec system calls) by calling an auto-generated and
;;    self-destructing "exit script" if the parent Emacs process exits with code
;;    254. It takes care to prevent nested child instances from clobbering the
;;    exit script.
;;
;; 3. To expose some information about the terminal and session:
;;    - $__DOOMGEOM holds the dimensions of the terminal (W . H).
;;    - $__DOOMPIPE indicates whether the script has been piped (in and/or out).
;;    - $__DOOMGPIPE indicates whether one of this process' parent has been
;;      piped to/from.
;;    - $__DOOMPID is a unique identifier for the parent script, so
;;      child processes can identify which persistent data files (like logs) it
;;      has access to.
;;    - $__DOOMSTEP counts how many levels deep we are in the dream (appending
;;      this to the exit script's filename avoids child processes clobbering the
;;      same exit script and causing read errors).
;;    - $TMPDIR (or $TEMP and $TMP on Windows) aren't guaranteed to have values,
;;      and mktemp isn't available on all systems, but you know what is? Emacs!
;;      So I use it to print `temporary-file-directory'. And it seconds as a
;;      quick sanity check for Emacs' existence (for goal #1).
;;
;; Other weird facts about this shebang line:
;;
;; - The :; hack exploits properties of : and ; in shell scripting and elisp to
;;   allow shell script and elisp to coexist in the same file without either's
;;   interpreter throwing foreign syntax errors:
;;
;;   - In elisp, ":" is a valid keyword symbol literal; it evaluates to itself
;;     and has no side-effect.
;;   - In the shell, ":" is a valid command that does nothing and ignores its
;;     arguments.
;;   - In elisp, ";" begins a comment. I.e. the interpreter ignores everything
;;     after it.
;;   - In the shell, ";" is a command separator.
;;
;;   Put together, plus a strategically placed exit call, the shell will read
;;   one part of this file and ignore the rest, while the elisp interpreter will
;;   do the opposite.
;; - I intentionally suppress loading site files (must be done with '-q
;;   --no-site-file' NOT '-Q', as the latter omits the site-lisp dir from
;;   `load-path' too), so `doom-initialize' can load them manually later (so I
;;   can suppress the extraneous output that site-lisp files produce).
;; - POSIX-compliancy is paramount: there's no guarantee what /bin/sh will be
;;   symlinked to in the esoteric OSes/distros Emacs users use.
;; - The user may have mounted /tmp with a noexec flag, so pass the exit script
;;   to /bin/sh rather than executing them directly.
;; - The (setq warning-inhibit-types '((files missing-lexbind-cookie))) is
;;   necessary on later versions of Emacs 31, where Emacs will throw warnings
;;   when loading elisp that lack 'lexical-binding: t' header line. Setting it
;;   here or in early-init.el is not enough, though I do it anyway.

(unless default-directory
  (message "Error: executed from a non-existent directory. CD somewhere safe, first!")
  (kill-emacs 1))

;; Doom's core sets up everything we need; including `doom-*-dir' variables,
;; universal defaults, and autoloads for doom-*-initialize functions.
(condition-case e
    (let* ((bin-dir (file-name-directory (file-truename load-file-name)))
           (init-file (expand-file-name "../early-init.el" bin-dir)))
      (or (and (load init-file nil 'nomessage 'nosuffix)
               (featurep 'doom))
          (user-error "Failed to load Doom from %s" init-file)))
  ;; Prevent ugly backtraces for trivial errors
  (user-error (message "Error: %s" (cadr e))
              (kill-emacs 2)))

;; UX: Abort if the user is using 'doom' as root, unless $EMACSDIR is owned by
;;   root, in which case we can safely assume the user genuinely wants root to
;;   be their primary user account for this session.
(when (equal 0 (user-real-uid))
  (unless (equal 0 (file-attribute-user-id (file-attributes doom-emacs-dir)))
    (message
     (concat
      "Error: this script was executed as root, which is likely not what you want.\n"
      "It will cause file permissions errors later, when you run Doom as another\n"
      "user.\n\n"
      "If this really *is* what you want, then change the owner of your Emacs\n"
      "config to root:\n\n"
      ;; TODO Add cmd.exe/powershell commands
      "  chown root:root -R " (abbreviate-file-name doom-emacs-dir) "\n\n"
      "Aborting..."))
    (kill-emacs 2)))


;;
;;; * Entry point

(defcli! doom (&args _command)
  "A command line interface to Doom Emacs.

Includes package management, diagnostics, unit tests, and byte-compilation.

This tool also makes it trivial to launch Emacs out of a different folder or
with a different private module.

ENVIRONMENT VARIABLES:
  `$EMACS'
    The Emacs executable or command to use for any Emacs operations in this or
    other Doom CLI shell scripts (default: first emacs found in `$PATH').

  `$EMACSDIR'
    The location of your Doom Emacs installation (defaults to ~/.config/emacs or
    ~/.emacs.d; whichever is found first). This is *not* your private Doom
    configuration. The `--emacsdir' option also sets this variable.

  `$DOOMDIR'
    The location of your private configuration for Doom Emacs (defaults to
    ~/.config/doom or ~/.doom.d; whichever it finds first). This is *not* the
    place you've cloned doomemacs/core to. The `--doomdir' option also sets this
    variable.

  `$DOOMPAGER'
    The pager to invoke for large output (default: \"less +g\"). The `--pager'
    option also sets this variable.

  `$DOOMPATH'
    A colon-delimited list of directories where Doom's CLI searches for doom-*
    executables when invoking an unrecognized command.

  `$DOOMPROFILE'
    Which Doom profile to activate (default: \"_@0\"). The `--profile' option
    also sets this variable.

  `$DOOMPROFILELOADFILE'
    Doom generates a profile loader script on 'doom sync' or 'doom profile
    sync'. By default, this file is written to and loaded from
    $EMACSDIR/profiles/load.el. Set this envvar to change that. Note that this
    envvar must be in scope for both interactive and non-interactive sessions
    for it to be effective. This is especially useful for folks on Nix/Guix, who
    have deployed Doom to a read-only directory.

    Note: this file *must* end with a .el extension. It will be byte-compiled
    after it is generated.

  `$DOOMPROFILELOADPATH'
    A colon-delimited (semi-colon on Windows) list of profile config files or
    directories under which Doom will search for implicit profiles. See
    `var:doom-profile-load-path' for its default value.

EXIT CODES:
  0        Successful run
  1        No error occurred, but the command couldn't complete
  2        General internal error
  3        Error with Emacs/Doom install or execution context
  4        Unrecognized user input error
  5        Command not found, or is incorrect/deprecated
  6        Invalid, missing, or extra options/arguments
  7-15     Reserved for Doom
  16-192   Reserved for the user's extensions
  254      Successful run (then execute the dynamically generated after-script)
  255      Uncaught critical errors

SEE ALSO:
  https://doomemacs.org               Homepage
  https://docs.doomemacs.org          Official documentation
  https://wiki.doomemacs.org          Community wiki
  https://doomemacs.org/discuss       Github discussions & user support board
  https://doomemacs.org/discord       Discord chat server"
  :partial t)

(defcli! :before
    ((force?   ("-!" "--force")   "Suppress prompts by auto-accepting their consequences")
     (debug?   ("-D" "--debug")   "Enable debug output")
     (verbose? ("-v" "--verbose") "Enable verbose output")
     (doomdir  ("--doomdir" dir)  "Use Doom config living in `DIR' (e.g. ~/.doom.d)")
     (emacsdir ("--emacsdir" dir) "Use Doom install living in `DIR' (e.g. ~/.emacs.d)")
     (pager    ("--pager" cmd)    "Pager command to use for large output")
     (profile  ("--profile" name) "Use profile named NAME")
     (bench?   ("--benchmark")    "Always display the benchmark")
     &flags
     (color?   ("--color") "Whether or not to show ANSI color codes")
     &multiple
     (loads    ("-L" "--load" "--strict-load" file) "Load elisp `FILE' before executing `COMMAND'")
     (evals    ("-E" "--eval" form) "Evaluate `FORM' before executing commands")
     &input input
     &context context
     &args _)
  "OPTIONS:
  -E, -eval
    Can be used multiple times.

  -L, --load, --strict-load
    Can be used multiple times to load multiple files. Both -L and --load will
    silently fail on missing files, but --strict-load won't.

    Warning: files loaded this way load too late to define new commands. To
    define commands, do so from `$DOOMDIR'/cli.el, `$DOOMDIR'/init.el, or a
    doom-* executable with a doomscript shebang line in `$DOOMDIR'/bin/ or
    `$DOOMPATH' (which you can set)."
  (when bench?
    (setq doom-cli-benchmark-threshold 'always))
  (unless init-file-debug ; debug-mode implies verbose
    (when verbose?
      (setq doom-print-level 'info)))
  (when color?
    (setq doom-print-backend (if (eq color? :yes) 'ansi)))
  (when pager
    (setq doom-cli-pager pager))
  (when force?
    (put! :suppress-prompts? t)
    ;; DEPRECATED: Remove in v3
    (setf (doom-cli-context-suppress-prompts-p context) t))
  ;; For these settings to take full effect, the script must be restarted:
  (when (or debug?
            profile
            emacsdir
            doomdir)
    (let (omit)
      (when debug?
        (setenv "DEBUG" "3")
        (setq init-file-debug t)
        (push "--debug" omit)
        (push "-D" omit))
      (when profile
        (setenv "DOOMPROFILE" profile)
        (push "--profile=" omit))
      (when emacsdir
        (setenv "EMACSDIR" emacsdir)
        (push "--emacsdir=" omit))
      (when doomdir
        (setenv "DOOMDIR" doomdir)
        (push "--doomdir=" omit))
      (exit! :restart :omit omit)))
  ;; Load extra files and forms, as per given options.
  (dolist (file loads)
    (load (doom-path (cdr file))
          (not (equal (car file) "--strict-load"))
          (not init-file-debug) t))
  (dolist (form evals)
    (eval (read (cdr form)) t)))


;;
;;; * Load user config

;; Load $DOOMDIR/init.el, to read the user's `doom!' block and give users an
;; opportunity to customize the CLI environment, if they like. Otherwise, they
;; can do so in .doom or $DOOMDIR/cli.el
(doom-modules-initialize t)

;; Look for doom-* executables and autoloaded CLIs in these places. Like $PATH,
;; but for Doom CLIs.
(add-to-list 'doom-cli-load-path (doom-emacs-dir "bin/"))
(add-to-list 'doom-cli-load-path (doom-user-dir "bin/"))
(when-let* ((doompath (getenv "DOOMPATH")))
  (dolist (dir (nreverse (split-string doompath path-separator)))
    (add-to-list 'doom-cli-load-path dir)))

(add-to-list 'load-path (doom-emacs-dir "bin/"))


;;
;;; * CLI Bootstrapper

(defvar doom-cli-loaddefs-file
  (doom-cache-dir (format "doom-cli-loaddefs.%s.el" doom-version))
  "Where to store Doom's CLI autoloads.")

;; There are a lot of CLIs, and some have expensive initialization, so best we
;; load them lazily.
(defcli-group!
  :prefix 'doom
  (defcli-alias! ((version v)) (:root :version))
  ;; Import this for implicit 'X help' commands for your script:
  (defcli-alias! ((help h)) (:root :help))
  ;; And suggest its use when errors occur.
  (add-to-list 'doom-help-commands "%p h[elp] %c")

  ;; Load or generate the CLI autoloads.
  (or (with-demoted-errors "doom-cli-loaddefs: %s"
        (require 'doom-cli-loaddefs doom-cli-loaddefs-file t))
      (require 'doom-cli-loaddefs
               (doom-file-write
                doom-cli-loaddefs-file
                `(";; -*- lexical-binding: t; no-byte-compile t; -*-"
                  ,@(doom-loaddefs--read
                     (seq-filter #'doom-cli-executable-p
                                 (doom-glob doom-emacs-dir "bin/doom-*"))
                     #'doom-loaddefs--scan-file nil (doom-emacs-dir "bin/"))
                  (provide 'doom-cli-loaddefs)))))

  ;; Eagerly load the two commands that should always be present. Running one of
  ;; these commands is required for the rest of the commands to work.
  (let ((doom-cli--loading t))
    (load! "doom-install")
    (load! "doom-sync"))

  (defcli-alias! s sync)
  (defcli-alias! pf profile)
  (defcli-alias! up upgrade)
  (defcli-alias! doc doctor)

  (defcli-stub! module)
  (defcli-stub! nuke)
  (defcli-stub! package)
  (defcli-stub! rollback)

  (defcli! run ()
    "Project-local tasks (for build pipelines, CI/CD, etc).

If this command has no sub-commands, it's because no enabled modules define any
(did you remember to enable :doom cli?) or the current project doesn't define
any from its .doom file or .doom.d directory.

If you expected this command to launch Doom Emacs, that command was moved to
'doom emacs' in v2.1.0."
    :partial t)

  ;; DEPRECATED: Backwards compatibility will be removed in v3.
  (defcli-obsolete! (profiles sync) (profile sync "--all") "2.1.0")
  (defcli-obsolete! ((build b)) (sync "--rebuild") "2.1.0")
  (defcli-obsolete! ((purge p)) (gc) "2.1.0")

  (with-doom-context 'module
    (dolist (key (doom-module-list))
      (when (eq (car key) :doom)
        (when-let* ((bindir (doom-module-locate-path key "bin/")))
          (add-to-list 'doom-cli-exec-path (doom-module-locate-path key "bin/") 'append)))
      (when-let* ((path (doom-module-locate-path key doom-module-cli-file)))
        (if (eq (car key) :doom)
            (doom-load (file-name-sans-extension path))
          (defcli-group! "Module commands" :prefix (if (cdr key) (format "+%s" (cdr key)))
            (doom-load (file-name-sans-extension path))))))))


;;
;;; * Let 'er rip

(run! "doom" (cdr (member "--" argv)))

;;; doom ends here, unless...
