easy-command 1.0.4 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,109 +1,106 @@
1
- # easy-command
2
-
3
- One command to configure a polished Zsh terminal with Oh My Zsh, the `agnoster` theme, Git status, command suggestions, Tab completion, and syntax highlighting.
4
-
5
- > 一条命令配置统一的 Zsh 终端:Oh My Zsh、`agnoster` 主题、Git 状态、历史命令建议、Tab 补全与命令语法高亮。
1
+ <p align="right">
2
+ <strong>ENGLISH</strong> | <a href="README_CN.md">中文</a>
3
+ </p>
6
4
 
7
- ## Preview
5
+ # easy-command
8
6
 
9
- > 效果预览。
7
+ Configure a polished Zsh terminal in one command.
10
8
 
11
- ### Git branch and working-tree status
9
+ `easy-command` installs and configures Oh My Zsh, the `agnoster` theme, Git status, command suggestions, Tab completion, and syntax highlighting. It preserves your existing `.zshrc` content and creates a backup before every change.
12
10
 
13
- > Git 分支与工作区状态。
11
+ ## INSTALL
14
12
 
15
- ![agnoster displays Git branch and working-tree status](assets/git-status.png)
13
+ ### npm
16
14
 
17
- ### History-based command suggestions
15
+ ```bash
16
+ npx easy-command -y
17
+ ```
18
18
 
19
- > 历史命令自动建议。
19
+ ### GitHub
20
20
 
21
- ![A suggested history command appears while typing](assets/autosuggestions.png)
21
+ No Node.js or npm is required:
22
22
 
23
- ### Syntax highlighting and error feedback
23
+ ```bash
24
+ curl -fsSL https://raw.githubusercontent.com/UnionSchool/easy-command/main/install.sh | bash -s -- -y
25
+ ```
24
26
 
25
- > 命令语法高亮与错误提示。
27
+ When installation finishes, open a new terminal session or run:
26
28
 
27
- ![Valid commands are green and unknown commands are red](assets/syntax-highlighting.png)
29
+ ```bash
30
+ exec zsh -l
31
+ ```
28
32
 
29
- ## Quick start
33
+ ## Everyday commands
30
34
 
31
- > 快速开始。
35
+ ```bash
36
+ # Preview installation without changing your machine
37
+ npx easy-command --dry-run
32
38
 
33
- Install the latest version:
39
+ # Check Shell, dependencies, managed repositories, and .zshrc configuration
40
+ npx easy-command doctor
34
41
 
35
- > 安装最新版本:
42
+ # Restore missing managed files and configuration
43
+ npx easy-command repair -y
36
44
 
37
- ```bash
38
- npx easy-command --yes
45
+ # Safely update Oh My Zsh and managed plugins
46
+ npx easy-command update -y
39
47
  ```
40
48
 
41
- Or install directly from GitHub without Node.js/npm:
49
+ `npx` runs the command without a global npm installation. If you prefer the shorter `easy-command` command, install it globally with `npm install -g easy-command`.
42
50
 
43
- > 或者无需 Node.js/npm,直接从 GitHub 安装:
51
+ ## What you get
44
52
 
45
- ```bash
46
- curl -fsSL https://raw.githubusercontent.com/UnionSchool/easy-command/main/install.sh | bash -s -- --yes
47
- ```
53
+ - A clean `agnoster` prompt with your user, path, Git branch, and working-tree status.
54
+ - History-based command suggestions. Press `→` or `Ctrl+E` to accept one.
55
+ - Syntax highlighting: valid commands are green; unknown commands are bold red.
56
+ - Native Zsh completion. Press `Tab` to complete; press it twice to view candidates.
57
+ - Shared history across terminal sessions, retaining the latest 10,000 commands.
58
+ - Built-in preview, diagnostics, repair, and update commands for easy maintenance.
48
59
 
49
- After installation, sign in again or run:
60
+ ## Preview
50
61
 
51
- > 安装完成后重新登录,或运行:
62
+ ### Git branch and working-tree status
52
63
 
53
- ```bash
54
- exec zsh -l
55
- ```
64
+ ![agnoster displays Git branch and working-tree status](assets/git-status.png)
56
65
 
57
- ## What you get
66
+ ### History-based command suggestions
58
67
 
59
- > 提供的能力。
68
+ ![A suggested history command appears while typing](assets/autosuggestions.png)
60
69
 
61
- - `agnoster`: shows the user, current path, Git branch, and working-tree status.<br>
62
- `agnoster`:显示用户名、路径、Git 分支和工作区状态。
63
- - `zsh-autosuggestions`: shows gray suggestions from command history; press `→` or `Ctrl+E` to accept.<br>
64
- `zsh-autosuggestions`:从历史命令显示灰色建议,按 `→` 或 `Ctrl+E` 接受。
65
- - `zsh-syntax-highlighting`: valid commands are green; unknown commands are bold red.<br>
66
- `zsh-syntax-highlighting`:有效命令绿色、未知命令红色加粗。
67
- - Native Zsh completion: press `Tab` to complete commands, paths, and Git subcommands; press it twice for candidates.<br>
68
- Zsh 原生补全:按 `Tab` 补全命令、路径和 Git 子命令,连按两次显示候选项。
69
- - Shared history: the latest 10,000 commands are shared between terminal sessions.<br>
70
- 共享历史:多个终端会话共享最近 10,000 条命令。
70
+ ### Syntax highlighting and error feedback
71
71
 
72
- ## Options
72
+ ![Valid commands are green and unknown commands are red](assets/syntax-highlighting.png)
73
73
 
74
- > 参数。
74
+ ## Options
75
75
 
76
76
  ```bash
