claude-pro-minmax 1.0.2 → 1.2.0

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/install.sh CHANGED
@@ -1,146 +1,280 @@
1
1
  #!/bin/bash
2
2
  # Claude Pro MinMax - Installation Script
3
3
  set -e
4
- echo "🚀 Installing Claude Pro MinMax (CPMM)"
5
4
 
6
- # Check for dependencies
7
- if ! command -v jq &> /dev/null; then
8
- echo "⚠️ Warning: jq is not installed. Some features (JSON output, cost optimization) may not work."
9
- echo " Install with: brew install jq"
5
+ # Error handler: print failure line and exit message
6
+ _on_error() { echo ""; echo "❌ Installation failed at line $1. Check output above."; }
7
+ trap '_on_error $LINENO' ERR
8
+
9
+ # Guard unsupported invocations (`curl | bash`, process substitution)
10
+ if [[ -z "${BASH_SOURCE[0]}" || "${BASH_SOURCE[0]}" == "bash" ]]; then
11
+ echo "❌ This script cannot be run via 'curl | bash'."
12
+ echo " Please clone the repository first:"
13
+ echo " git clone https://github.com/move-hoon/claude-pro-minmax.git && cd claude-pro-minmax && bash install.sh"
14
+ exit 1
15
+ fi
16
+ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
17
+ if [ ! -f "$SCRIPT_DIR/.claude/CLAUDE.md" ] || [ ! -f "$SCRIPT_DIR/package.json" ]; then
18
+ echo "❌ This script must be run from a cloned CPMM repository directory."
19
+ echo " Please run:"
20
+ echo " git clone https://github.com/move-hoon/claude-pro-minmax.git && cd claude-pro-minmax && bash install.sh"
21
+ exit 1
10
22
  fi
11
23
 
12
- # Check for mgrep (Critical for output reduction)
13
- if ! command -v mgrep &> /dev/null; then
14
- echo "⚠️ Warning: mgrep is not installed. This is critical for 50% output reduction."
15
- echo " Install with: npm install -g @mixedbread/mgrep && mgrep install-claude-code"
24
+ # Marker-based detection for Install vs Update
25
+ CPMM_MARKER="$HOME/.claude/.cpmm-version"
26
+ IS_UPDATE=false
27
+ if [ -f "$CPMM_MARKER" ]; then IS_UPDATE=true; fi
28
+
29
+ # Legacy CPMM detection (marker was introduced after early releases)
30
+ LEGACY_CPMM=false
31
+ if [ "$IS_UPDATE" = false ] && [ -d "$HOME/.claude" ]; then
32
+ if [ -f "$HOME/.claude/CLAUDE.md" ] &&
33
+ [ -f "$HOME/.claude/settings.json" ] &&
34
+ [ -f "$HOME/.claude/commands/do.md" ] &&
35
+ [ -f "$HOME/.claude/commands/do-opus.md" ]; then
36
+ LEGACY_CPMM=true
37
+ fi
16
38
  fi
17
39
 
18
- # Backup and Clean existing ~/.claude (True Overwrite)
19
- if [ -d ~/.claude ]; then
20
- BACKUP_DIR=~/.claude-backup-$(date +"%Y%m%d-%H%M%S")
21
- echo "📦 Backing up and clearing existing ~/.claude → $BACKUP_DIR"
22
- mv ~/.claude "$BACKUP_DIR"
23
- echo " Clean overwrite enabled. Old settings preserved in backup."
40
+ # Header message: show Install vs Update mode
41
+ if [ "$IS_UPDATE" = true ]; then
42
+ echo "🔄 Updating Claude Pro MinMax (CPMM)"
43
+ INSTALLED_VERSION=$(head -1 "$CPMM_MARKER" 2>/dev/null || echo "unknown")
44
+ echo " Current version: $INSTALLED_VERSION"
45
+ else
46
+ echo "🚀 Installing Claude Pro MinMax (CPMM)"
24
47
  fi
25
48
 
