monol-plugin-scout 2.1.3 → 4.1.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.
Files changed (87) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +244 -0
  3. package/monol-plugin-scout-pkg/CLAUDE.md +125 -8
  4. package/monol-plugin-scout-pkg/api/mock/categories.json +81 -0
  5. package/monol-plugin-scout-pkg/api/mock/insights.json +111 -0
  6. package/monol-plugin-scout-pkg/api/mock/marketplace.json +501 -0
  7. package/monol-plugin-scout-pkg/api/mock/trending.json +96 -0
  8. package/monol-plugin-scout-pkg/commands/console.md +79 -0
  9. package/monol-plugin-scout-pkg/commands/frequency.md +32 -0
  10. package/monol-plugin-scout-pkg/commands/install.md +32 -0
  11. package/monol-plugin-scout-pkg/commands/priority.md +30 -0
  12. package/monol-plugin-scout-pkg/commands/quiet.md +32 -0
  13. package/monol-plugin-scout-pkg/commands/schedule.md +32 -0
  14. package/monol-plugin-scout-pkg/commands/scout.md +8 -5
  15. package/monol-plugin-scout-pkg/commands/skills.md +160 -0
  16. package/monol-plugin-scout-pkg/commands/team.md +32 -0
  17. package/monol-plugin-scout-pkg/commands/timing.md +32 -0
  18. package/monol-plugin-scout-pkg/commands/trust.md +85 -0
  19. package/monol-plugin-scout-pkg/commands/trusted-install.md +98 -0
  20. package/monol-plugin-scout-pkg/commands/uninstall.md +31 -0
  21. package/monol-plugin-scout-pkg/config.yaml +57 -0
  22. package/monol-plugin-scout-pkg/data/.cache/676d5ab664292155e5f509c69d4edae1 +10 -0
  23. package/monol-plugin-scout-pkg/data/.session +8 -0
  24. package/monol-plugin-scout-pkg/data/analytics.json +102 -0
  25. package/monol-plugin-scout-pkg/data/history.json +67 -11
  26. package/monol-plugin-scout-pkg/data/logs/scout.log +1 -0
  27. package/monol-plugin-scout-pkg/data/schedules.json +17 -0
  28. package/monol-plugin-scout-pkg/data/team.json +53 -0
  29. package/monol-plugin-scout-pkg/data/usage.json +100 -7
  30. package/monol-plugin-scout-pkg/docs/ARCHITECTURE.md +201 -0
  31. package/monol-plugin-scout-pkg/hooks/generate-insights.sh +102 -0
  32. package/monol-plugin-scout-pkg/hooks/hooks.json +36 -1
  33. package/monol-plugin-scout-pkg/hooks/on-session-end.sh +136 -0
  34. package/monol-plugin-scout-pkg/hooks/on-session-start.sh +43 -0
  35. package/monol-plugin-scout-pkg/hooks/on-stop.md +79 -0
  36. package/monol-plugin-scout-pkg/hooks/open-console.sh +111 -0
  37. package/monol-plugin-scout-pkg/hooks/open-dashboard.sh +61 -0
  38. package/monol-plugin-scout-pkg/hooks/track-usage.sh +59 -0
  39. package/monol-plugin-scout-pkg/lib/ai-recommender.sh +505 -0
  40. package/monol-plugin-scout-pkg/lib/cache.sh +194 -0
  41. package/monol-plugin-scout-pkg/lib/data-validator.sh +360 -0
  42. package/monol-plugin-scout-pkg/lib/error-handler.sh +296 -0
  43. package/monol-plugin-scout-pkg/lib/logger.sh +263 -0
  44. package/monol-plugin-scout-pkg/lib/plugin-manager.sh +239 -0
  45. package/monol-plugin-scout-pkg/lib/priority-scorer.sh +262 -0
  46. package/monol-plugin-scout-pkg/lib/profile-learner.sh +339 -0
  47. package/monol-plugin-scout-pkg/lib/project-analyzer.sh +281 -0
  48. package/monol-plugin-scout-pkg/lib/recommendation-controller.sh +290 -0
  49. package/monol-plugin-scout-pkg/lib/rejection-learner.sh +232 -0
  50. package/monol-plugin-scout-pkg/lib/scheduler.sh +275 -0
  51. package/monol-plugin-scout-pkg/lib/skill-scout.sh +729 -0
  52. package/monol-plugin-scout-pkg/lib/sync.sh +221 -0
  53. package/monol-plugin-scout-pkg/lib/team-learner.sh +450 -0
  54. package/monol-plugin-scout-pkg/lib/team-manager.sh +369 -0
  55. package/monol-plugin-scout-pkg/lib/trend-learner.sh +428 -0
  56. package/monol-plugin-scout-pkg/lib/trust-manager.sh +261 -0
  57. package/monol-plugin-scout-pkg/lib/trusted-installer.sh +738 -0
  58. package/monol-plugin-scout-pkg/plugin.json +3 -2
  59. package/monol-plugin-scout-pkg/skills/audit.md +6 -0
  60. package/monol-plugin-scout-pkg/skills/cleanup.md +6 -0
  61. package/monol-plugin-scout-pkg/skills/compare.md +6 -0
  62. package/monol-plugin-scout-pkg/skills/console.md +315 -0
  63. package/monol-plugin-scout-pkg/skills/explore.md +6 -0
  64. package/monol-plugin-scout-pkg/skills/fork.md +6 -0
  65. package/monol-plugin-scout-pkg/skills/frequency.md +93 -0
  66. package/monol-plugin-scout-pkg/skills/install.md +127 -0
  67. package/monol-plugin-scout-pkg/skills/priority.md +77 -0
  68. package/monol-plugin-scout-pkg/skills/quiet.md +73 -0
  69. package/monol-plugin-scout-pkg/skills/schedule.md +95 -0
  70. package/monol-plugin-scout-pkg/skills/scout.md +27 -17
  71. package/monol-plugin-scout-pkg/skills/skills.md +230 -0
  72. package/monol-plugin-scout-pkg/skills/team.md +117 -0
  73. package/monol-plugin-scout-pkg/skills/timing.md +97 -0
  74. package/monol-plugin-scout-pkg/skills/trust.md +120 -0
  75. package/monol-plugin-scout-pkg/skills/trusted-install.md +264 -0
  76. package/monol-plugin-scout-pkg/skills/uninstall.md +100 -0
  77. package/monol-plugin-scout-pkg/web/components/activity-chart.js +208 -0
  78. package/monol-plugin-scout-pkg/web/components/index.js +27 -0
  79. package/monol-plugin-scout-pkg/web/components/insight-card.js +365 -0
  80. package/monol-plugin-scout-pkg/web/components/overview.js +154 -0
  81. package/monol-plugin-scout-pkg/web/components/plugin-list.js +242 -0
  82. package/monol-plugin-scout-pkg/web/components/stats-card.js +126 -0
  83. package/monol-plugin-scout-pkg/web/components/team-list.js +346 -0
  84. package/monol-plugin-scout-pkg/web/console.html +2098 -0
  85. package/monol-plugin-scout-pkg/web/dashboard.html +2106 -0
  86. package/monol-plugin-scout-pkg/web/manifest.json +29 -0
  87. package/package.json +1 -1