77
- # Configure a specific user / 指定目标用户
78
- bash install.sh --yes --user bell
77
+ # Configure a specific user
78
+ bash install.sh -y --user bell
79
79
 
80
- # Keep the current login shell / 不改变默认登录 Shell
81
- bash install.sh --yes --no-chsh
80
+ # Keep the current login shell
81
+ bash install.sh -y --no-chsh
82
82
 
83
- # Remove only the configuration block managed by easy-command / 删除本工具管理的 .zshrc 区块
83
+ # Remove only the configuration block managed by easy-command
84
84
  bash install.sh --uninstall
85
+
86
+ # Enable optional directory jumping, fuzzy search, and Git aliases
87
+ bash install.sh -y --with-zoxide --with-fzf --with-git-aliases
85
88
  ```
86
89
 
87
- ## Supported platforms
90
+ `zoxide` enables `z <keyword>` directory jumping. `fzf` adds fuzzy history and file searching. Git aliases use the non-conflicting `ec-` prefix, such as `git ec-status`; existing aliases are never overwritten.
88
91
 
89
- > 支持范围。
92
+ ## Requirements
90
93
 
91
- - macOS: Homebrew must already be installed.<br>
92
- macOS:要求已安装 Homebrew。
93
- - Ubuntu / Debian: dependencies are installed with `apt-get`.<br>
94
- Ubuntu / Debian:使用 `apt-get` 安装依赖。
94
+ - macOS: Homebrew must already be installed.
95
+ - Ubuntu / Debian: dependencies are installed with `apt-get`.
95
96
 
96
97
  The script needs `sudo` to install packages and change the default shell. Without sudo access, use `--no-chsh` and have an administrator install Zsh first.
97
98
 
98
- > 脚本需要 `sudo` 来安装软件包和切换默认 Shell。没有 sudo 权限时,可加 `--no-chsh`,并由管理员预先安装 Zsh。
99
-
100
99
  ## Configuration and rollback
101
100
 
102
- > 配置与回退。
103
-
104
- Installed files:
101
+ The installer manages only the marked `easy-command` block in `~/.zshrc`. Before each update, it creates a timestamped backup. Running `--uninstall` removes only that managed block; it does not remove Oh My Zsh, plugins, command history, or other personal settings.
105
102
 
106
- > 安装内容位于:
103
+ Installed components:
107
104
 
108
105
  ```text
109
106
  ~/.easy-command/oh-my-zsh
@@ -111,38 +108,26 @@ Installed files:
111
108
  ~/.zsh_history
112
109
  ```
113
110
 
114
- Before each `.zshrc` update, the script creates a timestamped backup. `--uninstall` removes only the configuration block managed by easy-command; it does not remove Oh My Zsh, plugins, or other user settings.
115
-
116
- > 每次更新 `.zshrc` 前,脚本会创建带时间戳的备份。`--uninstall` 只删除由 easy-command 管理的配置块,不会删除 Oh My Zsh、插件或用户自己的其他配置。
117
-
118
111
  ## Security
119
112
 
120
- > 安全说明。
121
-
122
113
  For a first-time trial, download and inspect the script before running it:
123
114
 
124
- > 首次试用建议先下载并审阅脚本:
125
-
126
115
  ```bash
127
116
  curl -fsSLO https://raw.githubusercontent.com/UnionSchool/easy-command/main/install.sh
128
117
  less install.sh
129
- bash install.sh --yes
118
+ bash install.sh -y
130
119
  ```
131
120
 
132
121
  For production, pin a release tag instead of running `main` directly:
133
122
 
134
- > 生产环境建议固定到发布标签,而不是直接使用 `main`:
135
-
136
123
  ```bash
137
- npx easy-command@1.0.4 --yes
124
+ npx easy-command@2.0.2 -y
138
125
  ```
139
126
 
140
127
  Or, without npm:
141
128
 
142
- > 或者不使用 npm:
143
-
144
129
  ```bash