26
- # Create fresh directories
27
- mkdir -p ~/.claude/{agents,commands,rules,skills/cli-wrappers/references,contexts,sessions,scripts}
49
+ # Backup existing ~/.claude (Fresh Install Only)
50
+ if [ "$IS_UPDATE" = false ] && [ -d "$HOME/.claude" ]; then
51
+ if [ -d "$HOME/.claude.pre-cpmm" ]; then
52
+ echo "⚠️ ~/.claude.pre-cpmm already exists, skipping backup."
53
+ echo " User data preserved. Reinstalling CPMM files in-place..."
54
+ # No rm -rf — user data (learned/, plans/, projects/, sessions/) is safe
55
+ elif [ "$LEGACY_CPMM" = true ]; then
56
+ echo "⚠️ Detected legacy CPMM installation without marker, skipping backup."
57
+ echo " User data preserved. Reinstalling CPMM files in-place..."
58
+ # No rm -rf — user data (learned/, plans/, projects/, sessions/) is safe
59
+ else
60
+ mv "$HOME/.claude" "$HOME/.claude.pre-cpmm"
61
+ echo "📦 Backed up ~/.claude → ~/.claude.pre-cpmm"
62
+ fi
63
+ fi
28
64
 
29
- # Copy configurations
30
- SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
31
- cp "$SCRIPT_DIR/.claude/CLAUDE.md" ~/.claude/
32
- cp "$SCRIPT_DIR/.claude/settings.json" ~/.claude/
33
- # Copy settings.local.json from example template (users customize after install)
34
- if [ -f "$SCRIPT_DIR/.claude/settings.local.example.json" ]; then
35
- cp "$SCRIPT_DIR/.claude/settings.local.example.json" ~/.claude/settings.local.json
36
- fi
37
- cp "$SCRIPT_DIR/.claude/agents/"*.md ~/.claude/agents/
38
- cp "$SCRIPT_DIR/.claude/commands/"*.md ~/.claude/commands/
65
+ # Create required directories (user-owned dirs are never rm-rf'd)
66
+ # NITPICK #11: only create dirs not managed by rm-rf below
67
+ mkdir -p "$HOME/.claude/rules" "$HOME/.claude/skills/learned"
68
+ mkdir -p "$HOME/.claude/"{sessions,plans,projects}
69
+
70
+ # Copy core configurations
71
+ cp "$SCRIPT_DIR/.claude/CLAUDE.md" "$HOME/.claude/"
72
+ cp "$SCRIPT_DIR/.claude/settings.json" "$HOME/.claude/"
73
+ # Copy settings.local.json from example template (only if not already present)
74
+ if [ ! -f "$HOME/.claude/settings.local.json" ] && [ -f "$SCRIPT_DIR/.claude/settings.local.example.json" ]; then
75
+ cp "$SCRIPT_DIR/.claude/settings.local.example.json" "$HOME/.claude/settings.local.json"
76
+ fi
77
+
78
+ shopt -s nullglob
79
+
80
+ # agents/, commands/, contexts/ — fully CPMM managed: rm-rf to remove stale files
81
+ # EDGE CASE #6: these dirs are CPMM-only; user files should not be placed here
82
+ rm -rf "$HOME/.claude/agents" && mkdir -p "$HOME/.claude/agents"
83
+ _agent_files=("$SCRIPT_DIR/.claude/agents/"*.md)
84
+ if [ ${#_agent_files[@]} -gt 0 ]; then
85
+ for agent in "${_agent_files[@]}"; do
86
+ filename=$(basename "$agent")
87
+ [[ "$filename" == README* ]] && continue
88
+ [[ "$filename" == USER-MANUAL* ]] && continue
89
+ cp "$agent" "$HOME/.claude/agents/"
90
+ done
91
+ fi
92
+
93
+ rm -rf "$HOME/.claude/commands" && mkdir -p "$HOME/.claude/commands"
94
+ _cmd_files=("$SCRIPT_DIR/.claude/commands/"*.md)
95
+ if [ ${#_cmd_files[@]} -gt 0 ]; then
96
+ for cmd in "${_cmd_files[@]}"; do
97
+ filename=$(basename "$cmd")
98
+ [[ "$filename" == README* ]] && continue
99
+ [[ "$filename" == USER-MANUAL* ]] && continue
100
+ cp "$cmd" "$HOME/.claude/commands/"
101
+ done
102
+ fi
103
+
104
+ rm -rf "$HOME/.claude/contexts" && mkdir -p "$HOME/.claude/contexts"
105
+ _ctx_files=("$SCRIPT_DIR/.claude/contexts/"*.md)
106
+ if [ ${#_ctx_files[@]} -gt 0 ]; then
107
+ for ctx in "${_ctx_files[@]}"; do
108
+ filename=$(basename "$ctx")
109
+ [[ "$filename" == README* ]] && continue
110
+ [[ "$filename" == USER-MANUAL* ]] && continue
111
+ cp "$ctx" "$HOME/.claude/contexts/"
112
+ done
113
+ fi
114
+
115
+ # rules/ — language.md is user-owned: remove stale CPMM rules, preserve language.md
116
+ # BUG #2: rules loop is now inside nullglob block to prevent literal glob expansion
117
+ find "$HOME/.claude/rules/" -name "*.md" ! -name "language.md" -delete 2>/dev/null || true
39
118
  for rule in "$SCRIPT_DIR/.claude/rules/"*.md; do
119
+ [[ -f "$rule" ]] || continue # nullglob guard (no-op with nullglob, safety net)
40
120
  filename=$(basename "$rule")
41
121
  [ "$filename" = "language.md" ] && continue
42
- cp "$rule" ~/.claude/rules/
122
+ [[ "$filename" == README* ]] && continue
123
+ [[ "$filename" == USER-MANUAL* ]] && continue
124
+ cp "$rule" "$HOME/.claude/rules/"
43
125
  done
44
- cp -R "$SCRIPT_DIR/.claude/skills/"* ~/.claude/skills/
45
- cp "$SCRIPT_DIR/.claude/contexts/"*.md ~/.claude/contexts/
46
- # sessions/ dir created above; example files stay in repo only (not installed)
47
- cp -R "$SCRIPT_DIR/scripts/"* ~/.claude/scripts/
48
126
 
49
- # Clean up documentation files from ~/.claude to prevent parsing errors
50
- # (The 'find' command below removes all README/USER-MANUAL files from the installed directory)
51
- find ~/.claude -name "README*" -delete
52
- find ~/.claude -name "USER-MANUAL*" -delete
127
+ shopt -u nullglob
128
+
129
+ # cli-wrappers/ and scripts/ — fully CPMM managed: rm-rf to remove stale files
130
+ if [ -d "$SCRIPT_DIR/.claude/skills/cli-wrappers" ]; then
131
+ rm -rf "$HOME/.claude/skills/cli-wrappers"
132
+ cp -R "$SCRIPT_DIR/.claude/skills/cli-wrappers" "$HOME/.claude/skills/"
133
+ fi
134
+ # sessions/ dir created above; example files stay in repo only (not installed)
135
+ if [ -d "$SCRIPT_DIR/scripts" ]; then
136
+ rm -rf "$HOME/.claude/scripts"
137
+ cp -R "$SCRIPT_DIR/scripts" "$HOME/.claude/scripts"
138
+ fi
53
139
 
54
- # Copy MCP Configuration (User Scope - ~/.claude.json)
140
+ # Copy MCP Configuration
141
+ MCP_CONFIG_PRESENT=false
55
142
  if [ -f "$SCRIPT_DIR/.claude.json" ]; then
56
- if [ -f ~/.claude.json ]; then
57
- echo "📦 Backing up existing ~/.claude.json → ~/.claude.json.bak"
58
- cp ~/.claude.json ~/.claude.json.bak
143
+ MCP_CONFIG_PRESENT=true
144
+
145
+ if [ "$IS_UPDATE" = false ]; then
146
+ if [ -f "$HOME/.claude.json" ]; then
147
+ echo "📦 Backing up existing ~/.claude.json → ~/.claude.json.bak"
148
+ cp "$HOME/.claude.json" "$HOME/.claude.json.bak"
59
149
  fi
60
- cp "$SCRIPT_DIR/.claude.json" ~/.claude.json
150
+ cp "$SCRIPT_DIR/.claude.json" "$HOME/.claude.json"
61
151
  echo "✅ Installed .claude.json to ~/.claude.json (User Scope)"
152
+ elif [ ! -f "$HOME/.claude.json" ]; then
153
+ cp "$SCRIPT_DIR/.claude.json" "$HOME/.claude.json"
154
+ echo "✅ Restored missing ~/.claude.json from repository template"
155
+ fi
62
156
 
63
- # Create .mcp.json symlink (Force recreate to ensure it's a link)
64
- if [ -e ~/.mcp.json ] || [ -L ~/.mcp.json ]; then
65
- rm ~/.mcp.json
66
- fi
67
- ln -s ~/.claude.json ~/.mcp.json
68
- echo "✅ Created .mcp.json → .claude.json symlink (Ensured Link)"
69
- # Interactive Perplexity Setup (Read from /dev/tty for curl support)
70
- if [ -t 0 ] || [ -c /dev/tty ]; then
71
- if ! command -v jq &> /dev/null; then
72
- echo "⚠️ Skipping Perplexity setup (jq not installed). Install jq and re-run to configure."
73
- else
74
- echo ""
75
- echo "🔍 Perplexity API Setup (Recommended for /dplan)"
76
- echo -n " Enter your API Key (Press Enter to skip): "
77
- read -rs PERPLEXITY_KEY < /dev/tty || PERPLEXITY_KEY=""
78
- echo "" # Newline for silent read
79
-
80
- if [ -n "$PERPLEXITY_KEY" ]; then
81
- # Enable Perplexity (Rename key and inject API Key)
82
- jq --arg key "$PERPLEXITY_KEY" \
83
- '.mcpServers.perplexity = .mcpServers._perplexity_disabled_by_default |
84
- .mcpServers.perplexity.env.PERPLEXITY_API_KEY = $key |
85
- del(.mcpServers._perplexity_disabled_by_default)' \
86
- ~/.claude.json > ~/.claude.json.tmp && mv ~/.claude.json.tmp ~/.claude.json
87
- echo "✅ Perplexity API Key configured!"
88
- else
89
- # Skip: Completely remove the disabled block to keep config clean
90
- echo "⚠️ Skipping Perplexity setup. Disabling feature..."
91
- jq 'del(.mcpServers._perplexity_disabled_by_default)' ~/.claude.json > ~/.claude.json.tmp && mv ~/.claude.json.tmp ~/.claude.json
92
- echo " (Feature removed from config. Add manually to functionality if needed)"
93
- fi
94
- fi
157
+ # Fresh install: enforce symlink. Update: restore only when missing (do not overwrite existing file/link).
158
+ if [ "$IS_UPDATE" = false ]; then
159
+ if [ ! -L "$HOME/.mcp.json" ] || [ "$(readlink "$HOME/.mcp.json" 2>/dev/null)" != "$HOME/.claude.json" ]; then
160
+ ln -sf "$HOME/.claude.json" "$HOME/.mcp.json"
161
+ echo "✅ Ensured .mcp.json .claude.json symlink"
95
162
  fi
163
+ elif [ ! -e "$HOME/.mcp.json" ] && [ ! -L "$HOME/.mcp.json" ]; then
164
+ ln -s "$HOME/.claude.json" "$HOME/.mcp.json"
165
+ echo "✅ Restored missing .mcp.json → .claude.json symlink"
166
+ fi
96
167
  fi
97
168
 
98
- # Language Selection (Interactive)
99
- if [ -t 0 ] || [ -c /dev/tty ]; then
100
- echo ""
101
- echo "🌍 Output Language"
102
- echo " 1) English (default)"
103
- echo " 2) 한국어 (Korean)"
104
- echo " 3) 日本語 (Japanese)"
105
- echo " 4) 中文 (Chinese)"
106
- echo -n " Select [1-4]: "
107
- read -r LANG_CHOICE < /dev/tty || LANG_CHOICE="1"
108
-
109
- case $LANG_CHOICE in
110
- 2)
111
- cat > ~/.claude/rules/language.md <<'LANGEOF'
169
+ # Perplexity setup + language selection (Fresh Install Only)
170
+ if [ "$IS_UPDATE" = false ]; then
171
+ # EDGE CASE #8: check stdin is truly a terminal (not /dev/tty char-device trick)
172
+ # [ -c /dev/tty ] is always true on Linux/macOS — use [ -t 0 ] alone
173
+ if [ "$MCP_CONFIG_PRESENT" = true ] && [ -t 0 ]; then
174
+ if ! command -v jq &> /dev/null; then
175
+ echo "⚠️ Skipping Perplexity setup (jq not installed). Install jq and re-run to configure."
176
+ else
177
+ echo ""
178
+ echo "🔍 Perplexity API Setup (Recommended for /dplan)"
179
+ echo -n " Enter your API Key (Press Enter to skip): "
180
+ read -rs PERPLEXITY_KEY < /dev/tty || PERPLEXITY_KEY=""
181
+ echo "" # Newline for silent read
182
+
183
+ if [ -n "$PERPLEXITY_KEY" ]; then
184
+ # Enable Perplexity (use env var to avoid key exposure in process list)
185
+ # EDGE CASE #10: write to tmp then atomically rename; trap cleans up on failure
186
+ PERPLEXITY_API_KEY="$PERPLEXITY_KEY" jq \
187
+ '.mcpServers.perplexity = .mcpServers._perplexity_disabled_by_default |
188
+ .mcpServers.perplexity.env.PERPLEXITY_API_KEY = env.PERPLEXITY_API_KEY |
189
+ del(.mcpServers._perplexity_disabled_by_default)' \
190
+ "$HOME/.claude.json" > "$HOME/.claude.json.tmp"
191
+ mv "$HOME/.claude.json.tmp" "$HOME/.claude.json"
192
+ # BUG #5: unset both variables
193
+ unset PERPLEXITY_KEY PERPLEXITY_API_KEY
194
+ echo "✅ Perplexity API Key configured!"
195
+ else
196
+ # Skip: Completely remove the disabled block to keep config clean
197
+ echo "⚠️ Skipping Perplexity setup. Disabling feature..."
198
+ jq 'del(.mcpServers._perplexity_disabled_by_default)' \
199
+ "$HOME/.claude.json" > "$HOME/.claude.json.tmp"
200
+ mv "$HOME/.claude.json.tmp" "$HOME/.claude.json"
201
+ echo " (Feature removed from config. Add manually to functionality if needed)"
202
+ fi
203
+ fi
204
+ fi
205
+
206
+ # Language Selection (Interactive - Fresh Install Only)
207
+ if [ -t 0 ]; then
208
+ echo ""
209
+ echo "🌍 Output Language"
210
+ echo " 1) English (default)"
211
+ echo " 2) 한국어 (Korean)"
212
+ echo " 3) 日本語 (Japanese)"
213
+ echo " 4) 中文 (Chinese)"
214
+ echo -n " Select [1-4]: "
215
+ read -r LANG_CHOICE < /dev/tty || LANG_CHOICE="1"
216
+
217
+ case $LANG_CHOICE in
218
+ 2)
219
+ cat > "$HOME/.claude/rules/language.md" <<'LANGEOF'
112
220
  # Language Policy
113
221
  Respond in Korean (한국어). Code, commands, technical terms in English.
114
222
  LANGEOF
115
- echo "✅ Output language: Korean"
116
- ;;
117
- 3)
118
- cat > ~/.claude/rules/language.md <<'LANGEOF'
223
+ echo "✅ Output language: Korean"
224
+ ;;
225
+ 3)
226
+ cat > "$HOME/.claude/rules/language.md" <<'LANGEOF'
119
227
  # Language Policy
120
228
  Respond in Japanese (日本語). Code, commands, technical terms in English.
121
229
  LANGEOF
122
- echo "✅ Output language: Japanese"
123
- ;;
124
- 4)
125
- cat > ~/.claude/rules/language.md <<'LANGEOF'
230
+ echo "✅ Output language: Japanese"
231
+ ;;
232
+ 4)
233
+ cat > "$HOME/.claude/rules/language.md" <<'LANGEOF'
126
234
  # Language Policy
