openclacky 0.9.20 → 0.9.21
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/lib/clacky/default_skills/channel-setup/feishu_setup.rb +4 -4
- data/lib/clacky/default_skills/skill-creator/SKILL.md +6 -0
- data/lib/clacky/default_skills/skill-creator/scripts/validate_skill_frontmatter.rb +143 -0
- data/lib/clacky/version.rb +1 -1
- data/scripts/install.ps1 +4 -4
- data/scripts/install.sh +37 -19
- metadata +2 -2
- data/scripts/install_simple.sh +0 -630
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d48d1b62badf0b608e9f6dc6cacb6b0d2c8f92263a8df77d64f6c63ee26e3580
|
|
4
|
+
data.tar.gz: f4abb3c3aa7dc140a91622135c4eaff7dfbad66bca6908abfb3c76ab7c183629
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: acfdcca20845b40c076f1974dde40d5d62f194af03c14541a98c02c5fd431790df3e811ac17a0d35276938bb5475a53c3e22e8c8b9f56b87d0637f7a5168d7c5
|
|
7
|
+
data.tar.gz: a3ea45c455a7591917801963bb4a08587ea1ab24173d42cb8d1d2718dbf0a17fdf40e11296840dc107863d98e4aa1a2dad8008542b6214a10bda9d575fec7715
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.9.21] - 2026-03-30
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **Feishu channel setup compatibility with v2.6**: fixed Ruby 3.1 syntax incompatibility in the Feishu setup script that caused failures on newer Feishu API versions
|
|
14
|
+
|
|
15
|
+
### Improved
|
|
16
|
+
- **skill-creator YAML validation**: added frontmatter schema validation for skill files, catching malformed skill definitions before they cause runtime errors
|
|
17
|
+
|
|
18
|
+
### More
|
|
19
|
+
- Removed `install_simple.sh` (consolidated into `install.sh`)
|
|
20
|
+
|
|
10
21
|
## [0.9.20] - 2026-03-30
|
|
11
22
|
|
|
12
23
|
### Added
|
|
@@ -71,9 +71,9 @@ BOT_PERMISSIONS = %w[
|
|
|
71
71
|
# Logging helpers
|
|
72
72
|
# ---------------------------------------------------------------------------
|
|
73
73
|
|
|
74
|
-
def step(msg)
|
|
75
|
-
def ok(msg)
|
|
76
|
-
def warn(msg)
|
|
74
|
+
def step(msg); puts("[feishu-setup] #{msg}"); end
|
|
75
|
+
def ok(msg); puts("[feishu-setup] ✅ #{msg}"); end
|
|
76
|
+
def warn(msg); puts("[feishu-setup] ⚠️ #{msg}"); end
|
|
77
77
|
def fail!(msg)
|
|
78
78
|
puts("[feishu-setup] ❌ #{msg}")
|
|
79
79
|
exit 1
|
|
@@ -501,7 +501,7 @@ def run_setup(browser, api)
|
|
|
501
501
|
id = s["id"].to_s
|
|
502
502
|
name_to_id[name] = id if name && !id.empty?
|
|
503
503
|
end
|
|
504
|
-
ids = BOT_PERMISSIONS.
|
|
504
|
+
ids = BOT_PERMISSIONS.map { |n| name_to_id[n] }.compact
|
|
505
505
|
missing = BOT_PERMISSIONS.reject { |n| name_to_id.key?(n) }
|
|
506
506
|
warn "#{missing.size} permissions not matched: #{missing.join(", ")}" unless missing.empty?
|
|
507
507
|
fail! "No permission IDs matched. API response keys: #{name_to_id.keys.first(5).inspect}" if ids.empty?
|
|
@@ -86,6 +86,12 @@ Components to fill in:
|
|
|
86
86
|
>
|
|
87
87
|
> **YAML description gotcha**: If the description contains `word: value` patterns (colons followed by space), YAML treats them as key-value pairs and the frontmatter parse fails silently. Always wrap description values in single quotes. Avoid embedded double-quotes inside single-quoted strings (use rephrasing instead).
|
|
88
88
|
|
|
89
|
+
> **After writing SKILL.md — always validate and auto-fix**: Run this immediately after creating or updating any skill file:
|
|
90
|
+
> ```bash
|
|
91
|
+
> ruby SKILL_DIR/scripts/validate_skill_frontmatter.rb /path/to/new-skill/SKILL.md
|
|
92
|
+
> ```
|
|
93
|
+
> The script validates the YAML frontmatter and auto-fixes common issues (unquoted descriptions, multi-line block scalars with colons). If it prints `OK:` — you're done. If it prints `Auto-fixed and saved` — it repaired the file automatically. If it prints `ERROR` — manual fix required.
|
|
94
|
+
|
|
89
95
|
### Skill Writing Guide
|
|
90
96
|
|
|
91
97
|
#### Anatomy of a Skill
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# validate_skill_frontmatter.rb
|
|
5
|
+
#
|
|
6
|
+
# Validates and auto-fixes the YAML frontmatter of a SKILL.md file.
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# ruby validate_skill_frontmatter.rb <path/to/SKILL.md>
|
|
10
|
+
#
|
|
11
|
+
# What it does:
|
|
12
|
+
# 1. Parses the frontmatter between --- delimiters
|
|
13
|
+
# 2. If YAML is invalid OR description is not a plain String:
|
|
14
|
+
# - Extracts name/description via regex fallback
|
|
15
|
+
# - Re-wraps description in single quotes (collapsed to one line)
|
|
16
|
+
# - Rewrites the frontmatter in the file
|
|
17
|
+
# 3. Exits 0 on success (with or without auto-fix), 1 on unrecoverable error
|
|
18
|
+
|
|
19
|
+
require "yaml"
|
|
20
|
+
|
|
21
|
+
path = ARGV[0]
|
|
22
|
+
|
|
23
|
+
if path.nil? || path.strip.empty?
|
|
24
|
+
warn "Usage: ruby validate_skill_frontmatter.rb <path/to/SKILL.md>"
|
|
25
|
+
exit 1
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
unless File.exist?(path)
|
|
29
|
+
warn "File not found: #{path}"
|
|
30
|
+
exit 1
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
content = File.read(path)
|
|
34
|
+
|
|
35
|
+
# Extract frontmatter block
|
|
36
|
+
fm_match = content.match(/\A(---\n)(.*?)(\n---[ \t]*\n?)/m)
|
|
37
|
+
unless fm_match
|
|
38
|
+
warn "ERROR: No frontmatter block found in #{path}"
|
|
39
|
+
exit 1
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
prefix = fm_match[1] # "---\n"
|
|
43
|
+
yaml_raw = fm_match[2] # raw YAML text
|
|
44
|
+
suffix = fm_match[3] # "\n---\n"
|
|
45
|
+
body = content[fm_match.end(0)..] # rest of file after frontmatter
|
|
46
|
+
|
|
47
|
+
# Attempt normal YAML parse
|
|
48
|
+
parse_ok = false
|
|
49
|
+
data = nil
|
|
50
|
+
begin
|
|
51
|
+
data = YAML.safe_load(yaml_raw) || {}
|
|
52
|
+
parse_ok = data["description"].is_a?(String)
|
|
53
|
+
rescue Psych::Exception => e
|
|
54
|
+
warn "YAML parse error: #{e.message}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
if parse_ok
|
|
58
|
+
puts "OK: name=#{data['name'].inspect} description_length=#{data['description'].length}"
|
|
59
|
+
exit 0
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# --- Auto-fix ---
|
|
63
|
+
puts "Frontmatter invalid or description broken — attempting auto-fix..."
|
|
64
|
+
|
|
65
|
+
# Regex fallback: extract name and description lines
|
|
66
|
+
name_match = yaml_raw.match(/^name:\s*(.+)$/)
|
|
67
|
+
unless name_match
|
|
68
|
+
warn "ERROR: Cannot extract 'name' field from frontmatter. Manual fix required."
|
|
69
|
+
exit 1
|
|
70
|
+
end
|
|
71
|
+
name_value = name_match[1].strip.gsub(/\A['"]|['"]\z/, "")
|
|
72
|
+
|
|
73
|
+
# description may be:
|
|
74
|
+
# description: some text (unquoted)
|
|
75
|
+
# description: 'some text' (single-quoted)
|
|
76
|
+
# description: "some text" (double-quoted)
|
|
77
|
+
# description: first line\n continuation (multi-line block scalar)
|
|
78
|
+
desc_match = yaml_raw.match(/^description:\s*(.+?)(?=\n[a-z]|\z)/m)
|
|
79
|
+
unless desc_match
|
|
80
|
+
warn "ERROR: Cannot extract 'description' field from frontmatter. Manual fix required."
|
|
81
|
+
exit 1
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
raw_desc = desc_match[1].strip
|
|
85
|
+
|
|
86
|
+
# Strip existing outer quotes if present (simple single-line quoted values)
|
|
87
|
+
if raw_desc.start_with?("'") && raw_desc.end_with?("'")
|
|
88
|
+
raw_desc = raw_desc[1..-2]
|
|
89
|
+
elsif raw_desc.start_with?('"') && raw_desc.end_with?('"')
|
|
90
|
+
raw_desc = raw_desc[1..-2]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Collapse multi-line: strip leading whitespace from continuation lines
|
|
94
|
+
description_value = raw_desc.gsub(/\n\s+/, " ").strip
|
|
95
|
+
|
|
96
|
+
# Escape any single quotes inside the description value
|
|
97
|
+
description_value_escaped = description_value.gsub("'", "''")
|
|
98
|
+
|
|
99
|
+
# Extract all other frontmatter lines (everything except name: and description:)
|
|
100
|
+
other_lines = yaml_raw.each_line.reject do |line|
|
|
101
|
+
line.match?(/^(name|description):/) || line.match?(/^\s+\S/) && yaml_raw.match?(/^description:.*\n(\s+.+\n)*/m)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# More precise: collect lines that are not part of the name/description block
|
|
105
|
+
remaining = []
|
|
106
|
+
skip_continuation = false
|
|
107
|
+
yaml_raw.each_line do |line|
|
|
108
|
+
if line.match?(/^(name|description):/)
|
|
109
|
+
skip_continuation = true
|
|
110
|
+
next
|
|
111
|
+
end
|
|
112
|
+
if skip_continuation && line.match?(/^\s+\S/)
|
|
113
|
+
next # continuation of a multi-line block value
|
|
114
|
+
end
|
|
115
|
+
skip_continuation = false
|
|
116
|
+
remaining << line unless line.strip.empty? && remaining.empty?
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Rebuild frontmatter
|
|
120
|
+
fixed_fm_lines = []
|
|
121
|
+
fixed_fm_lines << "name: #{name_value}"
|
|
122
|
+
fixed_fm_lines << "description: '#{description_value_escaped}'"
|
|
123
|
+
remaining.each { |l| fixed_fm_lines << l.chomp }
|
|
124
|
+
|
|
125
|
+
# Remove trailing blank lines from remaining
|
|
126
|
+
fixed_fm = fixed_fm_lines.join("\n").strip
|
|
127
|
+
|
|
128
|
+
new_content = "#{prefix}#{fixed_fm}#{suffix}#{body}"
|
|
129
|
+
|
|
130
|
+
File.write(path, new_content)
|
|
131
|
+
puts "Auto-fixed and saved: #{path}"
|
|
132
|
+
|
|
133
|
+
# Final verification
|
|
134
|
+
begin
|
|
135
|
+
verify_content = File.read(path)
|
|
136
|
+
verify_match = verify_content.match(/\A---\n(.*?)\n---/m)
|
|
137
|
+
verify_data = YAML.safe_load(verify_match[1])
|
|
138
|
+
raise "description not a String" unless verify_data["description"].is_a?(String)
|
|
139
|
+
puts "OK: name=#{verify_data['name'].inspect} description_length=#{verify_data['description'].length}"
|
|
140
|
+
rescue => e
|
|
141
|
+
warn "ERROR: Auto-fix failed, manual intervention required: #{e.message}"
|
|
142
|
+
exit 1
|
|
143
|
+
end
|
data/lib/clacky/version.rb
CHANGED
data/scripts/install.ps1
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
# After rebooting, run the same command again to complete installation.
|
|
18
18
|
#
|
|
19
19
|
# Development: .\install.ps1 -Local
|
|
20
|
-
# Uses
|
|
20
|
+
# Uses install.sh from the same directory as this script instead of CDN.
|
|
21
21
|
|
|
22
22
|
param(
|
|
23
23
|
[switch]$Local,
|
|
@@ -33,7 +33,7 @@ $global:DisplayCmd = if ($CommandName) { $CommandName } else { "openclacky" }
|
|
|
33
33
|
|
|
34
34
|
$CLACKY_CDN_BASE_URL = "https://oss.1024code.com"
|
|
35
35
|
$INSTALL_PS1_COMMAND = "powershell -c `"irm $CLACKY_CDN_BASE_URL/clacky-ai/openclacky/main/scripts/install.ps1 | iex`""
|
|
36
|
-
$INSTALL_SCRIPT_URL = "$CLACKY_CDN_BASE_URL/clacky-ai/openclacky/main/scripts/
|
|
36
|
+
$INSTALL_SCRIPT_URL = "$CLACKY_CDN_BASE_URL/clacky-ai/openclacky/main/scripts/install.sh"
|
|
37
37
|
$UBUNTU_WSL_AMD64_URL = "$CLACKY_CDN_BASE_URL/ubuntu-jammy-wsl-amd64-ubuntu22.04lts.rootfs.tar.gz"
|
|
38
38
|
$UBUNTU_WSL_AMD64_SHA256_URL = "$CLACKY_CDN_BASE_URL/ubuntu-jammy-wsl-amd64-ubuntu22.04lts.rootfs.tar.gz.sha256"
|
|
39
39
|
$UBUNTU_WSL_ARM64_URL = "$CLACKY_CDN_BASE_URL/ubuntu-jammy-wsl-arm64-ubuntu22.04lts.rootfs.tar.gz"
|
|
@@ -245,9 +245,9 @@ function Run-InstallInWsl {
|
|
|
245
245
|
if ($Local) {
|
|
246
246
|
# Convert Windows path to WSL path (e.g. C:\foo\bar -> /mnt/c/foo/bar)
|
|
247
247
|
$scriptDir = Split-Path -Parent $MyInvocation.PSCommandPath
|
|
248
|
-
$localScript = Join-Path $scriptDir "
|
|
248
|
+
$localScript = Join-Path $scriptDir "install.sh"
|
|
249
249
|
if (-not (Test-Path $localScript)) {
|
|
250
|
-
Write-Fail "Local mode:
|
|
250
|
+
Write-Fail "Local mode: install.sh not found at $localScript"
|
|
251
251
|
exit 1
|
|
252
252
|
}
|
|
253
253
|
$wslPath = ($localScript -replace '\', '/') -replace '^([A-Za-z]):', { '/mnt/' + $args[0].Groups[1].Value.ToLower() }
|
data/scripts/install.sh
CHANGED
|
@@ -368,9 +368,19 @@ ensure_ruby() {
|
|
|
368
368
|
return 0
|
|
369
369
|
fi
|
|
370
370
|
|
|
371
|
-
#
|
|
371
|
+
# Linux: try apt first (fast, Ubuntu 22.04 ships Ruby 3.0)
|
|
372
|
+
if [ "$OS" = "Linux" ] && ([ "$DISTRO" = "ubuntu" ] || [ "$DISTRO" = "debian" ]); then
|
|
373
|
+
print_info "Installing Ruby via apt..."
|
|
374
|
+
if sudo apt-get install -y ruby ruby-dev 2>/dev/null && check_ruby; then
|
|
375
|
+
return 0
|
|
376
|
+
fi
|
|
377
|
+
print_warning "apt Ruby install failed or version too old, falling back to mise..."
|
|
378
|
+
fi
|
|
379
|
+
|
|
380
|
+
# Fallback: install via mise (compiles from source)
|
|
372
381
|
print_step "Installing Ruby via mise..."
|
|
373
382
|
detect_shell
|
|
383
|
+
install_linux_build_deps
|
|
374
384
|
|
|
375
385
|
if ! install_mise; then
|
|
376
386
|
return 1
|
|
@@ -390,29 +400,37 @@ ensure_ruby() {
|
|
|
390
400
|
}
|
|
391
401
|
|
|
392
402
|
# --------------------------------------------------------------------------
|
|
393
|
-
# Linux:
|
|
403
|
+
# Linux: configure apt mirror + update (always runs before any apt install)
|
|
394
404
|
# --------------------------------------------------------------------------
|
|
395
|
-
|
|
396
|
-
if [ "$DISTRO"
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
sudo tee /etc/apt/sources.list > /dev/null <<EOF
|
|
405
|
+
setup_apt_mirror() {
|
|
406
|
+
if [ "$DISTRO" != "ubuntu" ] && [ "$DISTRO" != "debian" ]; then return 0; fi
|
|
407
|
+
|
|
408
|
+
if [ "$USE_CN_MIRRORS" = true ]; then
|
|
409
|
+
print_info "Configuring apt mirror (Aliyun)..."
|
|
410
|
+
local codename="${VERSION_CODENAME:-jammy}"
|
|
411
|
+
local mirror_base="https://mirrors.aliyun.com/ubuntu/"
|
|
412
|
+
local components="main restricted universe multiverse"
|
|
413
|
+
sudo tee /etc/apt/sources.list > /dev/null <<EOF
|
|
405
414
|
deb ${mirror_base} ${codename} ${components}
|
|
406
415
|
deb ${mirror_base} ${codename}-updates ${components}
|
|
407
416
|
deb ${mirror_base} ${codename}-backports ${components}
|
|
408
417
|
deb ${mirror_base} ${codename}-security ${components}
|
|
409
418
|
EOF
|
|
410
|
-
fi
|
|
411
|
-
|
|
412
|
-
sudo apt update
|
|
413
|
-
sudo apt install -y build-essential libssl-dev libyaml-dev zlib1g-dev libgmp-dev git
|
|
414
|
-
print_success "Build dependencies installed"
|
|
415
419
|
fi
|
|
420
|
+
|
|
421
|
+
sudo apt-get update -qq
|
|
422
|
+
print_success "apt updated"
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
# --------------------------------------------------------------------------
|
|
426
|
+
# Linux: install build deps (only needed when compiling Ruby via mise)
|
|
427
|
+
# --------------------------------------------------------------------------
|
|
428
|
+
install_linux_build_deps() {
|
|
429
|
+
if [ "$DISTRO" != "ubuntu" ] && [ "$DISTRO" != "debian" ]; then return 0; fi
|
|
430
|
+
|
|
431
|
+
print_step "Installing build dependencies..."
|
|
432
|
+
sudo apt-get install -y build-essential libssl-dev libyaml-dev zlib1g-dev libgmp-dev git
|
|
433
|
+
print_success "Build dependencies installed"
|
|
416
434
|
}
|
|
417
435
|
|
|
418
436
|
# --------------------------------------------------------------------------
|
|
@@ -580,10 +598,10 @@ main() {
|
|
|
580
598
|
detect_shell
|
|
581
599
|
detect_network_region
|
|
582
600
|
|
|
583
|
-
# Linux:
|
|
601
|
+
# Linux: configure apt mirror + update (always runs; build deps deferred to mise fallback)
|
|
584
602
|
if [ "$OS" = "Linux" ]; then
|
|
585
603
|
if [ "$DISTRO" = "ubuntu" ] || [ "$DISTRO" = "debian" ]; then
|
|
586
|
-
|
|
604
|
+
setup_apt_mirror
|
|
587
605
|
else
|
|
588
606
|
print_error "Unsupported Linux distribution: $DISTRO"
|
|
589
607
|
print_info "Please install Ruby >= 2.6.0 manually and run: gem install openclacky"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: openclacky
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.9.
|
|
4
|
+
version: 0.9.21
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- windy
|
|
@@ -368,6 +368,7 @@ files:
|
|
|
368
368
|
- lib/clacky/default_skills/skill-creator/scripts/run_eval.py
|
|
369
369
|
- lib/clacky/default_skills/skill-creator/scripts/run_loop.py
|
|
370
370
|
- lib/clacky/default_skills/skill-creator/scripts/utils.py
|
|
371
|
+
- lib/clacky/default_skills/skill-creator/scripts/validate_skill_frontmatter.rb
|
|
371
372
|
- lib/clacky/idle_compression_timer.rb
|
|
372
373
|
- lib/clacky/json_ui_controller.rb
|
|
373
374
|
- lib/clacky/message_format/anthropic.rb
|
|
@@ -482,7 +483,6 @@ files:
|
|
|
482
483
|
- scripts/install.ps1
|
|
483
484
|
- scripts/install.sh
|
|
484
485
|
- scripts/install_full.sh
|
|
485
|
-
- scripts/install_simple.sh
|
|
486
486
|
- scripts/uninstall.sh
|
|
487
487
|
- sig/clacky.rbs
|
|
488
488
|
homepage: https://github.com/yafeilee/clacky
|
data/scripts/install_simple.sh
DELETED
|
@@ -1,630 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# OpenClacky Installation Script
|
|
3
|
-
# This script automatically detects your system and installs OpenClacky
|
|
4
|
-
|
|
5
|
-
set -e
|
|
6
|
-
|
|
7
|
-
# Brand configuration (populated by --brand-name / --command flags)
|
|
8
|
-
BRAND_NAME=""
|
|
9
|
-
BRAND_COMMAND=""
|
|
10
|
-
|
|
11
|
-
# Colors for output
|
|
12
|
-
RED='\033[0;31m'
|
|
13
|
-
GREEN='\033[0;32m'
|
|
14
|
-
YELLOW='\033[1;33m'
|
|
15
|
-
BLUE='\033[0;34m'
|
|
16
|
-
NC='\033[0m' # No Color
|
|
17
|
-
|
|
18
|
-
print_info() {
|
|
19
|
-
echo -e "${BLUE}ℹ${NC} $1"
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
print_success() {
|
|
23
|
-
echo -e "${GREEN}✓${NC} $1"
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
print_warning() {
|
|
27
|
-
echo -e "${YELLOW}⚠${NC} $1"
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
print_error() {
|
|
31
|
-
echo -e "${RED}✗${NC} $1"
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
print_step() {
|
|
35
|
-
echo -e "\n${BLUE}==>${NC} $1"
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
# Detect OS
|
|
39
|
-
detect_os() {
|
|
40
|
-
case "$(uname -s)" in
|
|
41
|
-
Linux*) OS=Linux;;
|
|
42
|
-
Darwin*) OS=macOS;;
|
|
43
|
-
CYGWIN*) OS=Windows;;
|
|
44
|
-
MINGW*) OS=Windows;;
|
|
45
|
-
*) OS=Unknown;;
|
|
46
|
-
esac
|
|
47
|
-
print_info "Detected OS: $OS"
|
|
48
|
-
|
|
49
|
-
if [ "$OS" = "Linux" ]; then
|
|
50
|
-
if [ -f /etc/os-release ]; then
|
|
51
|
-
. /etc/os-release
|
|
52
|
-
DISTRO=$ID
|
|
53
|
-
print_info "Detected Linux distribution: $DISTRO"
|
|
54
|
-
else
|
|
55
|
-
DISTRO=unknown
|
|
56
|
-
fi
|
|
57
|
-
fi
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
# Check if command exists
|
|
61
|
-
command_exists() {
|
|
62
|
-
command -v "$1" >/dev/null 2>&1
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
# Detect current shell type
|
|
66
|
-
detect_shell() {
|
|
67
|
-
local shell_name
|
|
68
|
-
shell_name=$(basename "$SHELL")
|
|
69
|
-
|
|
70
|
-
case "$shell_name" in
|
|
71
|
-
zsh)
|
|
72
|
-
CURRENT_SHELL="zsh"
|
|
73
|
-
SHELL_RC="$HOME/.zshrc"
|
|
74
|
-
;;
|
|
75
|
-
bash)
|
|
76
|
-
CURRENT_SHELL="bash"
|
|
77
|
-
if [ "$OS" = "macOS" ]; then
|
|
78
|
-
SHELL_RC="$HOME/.bash_profile"
|
|
79
|
-
else
|
|
80
|
-
SHELL_RC="$HOME/.bashrc"
|
|
81
|
-
fi
|
|
82
|
-
;;
|
|
83
|
-
fish)
|
|
84
|
-
CURRENT_SHELL="fish"
|
|
85
|
-
SHELL_RC="$HOME/.config/fish/config.fish"
|
|
86
|
-
;;
|
|
87
|
-
*)
|
|
88
|
-
CURRENT_SHELL="bash"
|
|
89
|
-
SHELL_RC="$HOME/.bashrc"
|
|
90
|
-
;;
|
|
91
|
-
esac
|
|
92
|
-
|
|
93
|
-
print_info "Detected shell: $CURRENT_SHELL (rc file: $SHELL_RC)"
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
# --------------------------------------------------------------------------
|
|
97
|
-
# Network region detection & mirror selection
|
|
98
|
-
# --------------------------------------------------------------------------
|
|
99
|
-
SLOW_THRESHOLD_MS=5000
|
|
100
|
-
NETWORK_REGION="global"
|
|
101
|
-
USE_CN_MIRRORS=false
|
|
102
|
-
|
|
103
|
-
GITHUB_RAW_BASE_URL="https://raw.githubusercontent.com"
|
|
104
|
-
DEFAULT_RUBYGEMS_URL="https://rubygems.org"
|
|
105
|
-
DEFAULT_MISE_INSTALL_URL="https://mise.run"
|
|
106
|
-
|
|
107
|
-
CN_CDN_BASE_URL="https://oss.1024code.com"
|
|
108
|
-
CN_MISE_INSTALL_URL="${CN_CDN_BASE_URL}/mise.sh"
|
|
109
|
-
CN_RUBY_PRECOMPILED_URL="${CN_CDN_BASE_URL}/ruby/ruby-{version}.{platform}.tar.gz"
|
|
110
|
-
CN_RUBYGEMS_URL="https://mirrors.aliyun.com/rubygems/"
|
|
111
|
-
CN_GEM_BASE_URL="${CN_CDN_BASE_URL}/openclacky"
|
|
112
|
-
CN_GEM_LATEST_URL="${CN_GEM_BASE_URL}/latest.txt"
|
|
113
|
-
|
|
114
|
-
# Active values (overridden by detect_network_region)
|
|
115
|
-
MISE_INSTALL_URL="$DEFAULT_MISE_INSTALL_URL"
|
|
116
|
-
RUBYGEMS_INSTALL_URL="$DEFAULT_RUBYGEMS_URL"
|
|
117
|
-
RUBY_VERSION_SPEC="ruby@3" # mise will pick latest stable
|
|
118
|
-
|
|
119
|
-
_probe_url() {
|
|
120
|
-
local url="$1"
|
|
121
|
-
local timeout_sec=5
|
|
122
|
-
local curl_output http_code total_time elapsed_ms
|
|
123
|
-
|
|
124
|
-
curl_output=$(curl -s -o /dev/null -w "%{http_code} %{time_total}" \
|
|
125
|
-
--connect-timeout "$timeout_sec" \
|
|
126
|
-
--max-time "$timeout_sec" \
|
|
127
|
-
"$url" 2>/dev/null) || true
|
|
128
|
-
http_code="${curl_output%% *}"
|
|
129
|
-
total_time="${curl_output#* }"
|
|
130
|
-
|
|
131
|
-
if [ -z "$http_code" ] || [ "$http_code" = "000" ] || [ "$http_code" = "$curl_output" ]; then
|
|
132
|
-
echo "timeout"
|
|
133
|
-
else
|
|
134
|
-
elapsed_ms=$(awk -v seconds="$total_time" 'BEGIN { printf "%d", seconds * 1000 }')
|
|
135
|
-
echo "$elapsed_ms"
|
|
136
|
-
fi
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
_is_slow_or_unreachable() {
|
|
140
|
-
local result="$1"
|
|
141
|
-
if [ "$result" = "timeout" ]; then
|
|
142
|
-
return 0
|
|
143
|
-
fi
|
|
144
|
-
[ "$result" -ge "$SLOW_THRESHOLD_MS" ] 2>/dev/null
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
_format_probe_time() {
|
|
148
|
-
local result="$1"
|
|
149
|
-
if [ "$result" = "timeout" ]; then
|
|
150
|
-
echo "timeout"
|
|
151
|
-
else
|
|
152
|
-
awk -v ms="$result" 'BEGIN { printf "%.1fs", ms / 1000 }'
|
|
153
|
-
fi
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
_print_probe_result() {
|
|
157
|
-
local label="$1"
|
|
158
|
-
local result="$2"
|
|
159
|
-
|
|
160
|
-
if [ "$result" = "timeout" ]; then
|
|
161
|
-
print_warning "UNREACHABLE ${label}"
|
|
162
|
-
elif _is_slow_or_unreachable "$result"; then
|
|
163
|
-
print_warning "SLOW ($(_format_probe_time "$result")) ${label}"
|
|
164
|
-
else
|
|
165
|
-
print_success "OK ($(_format_probe_time "$result")) ${label}"
|
|
166
|
-
fi
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
_probe_url_with_retry() {
|
|
170
|
-
local url="$1"
|
|
171
|
-
local max_retries="${2:-2}"
|
|
172
|
-
local result
|
|
173
|
-
|
|
174
|
-
for _ in $(seq 1 "$max_retries"); do
|
|
175
|
-
result=$(_probe_url "$url")
|
|
176
|
-
if ! _is_slow_or_unreachable "$result"; then
|
|
177
|
-
echo "$result"
|
|
178
|
-
return 0
|
|
179
|
-
fi
|
|
180
|
-
done
|
|
181
|
-
echo "$result"
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
detect_network_region() {
|
|
185
|
-
print_step "Network pre-flight check..."
|
|
186
|
-
echo ""
|
|
187
|
-
|
|
188
|
-
print_info "Detecting network region..."
|
|
189
|
-
local google_result baidu_result
|
|
190
|
-
google_result=$(_probe_url "https://www.google.com")
|
|
191
|
-
baidu_result=$(_probe_url "https://www.baidu.com")
|
|
192
|
-
|
|
193
|
-
_print_probe_result "google.com" "$google_result"
|
|
194
|
-
_print_probe_result "baidu.com" "$baidu_result"
|
|
195
|
-
|
|
196
|
-
local google_ok=false baidu_ok=false
|
|
197
|
-
! _is_slow_or_unreachable "$google_result" && google_ok=true
|
|
198
|
-
! _is_slow_or_unreachable "$baidu_result" && baidu_ok=true
|
|
199
|
-
|
|
200
|
-
if [ "$google_ok" = true ]; then
|
|
201
|
-
NETWORK_REGION="global"
|
|
202
|
-
print_success "Region: global"
|
|
203
|
-
elif [ "$baidu_ok" = true ]; then
|
|
204
|
-
NETWORK_REGION="china"
|
|
205
|
-
print_success "Region: china"
|
|
206
|
-
else
|
|
207
|
-
NETWORK_REGION="unknown"
|
|
208
|
-
print_warning "Region: unknown (both unreachable)"
|
|
209
|
-
fi
|
|
210
|
-
echo ""
|
|
211
|
-
|
|
212
|
-
if [ "$NETWORK_REGION" = "china" ]; then
|
|
213
|
-
local cdn_result mirror_result
|
|
214
|
-
cdn_result=$(_probe_url_with_retry "$CN_MISE_INSTALL_URL")
|
|
215
|
-
mirror_result=$(_probe_url_with_retry "$CN_RUBYGEMS_URL")
|
|
216
|
-
|
|
217
|
-
_print_probe_result "CN CDN (mise/Ruby)" "$cdn_result"
|
|
218
|
-
_print_probe_result "Aliyun (gem)" "$mirror_result"
|
|
219
|
-
|
|
220
|
-
local cdn_ok=false mirror_ok=false
|
|
221
|
-
! _is_slow_or_unreachable "$cdn_result" && cdn_ok=true
|
|
222
|
-
! _is_slow_or_unreachable "$mirror_result" && mirror_ok=true
|
|
223
|
-
|
|
224
|
-
if [ "$cdn_ok" = true ] || [ "$mirror_ok" = true ]; then
|
|
225
|
-
USE_CN_MIRRORS=true
|
|
226
|
-
MISE_INSTALL_URL="$CN_MISE_INSTALL_URL"
|
|
227
|
-
RUBYGEMS_INSTALL_URL="$CN_RUBYGEMS_URL"
|
|
228
|
-
RUBY_VERSION_SPEC="ruby@3.4.8"
|
|
229
|
-
print_info "CN mirrors applied."
|
|
230
|
-
else
|
|
231
|
-
print_warning "CN mirrors unreachable — falling back to global sources."
|
|
232
|
-
fi
|
|
233
|
-
else
|
|
234
|
-
local rubygems_result mise_result
|
|
235
|
-
rubygems_result=$(_probe_url_with_retry "$DEFAULT_RUBYGEMS_URL")
|
|
236
|
-
mise_result=$(_probe_url_with_retry "$DEFAULT_MISE_INSTALL_URL")
|
|
237
|
-
|
|
238
|
-
_print_probe_result "RubyGems" "$rubygems_result"
|
|
239
|
-
_print_probe_result "mise.run" "$mise_result"
|
|
240
|
-
|
|
241
|
-
_is_slow_or_unreachable "$rubygems_result" && print_warning "RubyGems is slow/unreachable."
|
|
242
|
-
_is_slow_or_unreachable "$mise_result" && print_warning "mise.run is slow/unreachable."
|
|
243
|
-
fi
|
|
244
|
-
|
|
245
|
-
echo ""
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
# --------------------------------------------------------------------------
|
|
249
|
-
# Version comparison
|
|
250
|
-
# --------------------------------------------------------------------------
|
|
251
|
-
version_ge() {
|
|
252
|
-
printf '%s\n%s\n' "$2" "$1" | sort -V -C
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
# --------------------------------------------------------------------------
|
|
256
|
-
# Ruby check (>= 2.6.0)
|
|
257
|
-
# --------------------------------------------------------------------------
|
|
258
|
-
check_ruby() {
|
|
259
|
-
if command_exists ruby; then
|
|
260
|
-
RUBY_VERSION=$(ruby -e 'puts RUBY_VERSION' 2>/dev/null)
|
|
261
|
-
print_info "Found Ruby $RUBY_VERSION"
|
|
262
|
-
|
|
263
|
-
if version_ge "$RUBY_VERSION" "2.6.0"; then
|
|
264
|
-
print_success "Ruby $RUBY_VERSION is compatible (>= 2.6.0)"
|
|
265
|
-
return 0
|
|
266
|
-
else
|
|
267
|
-
print_warning "Ruby $RUBY_VERSION is too old (need >= 2.6.0)"
|
|
268
|
-
return 1
|
|
269
|
-
fi
|
|
270
|
-
else
|
|
271
|
-
print_warning "Ruby not found"
|
|
272
|
-
return 1
|
|
273
|
-
fi
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
# --------------------------------------------------------------------------
|
|
277
|
-
# Configure gem source (CN mirror if needed)
|
|
278
|
-
# --------------------------------------------------------------------------
|
|
279
|
-
configure_gem_source() {
|
|
280
|
-
local gemrc="$HOME/.gemrc"
|
|
281
|
-
|
|
282
|
-
if [ "$USE_CN_MIRRORS" = true ]; then
|
|
283
|
-
# CN: point gem source to Aliyun mirror
|
|
284
|
-
if [ -f "$gemrc" ] && grep -q "${CN_RUBYGEMS_URL}" "$gemrc" 2>/dev/null; then
|
|
285
|
-
print_success "gem source already → ${CN_RUBYGEMS_URL}"
|
|
286
|
-
else
|
|
287
|
-
[ -f "$gemrc" ] && mv "$gemrc" "$HOME/.gemrc_clackybak"
|
|
288
|
-
cat > "$gemrc" <<GEMRC
|
|
289
|
-
:sources:
|
|
290
|
-
- ${CN_RUBYGEMS_URL}
|
|
291
|
-
GEMRC
|
|
292
|
-
print_success "gem source → ${CN_RUBYGEMS_URL}"
|
|
293
|
-
fi
|
|
294
|
-
else
|
|
295
|
-
# Global: restore original gemrc if we were the ones who changed it
|
|
296
|
-
if [ -f "$gemrc" ] && grep -q "${CN_RUBYGEMS_URL}" "$gemrc" 2>/dev/null; then
|
|
297
|
-
if [ -f "$HOME/.gemrc_clackybak" ]; then
|
|
298
|
-
mv "$HOME/.gemrc_clackybak" "$gemrc"
|
|
299
|
-
print_info "gem source restored from backup"
|
|
300
|
-
else
|
|
301
|
-
rm "$gemrc"
|
|
302
|
-
print_info "gem source restored to default"
|
|
303
|
-
fi
|
|
304
|
-
fi
|
|
305
|
-
fi
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
# --------------------------------------------------------------------------
|
|
309
|
-
# mise: install (if missing) and optionally activate
|
|
310
|
-
# --------------------------------------------------------------------------
|
|
311
|
-
install_mise() {
|
|
312
|
-
if ! command_exists mise && [ ! -x "$HOME/.local/bin/mise" ]; then
|
|
313
|
-
print_info "Installing mise..."
|
|
314
|
-
if curl -fsSL "$MISE_INSTALL_URL" | sh; then
|
|
315
|
-
# Add mise activation to shell rc
|
|
316
|
-
local mise_init_line='eval "$(~/.local/bin/mise activate '"$CURRENT_SHELL"')"'
|
|
317
|
-
if [ -f "$SHELL_RC" ]; then
|
|
318
|
-
echo "$mise_init_line" >> "$SHELL_RC"
|
|
319
|
-
else
|
|
320
|
-
echo "$mise_init_line" > "$SHELL_RC"
|
|
321
|
-
fi
|
|
322
|
-
print_info "Added mise activation to $SHELL_RC"
|
|
323
|
-
export PATH="$HOME/.local/bin:$PATH"
|
|
324
|
-
eval "$(~/.local/bin/mise activate bash)"
|
|
325
|
-
print_success "mise installed"
|
|
326
|
-
else
|
|
327
|
-
print_error "Failed to install mise"
|
|
328
|
-
return 1
|
|
329
|
-
fi
|
|
330
|
-
else
|
|
331
|
-
export PATH="$HOME/.local/bin:$PATH"
|
|
332
|
-
eval "$(~/.local/bin/mise activate bash)" 2>/dev/null || true
|
|
333
|
-
print_success "mise already installed"
|
|
334
|
-
fi
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
# --------------------------------------------------------------------------
|
|
338
|
-
# Install Ruby via mise (precompiled when CN mirrors active)
|
|
339
|
-
# --------------------------------------------------------------------------
|
|
340
|
-
install_ruby_via_mise() {
|
|
341
|
-
print_info "Installing Ruby via mise ($RUBY_VERSION_SPEC)..."
|
|
342
|
-
|
|
343
|
-
if [ "$USE_CN_MIRRORS" = true ]; then
|
|
344
|
-
~/.local/bin/mise settings ruby.compile=false
|
|
345
|
-
~/.local/bin/mise settings ruby.precompiled_url="$CN_RUBY_PRECOMPILED_URL"
|
|
346
|
-
print_info "Using precompiled Ruby from CN CDN"
|
|
347
|
-
else
|
|
348
|
-
~/.local/bin/mise settings unset ruby.compile >/dev/null 2>&1 || true
|
|
349
|
-
~/.local/bin/mise settings unset ruby.precompiled_url >/dev/null 2>&1 || true
|
|
350
|
-
fi
|
|
351
|
-
|
|
352
|
-
if ~/.local/bin/mise use -g "$RUBY_VERSION_SPEC"; then
|
|
353
|
-
eval "$(~/.local/bin/mise activate bash)"
|
|
354
|
-
print_success "Ruby installed via mise"
|
|
355
|
-
else
|
|
356
|
-
print_error "Failed to install Ruby via mise"
|
|
357
|
-
return 1
|
|
358
|
-
fi
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
# --------------------------------------------------------------------------
|
|
362
|
-
# Ensure Ruby >= 2.6 is available (install if needed)
|
|
363
|
-
# --------------------------------------------------------------------------
|
|
364
|
-
ensure_ruby() {
|
|
365
|
-
print_step "Checking Ruby..."
|
|
366
|
-
|
|
367
|
-
if check_ruby; then
|
|
368
|
-
return 0
|
|
369
|
-
fi
|
|
370
|
-
|
|
371
|
-
# Linux: try apt first (fast, Ubuntu 22.04 ships Ruby 3.0)
|
|
372
|
-
if [ "$OS" = "Linux" ] && ([ "$DISTRO" = "ubuntu" ] || [ "$DISTRO" = "debian" ]); then
|
|
373
|
-
print_info "Installing Ruby via apt..."
|
|
374
|
-
if sudo apt-get install -y ruby ruby-dev 2>/dev/null && check_ruby; then
|
|
375
|
-
return 0
|
|
376
|
-
fi
|
|
377
|
-
print_warning "apt Ruby install failed or version too old, falling back to mise..."
|
|
378
|
-
fi
|
|
379
|
-
|
|
380
|
-
# Fallback: install via mise (compiles from source)
|
|
381
|
-
print_step "Installing Ruby via mise..."
|
|
382
|
-
detect_shell
|
|
383
|
-
install_linux_build_deps
|
|
384
|
-
|
|
385
|
-
if ! install_mise; then
|
|
386
|
-
return 1
|
|
387
|
-
fi
|
|
388
|
-
|
|
389
|
-
if ! install_ruby_via_mise; then
|
|
390
|
-
return 1
|
|
391
|
-
fi
|
|
392
|
-
|
|
393
|
-
# Verify
|
|
394
|
-
if check_ruby; then
|
|
395
|
-
return 0
|
|
396
|
-
else
|
|
397
|
-
print_error "Ruby installation verification failed"
|
|
398
|
-
return 1
|
|
399
|
-
fi
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
# --------------------------------------------------------------------------
|
|
403
|
-
# Linux: configure apt mirror + update (always runs before any apt install)
|
|
404
|
-
# --------------------------------------------------------------------------
|
|
405
|
-
setup_apt_mirror() {
|
|
406
|
-
if [ "$DISTRO" != "ubuntu" ] && [ "$DISTRO" != "debian" ]; then return 0; fi
|
|
407
|
-
|
|
408
|
-
if [ "$USE_CN_MIRRORS" = true ]; then
|
|
409
|
-
print_info "Configuring apt mirror (Aliyun)..."
|
|
410
|
-
local codename="${VERSION_CODENAME:-jammy}"
|
|
411
|
-
local mirror_base="https://mirrors.aliyun.com/ubuntu/"
|
|
412
|
-
local components="main restricted universe multiverse"
|
|
413
|
-
sudo tee /etc/apt/sources.list > /dev/null <<EOF
|
|
414
|
-
deb ${mirror_base} ${codename} ${components}
|
|
415
|
-
deb ${mirror_base} ${codename}-updates ${components}
|
|
416
|
-
deb ${mirror_base} ${codename}-backports ${components}
|
|
417
|
-
deb ${mirror_base} ${codename}-security ${components}
|
|
418
|
-
EOF
|
|
419
|
-
fi
|
|
420
|
-
|
|
421
|
-
sudo apt-get update -qq
|
|
422
|
-
print_success "apt updated"
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
# --------------------------------------------------------------------------
|
|
426
|
-
# Linux: install build deps (only needed when compiling Ruby via mise)
|
|
427
|
-
# --------------------------------------------------------------------------
|
|
428
|
-
install_linux_build_deps() {
|
|
429
|
-
if [ "$DISTRO" != "ubuntu" ] && [ "$DISTRO" != "debian" ]; then return 0; fi
|
|
430
|
-
|
|
431
|
-
print_step "Installing build dependencies..."
|
|
432
|
-
sudo apt-get install -y build-essential libssl-dev libyaml-dev zlib1g-dev libgmp-dev git
|
|
433
|
-
print_success "Build dependencies installed"
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
# --------------------------------------------------------------------------
|
|
437
|
-
# Set GEM_HOME to user directory when system Ruby gem dir is not writable
|
|
438
|
-
# (avoids needing sudo for gem install)
|
|
439
|
-
# --------------------------------------------------------------------------
|
|
440
|
-
setup_gem_home() {
|
|
441
|
-
local gem_dir
|
|
442
|
-
gem_dir=$(gem environment gemdir 2>/dev/null || true)
|
|
443
|
-
|
|
444
|
-
# Gem dir is writable (e.g. mise-managed Ruby) — nothing to do
|
|
445
|
-
if [ -w "$gem_dir" ]; then
|
|
446
|
-
return 0
|
|
447
|
-
fi
|
|
448
|
-
|
|
449
|
-
# System Ruby: redirect gems to ~/.gem/ruby/<api_version>
|
|
450
|
-
local ruby_api
|
|
451
|
-
ruby_api=$(ruby -e 'puts RbConfig::CONFIG["ruby_version"]' 2>/dev/null)
|
|
452
|
-
|
|
453
|
-
export GEM_HOME="$HOME/.gem/ruby/${ruby_api}"
|
|
454
|
-
export GEM_PATH="$HOME/.gem/ruby/${ruby_api}"
|
|
455
|
-
export PATH="$HOME/.local/bin:$HOME/.gem/ruby/${ruby_api}/bin:$PATH"
|
|
456
|
-
|
|
457
|
-
print_info "System Ruby detected — gems will install to ~/.gem/ruby/${ruby_api}"
|
|
458
|
-
|
|
459
|
-
# Persist to shell rc (use $HOME so the line is portable)
|
|
460
|
-
# Also add ~/.local/bin so brand wrapper commands installed there are found
|
|
461
|
-
if [ -n "$SHELL_RC" ] && ! grep -q "GEM_HOME" "$SHELL_RC" 2>/dev/null; then
|
|
462
|
-
{
|
|
463
|
-
echo ""
|
|
464
|
-
echo "# Ruby user gem dir (added by openclacky installer)"
|
|
465
|
-
echo "export GEM_HOME=\"\$HOME/.gem/ruby/${ruby_api}\""
|
|
466
|
-
echo "export GEM_PATH=\"\$HOME/.gem/ruby/${ruby_api}\""
|
|
467
|
-
echo "export PATH=\"\$HOME/.local/bin:\$HOME/.gem/ruby/${ruby_api}/bin:\$PATH\""
|
|
468
|
-
} >> "$SHELL_RC"
|
|
469
|
-
print_info "GEM_HOME written to $SHELL_RC"
|
|
470
|
-
fi
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
# --------------------------------------------------------------------------
|
|
474
|
-
# gem install openclacky
|
|
475
|
-
# --------------------------------------------------------------------------
|
|
476
|
-
install_via_gem() {
|
|
477
|
-
print_step "Installing ${DISPLAY_NAME} via gem..."
|
|
478
|
-
|
|
479
|
-
configure_gem_source
|
|
480
|
-
setup_gem_home
|
|
481
|
-
|
|
482
|
-
if [ "$USE_CN_MIRRORS" = true ]; then
|
|
483
|
-
# CN: download .gem from OSS, install dependencies from Aliyun mirror
|
|
484
|
-
print_info "Fetching latest version from OSS..."
|
|
485
|
-
local cn_version
|
|
486
|
-
cn_version=$(curl -fsSL "$CN_GEM_LATEST_URL" | tr -d '[:space:]')
|
|
487
|
-
print_info "Latest version: ${cn_version}"
|
|
488
|
-
|
|
489
|
-
local gem_url="${CN_GEM_BASE_URL}/openclacky-${cn_version}.gem"
|
|
490
|
-
local gem_file="/tmp/openclacky-${cn_version}.gem"
|
|
491
|
-
print_info "Downloading openclacky-${cn_version}.gem from OSS..."
|
|
492
|
-
curl -fsSL "$gem_url" -o "$gem_file"
|
|
493
|
-
print_info "Installing gem and dependencies from Aliyun mirror..."
|
|
494
|
-
gem install "$gem_file" --no-document --source "$CN_RUBYGEMS_URL"
|
|
495
|
-
else
|
|
496
|
-
print_info "Installing gem and dependencies from RubyGems..."
|
|
497
|
-
gem install openclacky --no-document
|
|
498
|
-
fi
|
|
499
|
-
|
|
500
|
-
if [ $? -eq 0 ]; then
|
|
501
|
-
print_success "${DISPLAY_NAME} installed successfully!"
|
|
502
|
-
return 0
|
|
503
|
-
else
|
|
504
|
-
print_error "gem install failed"
|
|
505
|
-
return 1
|
|
506
|
-
fi
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
# --------------------------------------------------------------------------
|
|
510
|
-
# Parse CLI args
|
|
511
|
-
# --------------------------------------------------------------------------
|
|
512
|
-
parse_args() {
|
|
513
|
-
for arg in "$0" "$@"; do
|
|
514
|
-
case "$arg" in
|
|
515
|
-
--brand-name=*)
|
|
516
|
-
BRAND_NAME="${arg#--brand-name=}"
|
|
517
|
-
;;
|
|
518
|
-
--command=*)
|
|
519
|
-
BRAND_COMMAND="${arg#--command=}"
|
|
520
|
-
;;
|
|
521
|
-
esac
|
|
522
|
-
done
|
|
523
|
-
DISPLAY_NAME="${BRAND_NAME:-OpenClacky}"
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
# --------------------------------------------------------------------------
|
|
527
|
-
# Brand setup
|
|
528
|
-
# --------------------------------------------------------------------------
|
|
529
|
-
setup_brand() {
|
|
530
|
-
[ -z "$BRAND_NAME" ] && return 0
|
|
531
|
-
|
|
532
|
-
local clacky_dir="$HOME/.clacky"
|
|
533
|
-
local brand_file="$clacky_dir/brand.yml"
|
|
534
|
-
mkdir -p "$clacky_dir"
|
|
535
|
-
|
|
536
|
-
print_step "Configuring brand: $BRAND_NAME"
|
|
537
|
-
|
|
538
|
-
cat > "$brand_file" <<YAML
|
|
539
|
-
product_name: "${BRAND_NAME}"
|
|
540
|
-
package_name: "${BRAND_COMMAND}"
|
|
541
|
-
YAML
|
|
542
|
-
print_success "Brand config written to $brand_file"
|
|
543
|
-
|
|
544
|
-
if [ -n "$BRAND_COMMAND" ]; then
|
|
545
|
-
local bin_dir="$HOME/.local/bin"
|
|
546
|
-
mkdir -p "$bin_dir"
|
|
547
|
-
local wrapper="$bin_dir/$BRAND_COMMAND"
|
|
548
|
-
cat > "$wrapper" <<WRAPPER
|
|
549
|
-
#!/bin/sh
|
|
550
|
-
exec openclacky "\$@"
|
|
551
|
-
WRAPPER
|
|
552
|
-
chmod +x "$wrapper"
|
|
553
|
-
print_success "Wrapper installed: $wrapper"
|
|
554
|
-
|
|
555
|
-
case ":$PATH:" in
|
|
556
|
-
*":$bin_dir:"*) ;;
|
|
557
|
-
*)
|
|
558
|
-
print_warning "Add to your shell profile: export PATH=\"\$HOME/.local/bin:\$PATH\""
|
|
559
|
-
;;
|
|
560
|
-
esac
|
|
561
|
-
fi
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
# --------------------------------------------------------------------------
|
|
565
|
-
# Post-install info
|
|
566
|
-
# --------------------------------------------------------------------------
|
|
567
|
-
show_post_install_info() {
|
|
568
|
-
local cmd="${BRAND_COMMAND:-openclacky}"
|
|
569
|
-
|
|
570
|
-
echo ""
|
|
571
|
-
echo -e " ${GREEN}${DISPLAY_NAME} installed successfully!${NC}"
|
|
572
|
-
echo ""
|
|
573
|
-
echo " Reload your shell:"
|
|
574
|
-
echo ""
|
|
575
|
-
echo -e " ${YELLOW}source ${SHELL_RC}${NC}"
|
|
576
|
-
echo ""
|
|
577
|
-
echo -e " ${GREEN}Web UI${NC} (recommended):"
|
|
578
|
-
echo " $cmd server"
|
|
579
|
-
echo " Open http://localhost:7070"
|
|
580
|
-
echo ""
|
|
581
|
-
echo -e " ${GREEN}Terminal${NC}:"
|
|
582
|
-
echo " $cmd"
|
|
583
|
-
echo ""
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
# --------------------------------------------------------------------------
|
|
587
|
-
# Main
|
|
588
|
-
# --------------------------------------------------------------------------
|
|
589
|
-
main() {
|
|
590
|
-
parse_args "$@"
|
|
591
|
-
|
|
592
|
-
echo ""
|
|
593
|
-
echo "${DISPLAY_NAME} Installation"
|
|
594
|
-
echo ""
|
|
595
|
-
|
|
596
|
-
detect_os
|
|
597
|
-
detect_shell
|
|
598
|
-
detect_network_region
|
|
599
|
-
|
|
600
|
-
# Linux: configure apt mirror + update (always runs; build deps deferred to mise fallback)
|
|
601
|
-
if [ "$OS" = "Linux" ]; then
|
|
602
|
-
if [ "$DISTRO" = "ubuntu" ] || [ "$DISTRO" = "debian" ]; then
|
|
603
|
-
setup_apt_mirror
|
|
604
|
-
else
|
|
605
|
-
print_error "Unsupported Linux distribution: $DISTRO"
|
|
606
|
-
print_info "Please install Ruby >= 2.6.0 manually and run: gem install openclacky"
|
|
607
|
-
exit 1
|
|
608
|
-
fi
|
|
609
|
-
elif [ "$OS" != "macOS" ]; then
|
|
610
|
-
print_error "Unsupported OS: $OS"
|
|
611
|
-
print_info "Please install Ruby >= 2.6.0 manually and run: gem install openclacky"
|
|
612
|
-
exit 1
|
|
613
|
-
fi
|
|
614
|
-
|
|
615
|
-
if ! ensure_ruby; then
|
|
616
|
-
print_error "Could not install a compatible Ruby"
|
|
617
|
-
exit 1
|
|
618
|
-
fi
|
|
619
|
-
|
|
620
|
-
if install_via_gem; then
|
|
621
|
-
setup_brand
|
|
622
|
-
show_post_install_info
|
|
623
|
-
exit 0
|
|
624
|
-
else
|
|
625
|
-
print_error "Failed to install ${DISPLAY_NAME}"
|
|
626
|
-
exit 1
|
|
627
|
-
fi
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
main "$@"
|