145
- curl -fsSL https://raw.githubusercontent.com/UnionSchool/easy-command/v1.0.4/install.sh | bash -s -- --yes
130
+ curl -fsSL https://raw.githubusercontent.com/UnionSchool/easy-command/v2.0.2/install.sh | bash -s -- -y
146
131
  ```
147
132
 
148
133
  ## License
package/README_CN.md ADDED
@@ -0,0 +1,135 @@
1
+ <p align="right">
2
+ <a href="README.md">ENGLISH</a> | <strong>中文</strong>
3
+ </p>
4
+
5
+ # easy-command
6
+
7
+ 一条命令配置更顺手的 Zsh 终端。
8
+
9
+ `easy-command` 会安装并配置 Oh My Zsh、`agnoster` 主题、Git 状态、命令建议、Tab 补全和语法高亮。每次修改 `.zshrc` 前都会备份,并保留用户已有的配置内容。
10
+
11
+ ## 安装
12
+
13
+ ### npm 安装
14
+
15
+ ```bash
16
+ npx easy-command -y
17
+ ```
18
+
19
+ ### GitHub 安装
20
+
21
+ 无需安装 Node.js 或 npm:
22
+
23
+ ```bash
24
+ curl -fsSL https://raw.githubusercontent.com/UnionSchool/easy-command/main/install.sh | bash -s -- -y
25
+ ```
26
+
27
+ 安装完成后,重新打开终端,或执行:
28
+
29
+ ```bash
30
+ exec zsh -l
31
+ ```
32
+
33
+ ## 日常命令
34
+
35
+ ```bash
36
+ # 预览安装过程,不修改本机环境
37
+ npx easy-command --dry-run
38
+
39
+ # 检查 Shell、依赖、托管仓库和 .zshrc 配置
40
+ npx easy-command doctor
41
+
42
+ # 恢复缺失的托管文件和配置
43
+ npx easy-command repair -y
44
+
45
+ # 安全更新 Oh My Zsh 和托管插件
46
+ npx easy-command update -y
47
+ ```
48
+
49
+ `npx` 无需全局安装即可运行命令。若希望直接使用更短的 `easy-command` 命令,可执行 `npm install -g easy-command`。
50
+
51
+ ## 安装后即可使用
52
+
53
+ - `agnoster` 提示符:显示用户名、当前路径、Git 分支和工作区状态。
54
+ - 历史命令建议:输入时显示灰色建议,按 `→` 或 `Ctrl+E` 接受。
55
+ - 语法高亮:有效命令为绿色,未知命令为红色加粗。
56
+ - 原生 Zsh 补全:按 `Tab` 补全命令、路径和 Git 子命令;连按两次显示候选项。
57
+ - 多终端共享历史:保留最近 10,000 条命令。
58
+ - 内置预览、诊断、修复和更新命令,后续维护更简单。
59
+
60
+ ## 效果预览
61
+
62
+ ### Git 分支与工作区状态
63
+
64
+ ![agnoster displays Git branch and working-tree status](assets/git-status.png)
65
+
66
+ ### 历史命令自动建议
67
+
68
+ ![A suggested history command appears while typing](assets/autosuggestions.png)
69
+
70
+ ### 命令语法高亮与错误提示
71
+
72
+ ![Valid commands are green and unknown commands are red](assets/syntax-highlighting.png)
73
+
74
+ ## 常用参数
75
+
76
+ ```bash
77
+ # 指定目标用户
78
+ bash install.sh -y --user bell
79
+
80
+ # 不修改默认登录 Shell
81
+ bash install.sh -y --no-chsh
82
+
83
+ # 仅删除 easy-command 管理的 .zshrc 配置区块
84
+ bash install.sh --uninstall
85
+
86
+ # 启用可选的目录跳转、模糊搜索和 Git 快捷操作
87
+ bash install.sh -y --with-zoxide --with-fzf --with-git-aliases
88
+ ```
89
+
90
+ `zoxide` 提供 `z <关键词>` 目录跳转;`fzf` 提供历史命令和文件的模糊搜索;Git 快捷操作使用 `ec-` 前缀,例如 `git ec-status`,不会覆盖已有别名。
91
+
92
+ ## 系统要求
93
+
94
+ - macOS:需预先安装 Homebrew。
95
+ - Ubuntu / Debian:脚本会使用 `apt-get` 安装依赖。
96
+
97
+ 脚本需要 `sudo` 权限安装软件包和切换默认 Shell。没有 sudo 权限时,请使用 `--no-chsh`,并由管理员预先安装 Zsh。
98
+
99
+ ## 配置与回退
100
+
101
+ 安装器只管理 `~/.zshrc` 中带有 `easy-command` 标记的配置区块。每次更新前会创建带时间戳的备份。执行 `--uninstall` 仅删除该托管区块,不会删除 Oh My Zsh、插件、命令历史或其他个人配置。
102
+
103
+ 安装内容:
104
+
105
+ ```text
106
+ ~/.easy-command/oh-my-zsh
107
+ ~/.zshrc
108
+ ~/.zsh_history
109
+ ```
110
+
111
+ ## 安全说明
112
+
113
+ 首次使用时,建议先下载并审阅脚本:
114
+
115
+ ```bash
116
+ curl -fsSLO https://raw.githubusercontent.com/UnionSchool/easy-command/main/install.sh
117
+ less install.sh
118
+ bash install.sh -y
119
+ ```
120
+
121
+ 生产环境建议固定到发布标签,而不是直接使用 `main`:
122
+
123
+ ```bash
124
+ npx easy-command@2.0.2 -y
125
+ ```
126
+
127
+ 或者不使用 npm:
128
+
129
+ ```bash
130
+ curl -fsSL https://raw.githubusercontent.com/UnionSchool/easy-command/v2.0.2/install.sh | bash -s -- -y
131
+ ```
132
+
133
+ ## 许可
134
+
135
+ [MIT](LICENSE)
Binary file
Binary file
Binary file
package/install.sh CHANGED
@@ -2,59 +2,68 @@
2
2
 
3
3
  set -Eeuo pipefail
4
4
 
5
- readonly VERSION='1.0.4'
5
+ readonly VERSION='2.0.2'
6
6
  readonly BEGIN_MARKER='# >>> easy-command zsh >>>'
7
7
  readonly END_MARKER='# <<< easy-command zsh <<<'
8
+ readonly BASE_DIR_NAME='.easy-command'
8
9
 
10
+ ACTION='install'
9
11
  ASSUME_YES=false
10
12
  CHANGE_LOGIN_SHELL=true
11
- UNINSTALL=false
13
+ DRY_RUN=false
14
+ ENABLE_ZOXIDE='auto'
15
+ ENABLE_FZF='auto'
16
+ ENABLE_GIT_ALIASES='auto'
12
17
  TARGET_USER="${SUDO_USER:-${USER}}"
18
+ TARGET_HOME=''
13
19
 
14
20
  info() { printf '\033[1;32m[easy-command]\033[0m %s\n' "$*"; }
15
21
  warn() { printf '\033[1;33m[easy-command]\033[0m %s\n' "$*" >&2; }
16
22
  fail() { printf '\033[1;31m[easy-command]\033[0m %s\n' "$*" >&2; exit 1; }
23
+ ok() { printf '\033[1;32m[ok]\033[0m %s\n' "$*"; }
24
+ check_warning() { printf '\033[1;33m[warn]\033[0m %s\n' "$*"; }
25
+ check_error() { printf '\033[1;31m[error]\033[0m %s\n' "$*"; }
17
26
 
18
27
  usage() {
19
28
  cat <<'EOF'
20
- Usage: install.sh [options]
29
+ Usage: easy-command [command] [options]
30
+
31
+ Commands:
32
+ install Install and configure easy-command (default).
33
+ doctor Check the Shell, dependencies, repositories and .zshrc block.
34
+ repair Restore missing dependencies, repositories and managed configuration.
35
+ update Safely update managed repositories and refresh managed configuration.
21
36
 
22
37
  Options:
23
- --yes, -y Run without confirmation.
24
- --user USER Configure this user. Defaults to the invoking user.
25
- --no-chsh Do not change the user's default login shell to zsh.
26
- --uninstall Remove only the easy-command managed .zshrc block.
27
- --help, -h Show this help.
38
+ --yes, -y Run without confirmation.
39
+ --user USER Configure this user. Defaults to the invoking user.
40
+ --no-chsh Do not change the user's default login shell to zsh.
41
+ --dry-run Preview changes without installing, writing files, or using sudo.
42
+ --with-zoxide Enable optional zoxide directory jumping.
43
+ --without-zoxide Disable zoxide in the managed configuration.
44
+ --with-fzf Enable optional fzf history and file searching.
45
+ --without-fzf Disable fzf in the managed configuration.
46
+ --with-git-aliases Add non-conflicting global Git aliases prefixed with ec-.
47
+ --without-git-aliases
48
+ Remove global Git aliases managed by easy-command.
49
+ --uninstall Remove only the easy-command managed .zshrc block.
50
+ --help, -h Show this help.
28
51
  EOF
29
52
  }
30
53
 
31
- while (($#)); do
32
- case "$1" in
33
- --yes|-y) ASSUME_YES=true ;;
34
- --user)
35
- shift
36
- (($#)) || fail '--user requires a value.'
37
- TARGET_USER="$1"
38
- ;;
39
- --no-chsh) CHANGE_LOGIN_SHELL=false ;;
40
- --uninstall) UNINSTALL=true ;;
41
- --help|-h)
42
- usage
43
- exit 0
44
- ;;
45
- *) fail "Unknown option: $1" ;;
46
- esac
47
- shift
48
- done
49
-
50
- TARGET_HOME="$(getent passwd "$TARGET_USER" | cut -d: -f6 2>/dev/null || true)"
51
- if [[ -z "$TARGET_HOME" && "$(uname -s)" == 'Darwin' ]]; then
52
- TARGET_HOME="$(dscl . -read "/Users/$TARGET_USER" NFSHomeDirectory | awk '{print $2}')"
53
- fi
54
- [[ -n "$TARGET_HOME" && -d "$TARGET_HOME" ]] || fail "Cannot determine home directory for $TARGET_USER."
54
+ command_preview() {
55
+ local output=''
56
+ local part
57
+ for part in "$@"; do
58
+ output+="$(printf '%q' "$part") "
59
+ done
60
+ info "[dry-run] ${output% }"
61
+ }
55
62
 
56
63
  run_as_root() {
57
- if [[ "$EUID" -eq 0 ]]; then
64
+ if "$DRY_RUN"; then
65
+ command_preview sudo "$@"
66
+ elif [[ "$EUID" -eq 0 ]]; then
58
67
  "$@"
59
68
  elif command -v sudo >/dev/null 2>&1; then
60
69
  sudo "$@"
@@ -64,64 +73,197 @@ run_as_root() {
64
73
  }
65
74
 
66
75
  run_as_user() {
67
- if [[ "$EUID" -eq 0 && "$TARGET_USER" != 'root' ]]; then
68
- runuser -u "$TARGET_USER" -- "$@"
76
+ if "$DRY_RUN"; then
77
+ if [[ "$EUID" -eq 0 && "$TARGET_USER" != 'root' ]]; then
78
+ if command -v runuser >/dev/null 2>&1; then
79
+ command_preview runuser -u "$TARGET_USER" -- "$@"
80
+ else
81
+ command_preview sudo -u "$TARGET_USER" -- "$@"
82
+ fi
83
+ else
84
+ command_preview "$@"
85
+ fi
86
+ elif [[ "$EUID" -eq 0 && "$TARGET_USER" != 'root' ]]; then
87
+ if command -v runuser >/dev/null 2>&1; then
88
+ runuser -u "$TARGET_USER" -- "$@"
89
+ elif command -v sudo >/dev/null 2>&1; then
90
+ sudo -u "$TARGET_USER" -- "$@"
91
+ else
92
+ fail 'runuser or sudo is required to run commands as the target user.'
93
+ fi
69
94
  else
70
95
  "$@"
71
96
  fi
72
97
  }
73
98
 
99
+ write_file() {
100
+ local source_file="$1"
101
+ local destination_file="$2"
102
+
103
+ if "$DRY_RUN"; then
104
+ command_preview cp "$source_file" "$destination_file"
105
+ return
106
+ fi
107
+
108
+ cp "$source_file" "$destination_file"
109
+ chown "$TARGET_USER" "$destination_file" 2>/dev/null || true
110
+ }
111
+
112
+ determine_target_home() {
113
+ if command -v getent >/dev/null 2>&1; then
114
+ TARGET_HOME="$(getent passwd "$TARGET_USER" | cut -d: -f6 2>/dev/null || true)"
115
+ fi
116
+ if [[ -z "$TARGET_HOME" && "$(uname -s)" == 'Darwin' ]]; then
117
+ TARGET_HOME="$(dscl . -read "/Users/$TARGET_USER" NFSHomeDirectory | awk '{print $2}')"
118
+ fi
119
+ [[ -n "$TARGET_HOME" && -d "$TARGET_HOME" ]] || fail "Cannot determine home directory for $TARGET_USER."
120
+ }
121
+
74
122
  backup_file() {
75
123
  local file="$1"
76
124
  [[ -f "$file" ]] || return 0
77
125
 
78
126
  local backup="${file}.easy-command-backup-$(date +%Y%m%d%H%M%S)"
79
- cp "$file" "$backup"
127
+ local sequence=1
128
+ while [[ -e "$backup" ]]; do
129
+ backup="${file}.easy-command-backup-$(date +%Y%m%d%H%M%S)-${sequence}"
130
+ sequence=$((sequence + 1))
131
+ done
132
+ if "$DRY_RUN"; then
133
+ command_preview cp "$file" "$backup"
134
+ else
135
+ cp "$file" "$backup"
136
+ chown "$TARGET_USER" "$backup" 2>/dev/null || true
137
+ fi
80
138
  info "Backed up $file to $backup"
81
139
  }
82
140
 
83
- remove_managed_block() {
141
+ managed_block_is_valid() {
84
142
  local zshrc="$TARGET_HOME/.zshrc"
85
143
  [[ -f "$zshrc" ]] || return 0
86
144
 
87
- local temp_file
88
- temp_file="$(mktemp)"
89
- awk -v begin="$BEGIN_MARKER" -v end="$END_MARKER" '
90
- $0 == begin { skipping = 1; next }
91
- $0 == end { skipping = 0; next }
92
- !skipping { print }
93
- ' "$zshrc" > "$temp_file"
94
- cat "$temp_file" > "$zshrc"
95
- rm -f "$temp_file"
96
- chown "$TARGET_USER" "$zshrc" 2>/dev/null || true
145
+ local begin_count end_count
146
+ begin_count="$(grep -Fxc "$BEGIN_MARKER" "$zshrc" || true)"
147
+ end_count="$(grep -Fxc "$END_MARKER" "$zshrc" || true)"
148
+ [[ "$begin_count" == '0' && "$end_count" == '0' ]] || [[ "$begin_count" == '1' && "$end_count" == '1' ]]
149
+ }
150
+
151
+ managed_option_enabled() {
152
+ local option="$1"
153
+ local zshrc="$TARGET_HOME/.zshrc"
154
+
155
+ [[ -f "$zshrc" ]] && grep -Fqx "# easy-command: ${option}" "$zshrc"
156
+ }
157
+
158
+ resolve_option() {
159
+ local requested="$1"
160
+ local marker="$2"
161
+
162
+ if [[ "$requested" == 'auto' ]]; then
163
+ if managed_option_enabled "$marker"; then
164
+ printf 'true'
165
+ else
166
+ printf 'false'
167
+ fi
168
+ else
169
+ printf '%s' "$requested"
170
+ fi
97
171
  }
98
172
 
99
173
  install_packages() {
174
+ local packages=(zsh git)
175
+ [[ "$(resolve_option "$ENABLE_ZOXIDE" 'zoxide')" == 'true' ]] && packages+=(zoxide)
176
+ [[ "$(resolve_option "$ENABLE_FZF" 'fzf')" == 'true' ]] && packages+=(fzf)
177
+
100
178
  case "$(uname -s)" in
101
179
  Darwin)
102
180
  command -v brew >/dev/null 2>&1 || fail 'Homebrew is required on macOS: https://brew.sh'
103
- brew install zsh git
181
+ run_as_user brew install "${packages[@]}"
104
182
  ;;
105
183
  Linux)
106
184
  command -v apt-get >/dev/null 2>&1 || fail 'Only Ubuntu/Debian (apt-get) is supported on Linux.'
107
185
  run_as_root apt-get update
108
- run_as_root env DEBIAN_FRONTEND=noninteractive apt-get install -y zsh git
186
+ run_as_root env DEBIAN_FRONTEND=noninteractive apt-get install -y "${packages[@]}"
109
187
  ;;
110
188
  *) fail "Unsupported operating system: $(uname -s)" ;;
111
189
  esac
112
190
  }
113
191
 
114
- install_repository() {
192
+ ensure_repository() {
115
193
  local repository="$1"
116
194
  local destination="$2"
117
- [[ -d "$destination/.git" ]] && return 0
118
195
 
119
- mkdir -p "$(dirname "$destination")"
196
+ if [[ -d "$destination/.git" ]]; then
197
+ return 0
198
+ fi
199
+ if [[ -e "$destination" ]]; then
200
+ fail "Expected Git repository at $destination, but the path is not a repository. Run repair after moving the path aside."
201
+ fi
202
+
203
+ if "$DRY_RUN"; then
204
+ command_preview mkdir -p "$(dirname "$destination")"
205
+ else
206
+ mkdir -p "$(dirname "$destination")"
207
+ chown "$TARGET_USER" "$(dirname "$destination")" 2>/dev/null || true
208
+ fi
120
209
  run_as_user git clone --depth 1 "$repository" "$destination"
121
210
  }
122
211
 
212
+ update_repository() {
213
+ local destination="$1"
214
+ [[ -d "$destination/.git" ]] || return 0
215
+
216
+ if ! run_as_user git -C "$destination" diff --quiet --ignore-submodules --; then
217
+ warn "Skipped $destination because it has local changes."
218
+ return 0
219
+ fi
220
+ run_as_user git -C "$destination" pull --ff-only
221
+ }
222
+
223
+ ensure_repositories() {
224
+ local base_dir="$TARGET_HOME/$BASE_DIR_NAME"
225
+ ensure_repository 'https://github.com/ohmyzsh/ohmyzsh.git' "$base_dir/oh-my-zsh"
226
+ ensure_repository 'https://github.com/zsh-users/zsh-autosuggestions.git' "$base_dir/oh-my-zsh/custom/plugins/zsh-autosuggestions"
227
+ ensure_repository 'https://github.com/zsh-users/zsh-syntax-highlighting.git' "$base_dir/oh-my-zsh/custom/plugins/zsh-syntax-highlighting"
228
+ }
229
+
230
+ update_repositories() {
231
+ local base_dir="$TARGET_HOME/$BASE_DIR_NAME/oh-my-zsh"
232
+ update_repository "$base_dir"
233
+ update_repository "$base_dir/custom/plugins/zsh-autosuggestions"
234
+ update_repository "$base_dir/custom/plugins/zsh-syntax-highlighting"
235
+ }
236
+
237
+ remove_managed_block() {
238
+ local zshrc="$TARGET_HOME/.zshrc"
239
+ [[ -f "$zshrc" ]] || return 0
240
+ managed_block_is_valid || fail "Managed .zshrc block is incomplete or duplicated. Refusing to change $zshrc; restore a backup or repair the markers manually."
241
+
242
+ if "$DRY_RUN"; then
243
+ info "[dry-run] Would remove the managed block from $zshrc"
244
+ return 0
245
+ fi
246
+
247
+ local temp_file
248
+ temp_file="$(mktemp)"
249
+ awk -v begin="$BEGIN_MARKER" -v end="$END_MARKER" '
250
+ $0 == begin { skipping = 1; next }
251
+ $0 == end { skipping = 0; next }
252
+ !skipping { print }
253
+ ' "$zshrc" > "$temp_file"
254
+ write_file "$temp_file" "$zshrc"
255
+ rm -f "$temp_file"
256
+ }
257
+
123
258
  write_zshrc_block() {
124
259
  local zshrc="$TARGET_HOME/.zshrc"
260
+ managed_block_is_valid || fail "Managed .zshrc block is incomplete or duplicated. Refusing to change $zshrc; restore a backup or repair the markers manually."
261
+
262
+ if "$DRY_RUN"; then
263
+ info "[dry-run] Would write the managed configuration block to $zshrc"
264
+ return 0
265
+ fi
266
+
125
267
  local temp_file
126
268
  temp_file="$(mktemp)"
127
269
 
@@ -131,35 +273,92 @@ write_zshrc_block() {
131
273
  !skipping { print }
132
274
  ' "$zshrc" > "$temp_file"
133
275
 
134
- cat >> "$temp_file" <<'EOF'
276
+ cat >> "$temp_file" <<EOF
135
277
 
136
278
  # >>> easy-command zsh >>>
137
279
  # Managed by easy-command. Re-run the installer to update this block.
138
- export ZSH="$HOME/.easy-command/oh-my-zsh"
280
+ export ZSH="\$HOME/.easy-command/oh-my-zsh"
139
281
  ZSH_THEME="agnoster"
140
282
  plugins=(git zsh-autosuggestions zsh-syntax-highlighting)
141
283
 
142
- HISTFILE="$HOME/.zsh_history"
284
+ HISTFILE="\$HOME/.zsh_history"
143
285
  HISTSIZE=10000
144
286
  SAVEHIST=10000
145
287
  setopt append_history share_history hist_ignore_dups
146
288
  ZSH_AUTOSUGGEST_STRATEGY=(history completion)
147
289
  ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE='fg=8'
148
290
 
149
- source "$ZSH/oh-my-zsh.sh"
291
+ source "\$ZSH/oh-my-zsh.sh"
150
292
 
151
293
  ZSH_HIGHLIGHT_STYLES[command]='fg=green'
152
294
  ZSH_HIGHLIGHT_STYLES[builtin]='fg=green'
153
295
  ZSH_HIGHLIGHT_STYLES[function]='fg=green'
154
296
  ZSH_HIGHLIGHT_STYLES[alias]='fg=green'
155
297
  ZSH_HIGHLIGHT_STYLES[unknown-token]='fg=red,bold'
298
+ EOF
299
+
300
+ if [[ "$(resolve_option "$ENABLE_ZOXIDE" 'zoxide')" == 'true' ]]; then
301
+ cat >> "$temp_file" <<'EOF'
302
+
303
+ # easy-command: zoxide
304
+ if command -v zoxide >/dev/null 2>&1; then
305
+ eval "$(zoxide init zsh)"
306
+ fi
307
+ EOF
308
+ fi
309
+
310
+ if [[ "$(resolve_option "$ENABLE_FZF" 'fzf')" == 'true' ]]; then
311
+ cat >> "$temp_file" <<'EOF'
312
+
313
+ # easy-command: fzf
314
+ if [[ -r /usr/share/doc/fzf/examples/key-bindings.zsh ]]; then
315
+ source /usr/share/doc/fzf/examples/key-bindings.zsh
316
+ elif command -v brew >/dev/null 2>&1 && [[ -r "$(brew --prefix)/opt/fzf/shell/key-bindings.zsh" ]]; then
317
+ source "$(brew --prefix)/opt/fzf/shell/key-bindings.zsh"
318
+ fi
319
+ EOF
320
+ fi
321
+
322
+ cat >> "$temp_file" <<'EOF'
156
323
  # <<< easy-command zsh <<<
157
324
  EOF
158
325
 
159
326
  backup_file "$zshrc"
160
- cat "$temp_file" > "$zshrc"
327
+ write_file "$temp_file" "$zshrc"
161
328
  rm -f "$temp_file"
162
- chown "$TARGET_USER" "$zshrc" 2>/dev/null || true
329
+ }
330
+
331
+ configure_git_aliases() {
332
+ local enabled
333
+ enabled="$(resolve_option "$ENABLE_GIT_ALIASES" 'git-aliases')"
334
+
335
+ local aliases=(
336
+ 'ec-status=status --short --branch'
337
+ 'ec-log=log --oneline --graph --decorate -12'
338
+ 'ec-last=log -1 --stat'
339
+ )
340
+ local entry name value
341
+ for entry in "${aliases[@]}"; do
342
+ name="${entry%%=*}"
343
+ value="${entry#*=}"
344
+ if "$DRY_RUN"; then
345
+ if [[ "$enabled" == 'true' ]]; then
346
+ command_preview git config --global "alias.$name" "$value"
347
+ elif [[ "$ENABLE_GIT_ALIASES" == 'false' ]]; then
348
+ command_preview git config --global --unset "alias.$name"
349
+ fi
350
+ continue
351
+ fi
352
+ if [[ "$enabled" == 'true' ]]; then
353
+ if run_as_user git config --global --get "alias.$name" >/dev/null 2>&1; then
354
+ check_warning "Git alias $name already exists; leaving it unchanged."
355
+ else
356
+ run_as_user git config --global "alias.$name" "$value"
357
+ fi
358
+ elif [[ "$ENABLE_GIT_ALIASES" == 'false' ]]; then
359
+ run_as_user git config --global --unset "alias.$name" 2>/dev/null || true
360
+ fi
361
+ done
163
362
  }
164
363
 
165
364
  change_login_shell() {
@@ -167,34 +366,164 @@ change_login_shell() {
167
366
 
168
367
  local zsh_path
169
368
  zsh_path="$(command -v zsh)"
170
- grep -qx "$zsh_path" /etc/shells || fail "$zsh_path is not listed in /etc/shells."
369
+ if ! grep -qx "$zsh_path" /etc/shells; then
370
+ if [[ -x /bin/zsh ]] && grep -qx '/bin/zsh' /etc/shells; then
371
+ warn "$zsh_path is not listed in /etc/shells; using /bin/zsh as the login shell."
372
+ zsh_path='/bin/zsh'
373
+ else
374
+ fail "$zsh_path is not listed in /etc/shells. Use --no-chsh or add the path to /etc/shells first."
375
+ fi
376
+ fi
171
377
  run_as_root chsh -s "$zsh_path" "$TARGET_USER"
172
378
  }
173
379
 
174
- main() {
175
- if "$UNINSTALL"; then
176
- backup_file "$TARGET_HOME/.zshrc"
177
- remove_managed_block
178
- info 'Removed the easy-command managed .zshrc block.'
179
- exit 0
380
+ confirm() {
381
+ "$DRY_RUN" && return 0
382
+ "$ASSUME_YES" && return 0
383
+
384
+ local answer
385
+ read -r -p "$1 [y/N] " answer
386
+ [[ "$answer" =~ ^[Yy]$ ]] || exit 0
387
+ }
388
+
389
+ doctor() {
390
+ local errors=0
391
+ local zshrc="$TARGET_HOME/.zshrc"
392
+ local base_dir="$TARGET_HOME/$BASE_DIR_NAME/oh-my-zsh"
393
+ local configured_shell=''
394
+
395
+ info "Checking easy-command for $TARGET_USER"
396
+ if command -v getent >/dev/null 2>&1; then
397
+ configured_shell="$(getent passwd "$TARGET_USER" | cut -d: -f7 2>/dev/null || true)"
398
+ fi
399
+ if [[ -z "$configured_shell" && "$(uname -s)" == 'Darwin' ]]; then
400
+ configured_shell="$(dscl . -read "/Users/$TARGET_USER" UserShell | awk '{print $2}')"
401
+ fi
402
+ if [[ "$configured_shell" == *zsh ]]; then
403
+ ok "Login shell: $configured_shell"
404
+ else
405
+ check_warning "Login shell is ${configured_shell:-unknown}; run install without --no-chsh to switch to zsh."
180
406
  fi
181
407
 
182
- if ! "$ASSUME_YES"; then
183
- read -r -p "Install easy-command for $TARGET_USER? [y/N] " answer
184
- [[ "$answer" =~ ^[Yy]$ ]] || exit 0
408
+ local command_name
409
+ for command_name in zsh git; do
410
+ if command -v "$command_name" >/dev/null 2>&1; then
411
+ ok "Command available: $command_name"
412
+ else
413
+ check_error "Missing command: $command_name"
414
+ errors=$((errors + 1))
415
+ fi
416
+ done
417
+
418
+ local repository
419
+ for repository in "$base_dir" "$base_dir/custom/plugins/zsh-autosuggestions" "$base_dir/custom/plugins/zsh-syntax-highlighting"; do
420
+ if [[ -d "$repository/.git" ]]; then
421
+ ok "Repository available: $repository"
422
+ else
423
+ check_error "Missing repository: $repository"
424
+ errors=$((errors + 1))
425
+ fi
426
+ done
427
+
428
+ local begin_count=0 end_count=0
429
+ if [[ -f "$zshrc" ]]; then
430
+ begin_count="$(grep -Fxc "$BEGIN_MARKER" "$zshrc" || true)"
431
+ end_count="$(grep -Fxc "$END_MARKER" "$zshrc" || true)"
432
+ fi
433
+ if [[ "$begin_count" == '1' && "$end_count" == '1' ]]; then
434
+ ok 'Managed .zshrc block is complete.'
435
+ else
436
+ check_error "Managed .zshrc block is invalid (begin: $begin_count, end: $end_count). Restore a backup or repair the markers manually."
437
+ errors=$((errors + 1))
185
438
  fi
186
439
 
187
- install_packages
440
+ if command -v zsh >/dev/null 2>&1 && [[ -f "$zshrc" ]]; then
441
+ if zsh -n "$zshrc"; then
442
+ ok '.zshrc syntax check passed.'
443
+ else
444
+ check_error '.zshrc syntax check failed.'
445
+ errors=$((errors + 1))
446
+ fi
447
+ fi
188
448
 
189
- local base_dir="$TARGET_HOME/.easy-command"
190
- install_repository 'https://github.com/ohmyzsh/ohmyzsh.git' "$base_dir/oh-my-zsh"
191
- install_repository 'https://github.com/zsh-users/zsh-autosuggestions.git' "$base_dir/oh-my-zsh/custom/plugins/zsh-autosuggestions"
192
- install_repository 'https://github.com/zsh-users/zsh-syntax-highlighting.git' "$base_dir/oh-my-zsh/custom/plugins/zsh-syntax-highlighting"
449
+ if ((errors > 0)); then
450
+ return 1
451
+ fi
452
+ }
193
453
 
194
- write_zshrc_block
195
- change_login_shell
454
+ parse_args() {
455
+ while (($#)); do
456
+ case "$1" in
457
+ install|doctor|repair|update)
458
+ ACTION="$1"
459
+ ;;
460
+ --yes|-y) ASSUME_YES=true ;;
461
+ --user)
462
+ shift
463
+ (($#)) || fail '--user requires a value.'
464
+ TARGET_USER="$1"
465
+ ;;
466
+ --no-chsh) CHANGE_LOGIN_SHELL=false ;;
467
+ --dry-run) DRY_RUN=true ;;
468
+ --with-zoxide) ENABLE_ZOXIDE=true ;;
469
+ --without-zoxide) ENABLE_ZOXIDE=false ;;
470
+ --with-fzf) ENABLE_FZF=true ;;
471
+ --without-fzf) ENABLE_FZF=false ;;
472
+ --with-git-aliases) ENABLE_GIT_ALIASES=true ;;
473
+ --without-git-aliases) ENABLE_GIT_ALIASES=false ;;
474
+ --uninstall) ACTION='uninstall' ;;
475
+ --help|-h)
476
+ usage
477
+ exit 0
478
+ ;;
479
+ *) fail "Unknown option or command: $1" ;;
480
+ esac
481
+ shift
482
+ done
483
+ }
484
+
485
+ main() {
486
+ parse_args "$@"
487
+ determine_target_home
196
488
 
197
- info "Installed easy-command $VERSION for $TARGET_USER. Reconnect or run: exec zsh -l"
489
+ case "$ACTION" in
490
+ doctor)
491
+ doctor
492
+ ;;
493
+ uninstall)
494
+ confirm "Remove the easy-command managed configuration for $TARGET_USER?"
495
+ backup_file "$TARGET_HOME/.zshrc"
496
+ remove_managed_block
497
+ info 'Removed the easy-command managed .zshrc block.'
498
+ ;;
499
+ install)
500
+ confirm "Install easy-command $VERSION for $TARGET_USER?"
501
+ install_packages
502
+ ensure_repositories
503
+ change_login_shell
504
+ write_zshrc_block
505
+ configure_git_aliases
506
+ info "Installed easy-command $VERSION for $TARGET_USER. Reconnect or run: exec zsh -l"
507
+ ;;
508
+ repair)
509
+ CHANGE_LOGIN_SHELL=false
510
+ confirm "Repair easy-command for $TARGET_USER?"
511
+ install_packages
512
+ ensure_repositories
513
+ write_zshrc_block
514
+ configure_git_aliases
515
+ info "Repaired easy-command for $TARGET_USER. Reconnect or run: exec zsh -l"
516
+ ;;
517
+ update)
518
+ CHANGE_LOGIN_SHELL=false
519
+ confirm "Update easy-command repositories for $TARGET_USER?"
520
+ ensure_repositories
521
+ update_repositories
522
+ write_zshrc_block
523
+ configure_git_aliases
524
+ info "Updated easy-command for $TARGET_USER. Reconnect or run: exec zsh -l"
525
+ ;;
526
+ esac
198
527
  }
199
528
 
200
- main
529
+ main "$@"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "easy-command",
3
- "version": "1.0.4",
3
+ "version": "2.0.2",
4
4
  "description": "Configure a polished Zsh terminal with one command.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -8,8 +8,10 @@
8
8
  },
9
9
  "files": [
10
10
  "bin",
11
+ "assets",
11
12
  "install.sh",
12
13
  "README.md",
14
+ "README_CN.md",
13
15
  "LICENSE"
14
16
  ],
15
17
  "repository": {