127
235
  Respond in Chinese (中文). Code, commands, technical terms in English.
128
236
  LANGEOF
129
- echo "✅ Output language: Chinese"
130
- ;;
131
- *)
132
- # English: no language.md needed (Claude defaults to English)
133
- rm -f ~/.claude/rules/language.md
134
- echo "✅ Output language: English"
135
- ;;
136
- esac
237
+ echo "✅ Output language: Chinese"
238
+ ;;
239
+ *)
240
+ # English: no language.md needed (Claude defaults to English)
241
+ rm -f "$HOME/.claude/rules/language.md"
242
+ echo "✅ Output language: English"
243
+ ;;
244
+ esac
245
+ fi
137
246
  fi
138
247
 
248
+ # EDGE CASE #9: guard find against missing dir (scripts/ may not exist on minimal installs)
139
249
  # Make scripts executable (Recursive)
140
- find ~/.claude/scripts -name "*.sh" -exec chmod +x {} \;
141
- find ~/.claude/scripts -name "*.js" -exec chmod +x {} \;
250
+ if [ -d "$HOME/.claude/scripts" ]; then
251
+ find "$HOME/.claude/scripts" -name "*.sh" -exec chmod +x {} \;
252
+ find "$HOME/.claude/scripts" -name "*.js" -exec chmod +x {} \;
253
+ fi
254
+
255
+ # Write installation marker
256
+ # SEC #4: avoid node -p with interpolated $SCRIPT_DIR (injection risk); use node -e with argument
257
+ PROJECT_VERSION=$(node -e "try{process.stdout.write(require(process.argv[1]).version)}catch(e){process.exit(1)}" \
258
+ "$SCRIPT_DIR/package.json" 2>/dev/null || \
259
+ grep '"version"' "$SCRIPT_DIR/package.json" 2>/dev/null | head -1 | sed 's/.*"version": *"\([^"]*\)".*/\1/' || \
260
+ echo "unknown")
261
+ printf '%s\ninstalled:%s\n' "$PROJECT_VERSION" "$(date)" > "$CPMM_MARKER"
142
262
 