@@ -0,0 +1,738 @@
1
+ #!/bin/bash
2
+ # Plugin Scout - 신뢰된 자동 설치 엔진
3
+ # 신뢰된 저자(Anthropic 등)의 공식 플러그인을 자동 설치
4
+
5
+ PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$(dirname "$0")/..}"
6
+ DATA_DIR="$PLUGIN_ROOT/data"
7
+
8
+ MARKETPLACE_FILE="$HOME/.claude/plugins/marketplaces/claude-plugins-official/.claude-plugin/marketplace.json"
9
+ CACHE_DIR="$DATA_DIR/.cache"
10
+ CACHE_FILE="$CACHE_DIR/trusted-catalog.json"
11
+ INSTALL_LOG="$DATA_DIR/.last-auto-install.json"
12
+ HISTORY_FILE="$DATA_DIR/history.json"
13
+ USAGE_FILE="$DATA_DIR/usage.json"
14
+ CLAUDE_SETTINGS="$HOME/.claude/settings.json"
15
+ CONFIG_FILE="$PLUGIN_ROOT/config.yaml"
16
+
17
+ LOGGER="$PLUGIN_ROOT/lib/logger.sh"
18
+ PLUGIN_MANAGER="$PLUGIN_ROOT/lib/plugin-manager.sh"
19
+ PROJECT_ANALYZER="$PLUGIN_ROOT/lib/project-analyzer.sh"
20
+
21
+ # ---------------------------------------------------------------------------
22
+ # Helpers
23
+ # ---------------------------------------------------------------------------
24
+
25
+ _log() {
26
+ local level="$1"
27
+ local msg="$2"
28
+ if [ -f "$LOGGER" ]; then
29
+ bash "$LOGGER" "$level" "$msg" "trusted-installer"
30
+ fi
31
+ }
32
+
33
+ _log_event() {
34
+ local event_type="$1"
35
+ local event_data="$2"
36
+ if [ -f "$LOGGER" ]; then
37
+ bash "$LOGGER" event "$event_type" "$event_data"
38
+ fi
39
+ }
40
+
41
+ # ---------------------------------------------------------------------------
42
+ # Config reading
43
+ # ---------------------------------------------------------------------------
44
+
45
+ # Read a value from config.yaml auto_install section
46
+ # Usage: read_config <key> [default]
47
+ read_config() {
48
+ local key="$1"
49
+ local default="$2"
50
+ local value=""
51
+
52
+ if [ -f "$CONFIG_FILE" ]; then
53
+ value=$(grep "^ ${key}:" "$CONFIG_FILE" 2>/dev/null | sed "s/^ ${key}:[[:space:]]*//" | sed 's/[[:space:]]*$//')
54
+ fi
55
+
56
+ if [ -z "$value" ]; then
57
+ echo "$default"
58
+ else
59
+ echo "$value"
60
+ fi
61
+ }
62
+
63
+ # Check if trusted auto-install is enabled
64
+ # Returns 0 if BOTH config.yaml and history.json agree, 1 otherwise
65
+ is_enabled() {
66
+ # 1. Check config.yaml auto_install.enabled
67
+ local config_enabled
68
+ config_enabled=$(read_config "enabled" "false")
69
+ if [ "$config_enabled" != "true" ]; then
70
+ return 1
71
+ fi
72
+
73
+ # 2. Check history.json preferences.trustedAutoInstall
74
+ if [ -f "$HISTORY_FILE" ] && command -v jq &> /dev/null; then
75
+ local history_enabled
76
+ history_enabled=$(jq -r '.preferences.trustedAutoInstall // false' "$HISTORY_FILE" 2>/dev/null)
77
+ if [ "$history_enabled" != "true" ]; then
78
+ return 1
79
+ fi
80
+ else
81
+ # No history file or no jq → treat as disabled
82
+ return 1
83
+ fi
84
+
85
+ return 0
86
+ }
87
+
88
+ # ---------------------------------------------------------------------------
89
+ # Trusted authors
90
+ # ---------------------------------------------------------------------------
91
+
92
+ # Get trusted authors list from config.yaml
93
+ # Returns JSON array, e.g. ["Anthropic"]
94
+ get_trusted_authors() {
95
+ local line
96
+ line=$(grep 'trusted_authors:' "$CONFIG_FILE" 2>/dev/null)
97
+ if [ -z "$line" ]; then
98
+ echo '["Anthropic"]'
99
+ return
100
+ fi
101
+
102
+ # Extract array content: trusted_authors: ["Anthropic", "vercel"] → JSON
103
+ local raw
104
+ raw=$(echo "$line" | sed 's/.*\[/[/' | sed 's/\].*/]/')
105
+ if echo "$raw" | jq '.' &>/dev/null; then
106
+ echo "$raw"
107
+ else
108
+ echo '["Anthropic"]'
109
+ fi
110
+ }
111
+
112
+ # ---------------------------------------------------------------------------
113
+ # Marketplace & installed plugins
114
+ # ---------------------------------------------------------------------------
115
+
116
+ # Parse marketplace.json and filter by trusted authors
117
+ # Returns JSON array of {name, description, author, category}
118
+ get_marketplace_plugins() {
119
+ local marketplace_data=""
120
+
121
+ if [ -f "$MARKETPLACE_FILE" ] && command -v jq &> /dev/null; then
122
+ marketplace_data=$(cat "$MARKETPLACE_FILE" 2>/dev/null)
123
+ fi
124
+
125
+ # Fallback to cache if marketplace file not available
126
+ if [ -z "$marketplace_data" ] || ! echo "$marketplace_data" | jq '.' &>/dev/null; then
127
+ if [ -f "$CACHE_FILE" ]; then
128
+ cat "$CACHE_FILE"
129
+ return 0
130
+ else
131
+ # No marketplace data and no cache — exit silently
132
+ echo "[]"
133
+ return 0
134
+ fi
135
+ fi
136
+
137
+ local trusted_authors
138
+ trusted_authors=$(get_trusted_authors)
139
+
140
+ # Filter plugins by trusted authors and project to {name, description, author, category}
141
+ local filtered
142
+ filtered=$(echo "$marketplace_data" | jq --argjson authors "$trusted_authors" '
143
+ [.plugins // .[] | objects] | flatten |
144
+ map(select(
145
+ .author.name as $aname |
146
+ ($authors | map(ascii_downcase) | index($aname | ascii_downcase)) != null
147
+ )) |
148
+ map({
149
+ name: (.name // .id // "unknown"),
150
+ description: (.description // ""),
151
+ author: (.author.name // "unknown"),
152
+ category: (.category // "general")
153
+ })
154
+ ' 2>/dev/null)
155
+
156
+ if [ -z "$filtered" ] || ! echo "$filtered" | jq '.' &>/dev/null; then
157
+ # Try alternate JSON structure (flat array)
158
+ filtered=$(echo "$marketplace_data" | jq --argjson authors "$trusted_authors" '
159
+ if type == "array" then . else [.] end |
160
+ map(select(
161
+ (.author.name // .author // "") as $aname |
162
+ ($authors | map(ascii_downcase) | index($aname | ascii_downcase)) != null
163
+ )) |
164
+ map({
165
+ name: (.name // .id // "unknown"),
166
+ description: (.description // ""),
167
+ author: (if .author | type == "object" then .author.name else (.author // "unknown") end),
168
+ category: (.category // "general")
169
+ })
170
+ ' 2>/dev/null)
171
+ fi
172
+
173
+ if [ -z "$filtered" ] || ! echo "$filtered" | jq '.' &>/dev/null; then
174
+ echo "[]"
175
+ return 0
176
+ fi
177
+
178
+ # Cache for offline fallback
179
+ local cache_catalog
180
+ cache_catalog=$(read_config "cache_catalog" "true")
181
+ if [ "$cache_catalog" = "true" ]; then
182
+ mkdir -p "$CACHE_DIR"
183
+ echo "$filtered" > "$CACHE_FILE"
184
+ fi
185
+
186
+ echo "$filtered"
187
+ }
188
+
189
+ # Get list of currently installed/enabled plugins
190
+ get_installed_plugins() {
191
+ if command -v jq &> /dev/null && [ -f "$CLAUDE_SETTINGS" ]; then
192
+ jq -r '.enabledPlugins // {} | keys[]' "$CLAUDE_SETTINGS" 2>/dev/null
193
+ fi
194
+ }
195
+
196
+ # ---------------------------------------------------------------------------
197
+ # Language matching (LSP plugins)
198
+ # ---------------------------------------------------------------------------
199
+
200
+ # Check if an LSP plugin matches the current project language
201
+ # Returns 0 (match) or 1 (no match). Non-LSP plugins always return 0.
202
+ detect_language_match() {
203
+ local plugin_name="$1"
204
+ local project_types=""
205
+
206
+ # Get project types from project-analyzer
207
+ if [ -f "$PROJECT_ANALYZER" ]; then
208
+ project_types=$(bash "$PROJECT_ANALYZER" types 2>/dev/null)
209
+ fi
210
+
211
+ case "$plugin_name" in
212
+ typescript-lsp)
213
+ echo "$project_types" | grep -qE '"nodejs"|"typescript"' && return 0
214
+ return 1
215
+ ;;
216
+ pyright-lsp|python-lsp)
217
+ echo "$project_types" | grep -q '"python"' && return 0
218
+ return 1
219
+ ;;
220
+ gopls-lsp|go-lsp)
221
+ echo "$project_types" | grep -q '"go"' && return 0
222
+ return 1
223
+ ;;
224
+ rust-analyzer-lsp|rust-lsp)
225
+ echo "$project_types" | grep -q '"rust"' && return 0
226
+ return 1
227
+ ;;
228
+ jdtls-lsp|java-lsp)
229
+ echo "$project_types" | grep -q '"java"' && return 0
230
+ return 1
231
+ ;;
232
+ php-lsp)
233
+ echo "$project_types" | grep -q '"php"' && return 0
234
+ return 1
235
+ ;;
236
+ clangd-lsp|c-lsp|cpp-lsp)
237
+ find . -maxdepth 3 -name "*.c" -o -name "*.cpp" -o -name "*.h" 2>/dev/null | head -1 | grep -q . && return 0
238
+ return 1
239
+ ;;
240
+ swift-lsp)
241
+ find . -maxdepth 3 -name "*.swift" 2>/dev/null | head -1 | grep -q . && return 0
242
+ return 1
243
+ ;;
244
+ kotlin-lsp)
245
+ find . -maxdepth 3 -name "*.kt" -o -name "*.kts" 2>/dev/null | head -1 | grep -q . && return 0
246
+ return 1
247
+ ;;
248
+ csharp-lsp)
249
+ find . -maxdepth 3 -name "*.cs" 2>/dev/null | head -1 | grep -q . && return 0
250
+ return 1
251
+ ;;
252
+ lua-lsp)
253
+ find . -maxdepth 3 -name "*.lua" 2>/dev/null | head -1 | grep -q . && return 0
254
+ return 1
255
+ ;;
256
+ *)
257
+ # Non-LSP plugins always match
258
+ return 0
259
+ ;;
260
+ esac
261
+ }
262
+
263
+ # ---------------------------------------------------------------------------
264
+ # Conflict detection
265
+ # ---------------------------------------------------------------------------
266
+
267
+ # Check for conflicting plugins already installed
268
+ # Returns 0 (no conflict) or 1 (conflict detected)
269
+ check_conflicts() {
270
+ local plugin_name="$1"
271
+ local installed
272
+ installed=$(get_installed_plugins)
273
+
274
+ # Define conflict groups
275
+ local -A conflict_groups
276
+ conflict_groups=(
277
+ ["prettier-format"]="formatters"
278
+ ["eslint-fix"]="formatters"
279
+ ["biome-format"]="formatters"
280
+ ["eslint-lsp"]="linters"
281
+ ["biome-lsp"]="linters"
282
+ ["commit-commands"]="git"
283
+ ["git-helper"]="git"
284
+ ["conventional-commits"]="git"
285
+ )
286
+
287
+ local plugin_group="${conflict_groups[$plugin_name]}"
288
+ if [ -z "$plugin_group" ]; then
289
+ # Plugin is not in any conflict group
290
+ return 0
291
+ fi
292
+
293
+ # Check if another plugin from the same group is installed
294
+ for key in "${!conflict_groups[@]}"; do
295
+ if [ "$key" = "$plugin_name" ]; then
296
+ continue
297
+ fi
298
+ if [ "${conflict_groups[$key]}" = "$plugin_group" ]; then
299
+ # Check if this conflicting plugin is installed
300
+ if echo "$installed" | grep -qE "^${key}(@|$)"; then
301
+ return 1
302
+ fi
303
+ fi
304
+ done
305
+
306
+ return 0
307
+ }
308
+
309
+ # ---------------------------------------------------------------------------
310
+ # Filter pipeline
311
+ # ---------------------------------------------------------------------------
312
+
313
+ # Main filter: get marketplace plugins → exclude installed → apply LSP/conflict filters
314
+ # Returns JSON array of candidate plugins
315
+ filter_relevant() {
316
+ local marketplace_plugins
317
+ marketplace_plugins=$(get_marketplace_plugins)
318
+
319
+ if [ "$marketplace_plugins" = "[]" ] || [ -z "$marketplace_plugins" ]; then
320
+ echo "[]"
321
+ return
322
+ fi
323
+
324
+ local installed
325
+ installed=$(get_installed_plugins)
326
+
327
+ local lsp_filter
328
+ lsp_filter=$(read_config "lsp_language_filter" "true")
329
+
330
+ local conflict_check
331
+ conflict_check=$(read_config "check_conflicts" "true")
332
+
333
+ local candidates="[]"
334
+
335
+ # Iterate marketplace plugins
336
+ local count
337
+ count=$(echo "$marketplace_plugins" | jq 'length' 2>/dev/null)
338
+ if [ -z "$count" ] || [ "$count" = "0" ]; then
339
+ echo "[]"
340
+ return
341
+ fi
342
+
343
+ local i=0
344
+ while [ "$i" -lt "$count" ]; do
345
+ local plugin_json
346
+ plugin_json=$(echo "$marketplace_plugins" | jq ".[$i]" 2>/dev/null)
347
+ local plugin_name
348
+ plugin_name=$(echo "$plugin_json" | jq -r '.name' 2>/dev/null)
349
+
350
+ # Skip if already installed
351
+ if echo "$installed" | grep -qE "^${plugin_name}(@|$)"; then
352
+ i=$((i + 1))
353
+ continue
354
+ fi
355
+
356
+ # Skip if LSP and language doesn't match
357
+ if [ "$lsp_filter" = "true" ]; then
358
+ if ! detect_language_match "$plugin_name"; then
359
+ i=$((i + 1))
360
+ continue
361
+ fi
362
+ fi
363
+
364
+ # Skip if conflict detected
365
+ if [ "$conflict_check" = "true" ]; then
366
+ if ! check_conflicts "$plugin_name"; then
367
+ i=$((i + 1))
368
+ continue
369
+ fi
370
+ fi
371
+
372
+ # Add to candidates
373
+ candidates=$(echo "$candidates" | jq --argjson p "$plugin_json" '. + [$p]' 2>/dev/null)
374
+
375
+ i=$((i + 1))
376
+ done
377
+
378
+ echo "$candidates"
379
+ }
380
+
381
+ # ---------------------------------------------------------------------------
382
+ # Installation
383
+ # ---------------------------------------------------------------------------
384
+
385
+ # Install a single plugin via plugin-manager.sh
386
+ execute_install() {
387
+ local plugin_name="$1"
388
+ local marketplace
389
+ marketplace=$(read_config "marketplace" "claude-plugins-official")
390
+
391
+ if [ -f "$PLUGIN_MANAGER" ]; then
392
+ bash "$PLUGIN_MANAGER" install "${plugin_name}@${marketplace}" "trusted-auto-install" 2>&1
393
+ return $?
394
+ else
395
+ _log "error" "plugin-manager.sh not found"
396
+ return 1
397
+ fi
398
+ }
399
+
400
+ # Record install result in history.json and usage.json
401
+ record_install() {
402
+ local plugin_name="$1"
403
+ local status="$2" # success or failure
404
+ local timestamp
405
+ timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
406
+ local date_only
407
+ date_only=$(date +"%Y-%m-%d")
408
+
409
+ # Add to trustedInstallLog in history.json
410
+ if [ -f "$HISTORY_FILE" ] && command -v jq &> /dev/null; then
411
+ jq --arg plugin "$plugin_name" \
412
+ --arg status "$status" \
413
+ --arg ts "$timestamp" \
414
+ --arg date "$date_only" \
415
+ '
416
+ .lastUpdated = $ts |
417
+ .trustedInstallLog = ((.trustedInstallLog // []) + [{
418
+ plugin: $plugin,
419
+ status: $status,
420
+ timestamp: $ts,
421
+ date: $date,
422
+ source: "trusted-auto-install"
423
+ }])
424
+ ' "$HISTORY_FILE" > "$HISTORY_FILE.tmp" && mv "$HISTORY_FILE.tmp" "$HISTORY_FILE"
425
+ fi
426
+
427
+ # Add to usage.json if success
428
+ if [ "$status" = "success" ] && [ -f "$USAGE_FILE" ] && command -v jq &> /dev/null; then
429
+ jq --arg plugin "$plugin_name" \
430
+ --arg date "$date_only" \
431
+ --arg ts "$timestamp" \
432
+ '
433
+ .lastUpdated = $ts |
434
+ .plugins[$plugin] = (.plugins[$plugin] // {
435
+ installed: $date,
436
+ usageCount: 0,
437
+ lastUsed: null,
438
+ source: "trusted-auto-install"
439
+ })
440
+ ' "$USAGE_FILE" > "$USAGE_FILE.tmp" && mv "$USAGE_FILE.tmp" "$USAGE_FILE"
441
+ fi
442
+
443
+ _log_event "trusted_install" "{\"plugin\":\"$plugin_name\",\"status\":\"$status\"}"
444
+ }
445
+
446
+ # ---------------------------------------------------------------------------
447
+ # New release detection
448
+ # ---------------------------------------------------------------------------
449
+
450
+ # Compare cached catalog with current to detect new plugins
451
+ detect_new_releases() {
452
+ if [ ! -f "$CACHE_FILE" ]; then
453
+ echo "[]"
454
+ return
455
+ fi
456
+
457
+ local old_cache
458
+ old_cache=$(cat "$CACHE_FILE" 2>/dev/null)
459
+ if [ -z "$old_cache" ] || ! echo "$old_cache" | jq '.' &>/dev/null; then
460
+ echo "[]"
461
+ return
462
+ fi
463
+
464
+ local current
465
+ current=$(get_marketplace_plugins)
466
+ if [ -z "$current" ] || ! echo "$current" | jq '.' &>/dev/null; then
467
+ echo "[]"
468
+ return
469
+ fi
470
+
471
+ # Find plugins in current that were not in old cache
472
+ local new_releases
473
+ new_releases=$(jq -n --argjson old "$old_cache" --argjson cur "$current" '
474
+ ($old | map(.name)) as $old_names |
475
+ [$cur[] | select(.name as $n | ($old_names | index($n)) == null)]
476
+ ' 2>/dev/null)
477
+
478
+ if [ -z "$new_releases" ] || ! echo "$new_releases" | jq '.' &>/dev/null; then
479
+ echo "[]"
480
+ else
481
+ echo "$new_releases"
482
+ fi
483
+ }
484
+
485
+ # ---------------------------------------------------------------------------
486
+ # Reporting
487
+ # ---------------------------------------------------------------------------
488
+
489
+ # Write install report to .last-auto-install.json
490
+ write_install_report() {
491
+ local installed="$1"
492
+ local skipped="$2"
493
+ local new_releases="$3"
494
+ local errors="$4"
495
+ local timestamp
496
+ timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
497
+
498
+ local installed_count
499
+ installed_count=$(echo "$installed" | jq 'length' 2>/dev/null || echo "0")
500
+ local skipped_count
501
+ skipped_count=$(echo "$skipped" | jq 'length' 2>/dev/null || echo "0")
502
+ local new_count
503
+ new_count=$(echo "$new_releases" | jq 'length' 2>/dev/null || echo "0")
504
+
505
+ jq -n \
506
+ --arg ts "$timestamp" \
507
+ --argjson installed "$installed" \
508
+ --argjson skipped "$skipped" \
509
+ --argjson newReleases "$new_releases" \
510
+ --argjson errors "$errors" \
511
+ --arg summary "Installed ${installed_count}, Skipped ${skipped_count}, New ${new_count}" \
512
+ '{
513
+ timestamp: $ts,
514
+ installed: $installed,
515
+ skipped: $skipped,
516
+ newReleases: $newReleases,
517
+ errors: $errors,
518
+ summary: $summary
519
+ }' > "$INSTALL_LOG"
520
+ }
521
+
522
+ # ---------------------------------------------------------------------------
523
+ # Main orchestrators
524
+ # ---------------------------------------------------------------------------
525
+
526
+ # Run: full auto-install cycle
527
+ run() {
528
+ # 1. Check enabled
529
+ if ! is_enabled; then
530
+ _log "info" "Trusted auto-install is disabled. Use '/scout trusted-install on' to enable."
531
+ return 0
532
+ fi
533
+
534
+ # 2. Ensure cache directory
535
+ mkdir -p "$CACHE_DIR"
536
+
537
+ # 3. Detect new releases (save old cache before refresh)
538
+ local new_releases="[]"
539
+ local detect_new
540
+ detect_new=$(read_config "detect_new_releases" "true")
541
+ if [ "$detect_new" = "true" ]; then
542
+ new_releases=$(detect_new_releases)
543
+ fi
544
+
545
+ # 4. Filter relevant candidates
546
+ local candidates
547
+ candidates=$(filter_relevant)
548
+ if [ -z "$candidates" ] || [ "$candidates" = "[]" ]; then
549
+ write_install_report "[]" "[]" "$new_releases" "[]"
550
+ _log "info" "No new trusted plugins to install"
551
+ return 0
552
+ fi
553
+
554
+ # 5. Install each candidate
555
+ local installed="[]"
556
+ local skipped="[]"
557
+ local errors="[]"
558
+ local count
559
+ count=$(echo "$candidates" | jq 'length' 2>/dev/null)
560
+
561
+ local i=0
562
+ while [ "$i" -lt "${count:-0}" ]; do
563
+ local plugin_name
564
+ plugin_name=$(echo "$candidates" | jq -r ".[$i].name" 2>/dev/null)
565
+
566
+ if [ -z "$plugin_name" ] || [ "$plugin_name" = "null" ]; then
567
+ i=$((i + 1))
568
+ continue
569
+ fi
570
+
571
+ local result
572
+ result=$(execute_install "$plugin_name" 2>&1)
573
+ local exit_code=$?
574
+
575
+ if [ $exit_code -eq 0 ]; then
576
+ record_install "$plugin_name" "success"
577
+ installed=$(echo "$installed" | jq --arg p "$plugin_name" '. + [$p]' 2>/dev/null)
578
+ _log "info" "Installed trusted plugin: $plugin_name"
579
+ else
580
+ record_install "$plugin_name" "failure"
581
+ errors=$(echo "$errors" | jq --arg p "$plugin_name" --arg e "$result" '. + [{plugin: $p, error: $e}]' 2>/dev/null)
582
+ _log "warn" "Failed to install: $plugin_name — $result"
583
+ fi
584
+
585
+ i=$((i + 1))
586
+ done
587
+
588
+ # 6. Write report
589
+ write_install_report "$installed" "$skipped" "$new_releases" "$errors"
590
+
591
+ # 7. Log summary
592
+ local installed_count
593
+ installed_count=$(echo "$installed" | jq 'length' 2>/dev/null || echo "0")
594
+ local error_count
595
+ error_count=$(echo "$errors" | jq 'length' 2>/dev/null || echo "0")
596
+ local new_count
597
+ new_count=$(echo "$new_releases" | jq 'length' 2>/dev/null || echo "0")
598
+
599
+ _log "info" "Trusted auto-install complete: ${installed_count} installed, ${error_count} errors, ${new_count} new releases"
600
+ }
601
+
602
+ # Check: dry-run mode (no installation)
603
+ check() {
604
+ echo "=== Trusted Auto-Install Check (Dry Run) ==="
605
+ echo ""
606
+
607
+ # Status
608
+ if is_enabled; then
609
+ echo "Status: ENABLED"
610
+ else
611
+ echo "Status: DISABLED"
612
+ fi
613
+ echo ""
614
+
615
+ # Trusted authors
616
+ local authors
617
+ authors=$(get_trusted_authors)
618
+ echo "Trusted Authors: $(echo "$authors" | jq -r 'join(", ")' 2>/dev/null)"
619
+ echo ""
620
+
621
+ # Candidates
622
+ local candidates
623
+ candidates=$(filter_relevant)
624
+ local count
625
+ count=$(echo "$candidates" | jq 'length' 2>/dev/null || echo "0")
626
+
627
+ echo "Candidates for installation: $count"
628
+ if [ "$count" != "0" ] && [ "$count" != "" ]; then
629
+ echo "$candidates" | jq -r '.[] | " - \(.name) (\(.author)) — \(.description)"' 2>/dev/null
630
+ fi
631
+ echo ""
632
+
633
+ # New releases
634
+ local detect_new
635
+ detect_new=$(read_config "detect_new_releases" "true")
636
+ if [ "$detect_new" = "true" ]; then
637
+ local new_releases
638
+ new_releases=$(detect_new_releases)
639
+ local new_count
640
+ new_count=$(echo "$new_releases" | jq 'length' 2>/dev/null || echo "0")
641
+
642
+ echo "New releases since last check: $new_count"
643
+ if [ "$new_count" != "0" ] && [ "$new_count" != "" ]; then
644
+ echo "$new_releases" | jq -r '.[] | " - \(.name) (\(.author)) — \(.description)"' 2>/dev/null
645
+ fi
646
+ fi
647
+ echo ""
648
+
649
+ # Installed plugins count
650
+ local installed_count
651
+ installed_count=$(get_installed_plugins | wc -l | tr -d ' ')
652
+ echo "Currently installed plugins: $installed_count"
653
+ }
654
+
655
+ # Enable trusted auto-install in both config.yaml and history.json
656
+ enable() {
657
+ # Update config.yaml
658
+ if [ -f "$CONFIG_FILE" ]; then
659
+ if grep -q "^ enabled:" "$CONFIG_FILE"; then
660
+ sed -i.bak 's/^ enabled:.*/ enabled: true/' "$CONFIG_FILE"
661
+ rm -f "${CONFIG_FILE}.bak"
662
+ fi
663
+ fi
664
+
665
+ # Update history.json
666
+ if [ -f "$HISTORY_FILE" ] && command -v jq &> /dev/null; then
667
+ local timestamp
668
+ timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
669
+ jq --arg ts "$timestamp" '
670
+ .lastUpdated = $ts |
671
+ .preferences.trustedAutoInstall = true
672
+ ' "$HISTORY_FILE" > "$HISTORY_FILE.tmp" && mv "$HISTORY_FILE.tmp" "$HISTORY_FILE"
673
+ fi
674
+
675
+ _log "info" "Trusted auto-install enabled"
676
+ _log_event "trusted_install_toggle" "{\"action\":\"enable\"}"
677
+ echo "Trusted auto-install: ENABLED"
678
+ }
679
+
680
+ # Disable trusted auto-install in both config.yaml and history.json
681
+ disable() {
682
+ # Update config.yaml
683
+ if [ -f "$CONFIG_FILE" ]; then
684
+ if grep -q "^ enabled:" "$CONFIG_FILE"; then
685
+ sed -i.bak 's/^ enabled:.*/ enabled: false/' "$CONFIG_FILE"
686
+ rm -f "${CONFIG_FILE}.bak"
687
+ fi
688
+ fi
689
+
690
+ # Update history.json
691
+ if [ -f "$HISTORY_FILE" ] && command -v jq &> /dev/null; then
692
+ local timestamp
693
+ timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
694
+ jq --arg ts "$timestamp" '
695
+ .lastUpdated = $ts |
696
+ .preferences.trustedAutoInstall = false
697
+ ' "$HISTORY_FILE" > "$HISTORY_FILE.tmp" && mv "$HISTORY_FILE.tmp" "$HISTORY_FILE"
698
+ fi
699
+
700
+ _log "info" "Trusted auto-install disabled"
701
+ _log_event "trusted_install_toggle" "{\"action\":\"disable\"}"
702
+ echo "Trusted auto-install: DISABLED"
703
+ }
704
+
705
+ # ---------------------------------------------------------------------------
706
+ # CLI interface
707
+ # ---------------------------------------------------------------------------
708
+
709
+ case "$1" in
710
+ run)
711
+ run
712
+ ;;
713
+ check)
714
+ check
715
+ ;;
716
+ status)
717
+ cat "$INSTALL_LOG" 2>/dev/null || echo "No install log found"
718
+ ;;
719
+ enable)
720
+ enable
721
+ ;;
722
+ disable)
723
+ disable
724
+ ;;
725
+ *)
726
+ echo "Usage: $0 {run|check|status|enable|disable}"
727
+ echo ""
728
+ echo "Commands:"
729
+ echo " run Execute trusted auto-install (install missing trusted plugins)"
730
+ echo " check Dry-run: show what would be installed (no changes)"
731
+ echo " status Show last auto-install report"
732
+ echo " enable Enable trusted auto-install (config + history)"
733
+ echo " disable Disable trusted auto-install (config + history)"
734
+ echo ""
735
+ echo "Config: config.yaml → auto_install section"
736
+ echo "Log: $INSTALL_LOG"
737
+ ;;
738
+ esac