143
- echo "✅ Installation complete!"
263
+ # Old backup cleanup hint
264
+ OLD_BACKUPS=$(find "$HOME" -maxdepth 1 -name ".claude-backup-*" -type d 2>/dev/null | wc -l | tr -d ' ')
265
+ if [ "$OLD_BACKUPS" -gt 0 ]; then
266
+ echo ""
267
+ echo "💡 Old backups detected: $OLD_BACKUPS directory(ies) named ~/.claude-backup-*"
268
+ echo " Review and remove: rm -rf ~/.claude-backup-*"
269
+ fi
270
+
271
+ # Final success message showing mode
272
+ echo ""
273
+ if [ "$IS_UPDATE" = true ]; then
274
+ echo "✅ Update complete!"
275
+ else
276
+ echo "✅ Installation complete!"
277
+ fi
144
278
  echo ""
145
279
  echo "Quick Start:"
146
280
  echo " claude"
@@ -148,6 +282,10 @@ echo " > /plan Design a new feature"
148
282
  echo " > /dplan Analyze complex architecture"
149
283
  echo " > /do Implement the login page"
150
284
  echo ""
285
+ echo "Dependency Check:"
286
+ echo " cpmm setup # install missing deps (jq, mgrep, tmux)"
287
+ echo " cpmm doctor # check status only"
288
+ echo ""
151
289
  echo "Language:"
152
290
  echo " To change language: edit ~/.claude/rules/language.md"
153
291
  echo " To use English: rm ~/.claude/rules/language.md"
package/lib/cli.js ADDED
@@ -0,0 +1,244 @@
1
+ "use strict";
2
+
3
+ const { execSync, spawnSync } = require("node:child_process");
4
+ const path = require("node:path");
5
+ const fs = require("node:fs");
6
+ const pkg = require("../package.json");
7
+
8
+ const DEPS = [
9
+ {
10
+ key: "claude",
11
+ command: "claude",
12
+ required: false,
13
+ installKind: "skip",
14
+ description: "Claude Code CLI (assumed pre-installed)",
15
+ },
16
+ {
17
+ key: "jq",
18
+ command: "jq",
19
+ required: true,
20
+ installKind: "system",
21
+ systemPackage: "jq",
22
+ description: "JSON processor for CLI workflows",
23
+ },
24
+ {
25
+ key: "mgrep",
26
+ command: "mgrep",
27
+ required: true,
28
+ installKind: "npm",
29
+ npmPackage: "@mixedbread/mgrep",
30
+ postInstall: ["mgrep", "install-claude-code"],
31
+ description: "Fast code search tool",
32
+ },
33
+ {
34
+ key: "tmux",
35
+ command: "tmux",
36
+ required: true,
37
+ installKind: "system",
38
+ systemPackage: "tmux",
39
+ description: "Terminal multiplexer for background agents",
40
+ },
41
+ ];
42
+
43
+ function commandExists(cmd) {
44
+ try {
45
+ execSync(`command -v ${cmd}`, { stdio: "ignore" });
46
+ return true;
47
+ } catch {
48
+ return false;
49
+ }
50
+ }
51
+
52
+ function detectSystemInstaller() {
53
+ if (commandExists("brew")) return { ok: true, installer: "brew", sudo: false };
54
+ if (commandExists("apt-get")) return { ok: true, installer: "apt-get", sudo: true };
55
+ if (commandExists("dnf")) return { ok: true, installer: "dnf", sudo: true };
56
+ if (commandExists("pacman")) return { ok: true, installer: "pacman", sudo: true };
57
+ if (commandExists("apk")) return { ok: true, installer: "apk", sudo: true };
58
+ return { ok: false };
59
+ }
60
+
61
+ function buildInstallCmd(installer, pkg, sudo) {
62
+ const prefix = sudo ? "sudo " : "";
63
+ switch (installer) {
64
+ case "brew": return `brew install ${pkg}`;
65
+ case "apt-get": return `${prefix}apt-get install -y ${pkg}`;
66
+ case "dnf": return `${prefix}dnf install -y ${pkg}`;
67
+ case "pacman": return `${prefix}pacman -S --noconfirm ${pkg}`;
68
+ case "apk": return `${prefix}apk add ${pkg}`;
69
+ default: return null;
70
+ }
71
+ }
72
+
73
+ function runCmd(cmd, label) {
74
+ console.log(` $ ${cmd}`);
75
+ const result = spawnSync("sh", ["-c", cmd], { stdio: "inherit" });
76
+ if (result.status !== 0) {
77
+ console.error(` FAIL: ${label} (exit ${result.status})`);
78
+ return false;
79
+ }
80
+ return true;
81
+ }
82
+
83
+ function installDep(dep) {
84
+ if (dep.installKind === "skip") return true;
85
+
86
+ if (dep.installKind === "npm") {
87
+ if (!commandExists("npm")) {
88
+ console.error(` npm not found. Install Node.js first, then: npm i -g ${dep.npmPackage}`);
89
+ return false;
90
+ }
91
+ if (!runCmd(`npm install -g ${dep.npmPackage}`, dep.key)) return false;
92
+ if (dep.postInstall) {
93
+ return runCmd(dep.postInstall.join(" "), `${dep.key} post-install`);
94
+ }
95
+ return true;
96
+ }
97
+
98
+ if (dep.installKind === "system") {
99
+ const sys = detectSystemInstaller();
100
+ if (!sys.ok) {
101
+ const hint = process.platform === "darwin"
102
+ ? `\n Install Homebrew first: /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"`
103
+ : "";
104
+ console.error(` No supported package manager found. Install ${dep.systemPackage} manually.${hint}`);
105
+ return false;
106
+ }
107
+ const cmd = buildInstallCmd(sys.installer, dep.systemPackage, sys.sudo);
108
+ return runCmd(cmd, dep.key);
109
+ }
110
+
111
+ return false;
112
+ }
113
+
114
+ function checkDeps() {
115
+ return DEPS.map((dep) => ({
116
+ ...dep,
117
+ installed: commandExists(dep.command),
118
+ }));
119
+ }
120
+
121
+ function printStatus(results) {
122
+ console.log("");
123
+ for (const r of results) {
124
+ const icon = r.installed ? "OK" : (r.required ? "MISSING" : "SKIP");
125
+ const tag = r.required ? "[required]" : "[optional]";
126
+ console.log(` ${icon} ${r.key.padEnd(8)} ${tag} ${r.description}`);
127
+ }
128
+ console.log("");
129
+ }
130
+
131
+ function runSetup() {
132
+ console.log(`CPMM v${pkg.version} - Setup`);
133
+
134
+ const results = checkDeps();
135
+ const missing = results.filter((r) => r.required && !r.installed);
136
+
137
+ if (missing.length > 0) {
138
+ console.log(`\nInstalling ${missing.length} missing dependency(ies)...\n`);
139
+
140
+ for (const dep of missing) {
141
+ console.log(`[${dep.key}] ${dep.description}`);
142
+ if (!installDep(dep)) {
143
+ // continue to install others
144
+ } else {
145
+ console.log(` Done.\n`);
146
+ }
147
+ }
148
+
149
+ const after = checkDeps();
150
+ printStatus(after);
151
+
152
+ const stillMissing = after.filter((r) => r.required && !r.installed);
153
+ if (stillMissing.length > 0) {
154
+ console.error(`${stillMissing.length} required dep(s) still missing.`);
155
+ return 1;
156
+ }
157
+ } else {
158
+ console.log("\nAll dependencies installed.");
159
+ printStatus(results);
160
+ }
161
+
162
+ // Run install.sh for config files, language, and Perplexity setup
163
+ if (!runInstallScript()) {
164
+ return 1;
165
+ }
166
+
167
+ console.log("Setup complete.");
168
+ return 0;
169
+ }
170
+
171
+ function runInstallScript() {
172
+ const scriptPath = path.resolve(__dirname, "..", "install.sh");
173
+ if (!fs.existsSync(scriptPath)) {
174
+ return true;
175
+ }
176
+ console.log("Configuring CPMM...\n");
177
+ const result = spawnSync("bash", [scriptPath], {
178
+ stdio: "inherit",
179
+ env: { ...process.env },
180
+ });
181
+ if (result.status !== 0) {
182
+ console.error("Config setup had issues. Run 'bash install.sh' manually if needed.");
183
+ return false;
184
+ }
185
+ return true;
186
+ }
187
+
188
+ function runDoctor() {
189
+ console.log(`CPMM v${pkg.version} - Doctor`);
190
+
191
+ const results = checkDeps();
192
+ printStatus(results);
193
+
194
+ const missing = results.filter((r) => r.required && !r.installed);
195
+ if (missing.length > 0) {
196
+ console.log(`Fix: cpmm setup`);
197
+ return 1;
198
+ }
199
+
200
+ console.log("All checks passed.");
201
+ return 0;
202
+ }
203
+
204
+ function printHelp() {
205
+ console.log(`CPMM v${pkg.version}
206
+
207
+ Usage:
208
+ cpmm setup Install deps + configure CPMM (language, Perplexity)
209
+ cpmm doctor Check dependency status
210
+ cpmm --help Show this help
211
+ cpmm --version Show version
212
+ `);
213
+ }
214
+
215
+ function runCli(argv) {
216
+ const cmd = argv[0] || "setup";
217
+
218
+ if (cmd === "--help" || cmd === "-h" || cmd === "help") {
219
+ printHelp();
220
+ return 0;
221
+ }
222
+
223
+ if (cmd === "--version" || cmd === "-v" || cmd === "version") {
224
+ console.log(pkg.version);
225
+ return 0;
226
+ }
227
+
228
+ if (process.platform === "win32") {
229
+ console.error("CPMM requires macOS/Linux. Windows users: use WSL.");
230
+ return 1;
231
+ }
232
+
233
+ switch (cmd) {
234
+ case "setup":
235
+ return runSetup();
236
+ case "doctor":
237
+ return runDoctor();
238
+ default:
239
+ console.error(`Unknown command: ${cmd}\nRun 'cpmm --help' for usage.`);
240
+ return 2;
241
+ }
242
+ }
243
+
244
+ module.exports = { runCli };