Files
cli/tools/apply-config.sh
T
jiantw83 e655f9a963 feat(setup): 新增引導與自動設定技能
What: 新增 jsc-cli:setup,讀 doctor 的待修清單逐項確認後修復,並新增 tools/apply-config.sh 負責實際寫入。
Why: 體檢找得出問題,修還是得靠人一個一個查文件。修法又分三種:算得出來的、要人給值的、只能手動的,混在一起講不清楚。
How: 依規格表的 fix 欄分流,auto 直接寫、ask 先用決策樹問到值、manual 印步驟。複合修復交回原主(deploy、hooks-install、models)。apply-config.sh 只動 rc 檔的 # jsc-config 標記段落,寫前備份到 $JSC_HOME/backup/config/,寫後重讀驗證,fish 自動改用 set -gx 語法。
Who: 體檢與修復流程,接在 jsc-cli:doctor 之後。
2026-08-26 10:43:50 +08:00

159 lines
6.0 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env sh
# apply-config.sh — 把設定寫進 shell rc 檔或建立設定目錄(供 /jsc-cli:setup 呼叫)。
# 用法:
# apply-config.sh set {KEY} {VALUE} # 寫入或更新一個環境變數(export)
# apply-config.sh unset {KEY} # 從 jsc 段落移除一個環境變數
# apply-config.sh mkdir {PATH} # 建立目錄(值可帶 $VAR 與開頭的 ~)
# apply-config.sh show # 印出目前 jsc 段落的內容(每行 KEY<TAB>VALUE)
# apply-config.sh rcfiles # 印出這次會寫入的 rc 檔路徑
#
# 寫入位置:每個既有的 shell rc 檔(~/.bashrc、~/.zshrc、~/.config/fish/config.fish)
# 都寫一份,全部不存在時才建立 ~/.bashrc。內容一律收在標記段落之間:
# # jsc-config
# export KEY='值'
# # /jsc-config
# 段落整段重寫,重跑只取代不疊加。段落外的內容一律不動——rc 檔是使用者自己的檔案,
# jsc 只負責自己那一段。
#
# 動到任何檔案之前先原樣備份到 $JSC_HOME/backup/config/{yyyyMMdd_HHmmss}/,備份失敗就不寫入。
# fish 的語法與 POSIX shell 不同,寫進去的是 set -gx KEY 值。
#
# 結束碼: 0=成功 2=用法錯誤 4=備份或寫入失敗
set -eu
JSC_HOME="${JSC_HOME:-$HOME/.jsc}"
MARK_OPEN='# jsc-config'
MARK_SHUT='# /jsc-config'
TAB=$(printf '\t')
usage() {
echo "用法:apply-config.sh {set {KEY} {VALUE}|unset {KEY}|mkdir {PATH}|show|rcfiles}" >&2
exit 2
}
cmd="${1:-}"
case "$cmd" in set|unset|mkdir|show|rcfiles) ;; *) usage ;; esac
rc_files() {
found=""
for f in "$HOME/.bashrc" "$HOME/.zshrc" "$HOME/.config/fish/config.fish"; do
[ -f "$f" ] && { printf '%s\n' "$f"; found=1; }
done
[ -n "$found" ] || printf '%s\n' "$HOME/.bashrc"
}
if [ "$cmd" = rcfiles ]; then rc_files; exit 0; fi
# 目前段落裡的設定,格式 KEY<TAB>VALUE。取第一個既有 rc 檔為準:寫入時每個檔案內容相同。
read_pairs() {
for f in $(rc_files); do
[ -f "$f" ] || continue
awk -v o="$MARK_OPEN" -v s="$MARK_SHUT" '
$0==o { inb=1; next }
$0==s { inb=0; next }
inb {
line=$0
sub(/^export /, "", line) # POSIX shell
sub(/^set -gx /, "", line) # fish
if (line ~ /^[A-Za-z_][A-Za-z0-9_]*=/) {
eq=index(line, "="); k=substr(line, 1, eq-1); v=substr(line, eq+1)
} else {
sp=index(line, " "); if (sp==0) next
k=substr(line, 1, sp-1); v=substr(line, sp+1)
}
gsub(/^'"'"'|'"'"'$/, "", v)
if (k != "") print k "\t" v
}
' "$f"
return 0
done
}
if [ "$cmd" = show ]; then read_pairs; exit 0; fi
if [ "$cmd" = mkdir ]; then
raw="${2:-}"; [ -n "$raw" ] || usage
case "$raw" in "~/"*) raw="$HOME/${raw#\~/}" ;; esac
path=$( set +u; eval "printf '%s' \"$raw\"" )
[ -n "$path" ] || { echo "路徑展開後是空的:${2:-}" >&2; exit 4; }
mkdir -p "$path" 2>/dev/null || { echo "無法建立目錄:$path" >&2; exit 4; }
printf 'created\t%s\n' "$path"
exit 0
fi
key="${2:-}"
[ -n "$key" ] || usage
case "$key" in
[A-Za-z_]*) ;;
*) echo "變數名不合法:$key" >&2; exit 2 ;;
esac
value="${3:-}"
if [ "$cmd" = set ] && [ -z "$value" ]; then echo "set 需要值" >&2; exit 2; fi
# 合併:既有設定 + 這次的異動,重寫整段。少了這一步,寫第二個變數會蓋掉第一個。
pairs=$(mktemp) || { echo "無法建立暫存檔" >&2; exit 4; }
read_pairs > "$pairs" 2>/dev/null || true
merged=$(mktemp) || { rm -f "$pairs"; echo "無法建立暫存檔" >&2; exit 4; }
while IFS="$TAB" read -r k v; do
[ -n "${k:-}" ] || continue
[ "$k" = "$key" ] && continue
printf '%s\t%s\n' "$k" "$v" >> "$merged"
done < "$pairs"
rm -f "$pairs"
if [ "$cmd" = set ]; then printf '%s\t%s\n' "$key" "$value" >> "$merged"; fi
# 備份:動到的每個檔案先原樣複製一份,備份不了就整個不寫。
stamp=$(date +%Y%m%d_%H%M%S)
backup_dir="$JSC_HOME/backup/config/$stamp"
mkdir -p "$backup_dir" 2>/dev/null || { rm -f "$merged"; echo "無法建立備份目錄:$backup_dir" >&2; exit 4; }
# 依 rc 檔語法組出段落內容。fish 用 set -gx,其餘用 export。
block_for() {
_f="$1"
while IFS="$TAB" read -r k v; do
[ -n "${k:-}" ] || continue
case "$_f" in
*config.fish) printf "set -gx %s '%s'\n" "$k" "$v" ;;
*) printf "export %s='%s'\n" "$k" "$v" ;;
esac
done < "$merged"
}
rc_list=$(mktemp) || { rm -f "$merged"; echo "無法建立暫存檔" >&2; exit 4; }
rc_files > "$rc_list"
rc=0
while IFS= read -r f; do
[ -n "$f" ] || continue
if [ -f "$f" ]; then
cp -p "$f" "$backup_dir/$(basename "$f")" 2>/dev/null \
|| { echo "無法備份 $f,沒有備份就不寫入" >&2; rc=4; continue; }
else
mkdir -p "$(dirname "$f")" 2>/dev/null || { echo "無法建立 $(dirname "$f")" >&2; rc=4; continue; }
touch "$f" 2>/dev/null || { echo "無法建立 $f" >&2; rc=4; continue; }
fi
content=$(block_for "$f")
block=$(printf '%s\n%s\n%s' "$MARK_OPEN" "$content" "$MARK_SHUT")
if grep -qF "$MARK_OPEN" "$f" 2>/dev/null; then
awk -v o="$MARK_OPEN" -v s="$MARK_SHUT" -v b="$block" '
$0==o { print b; skip=1; next }
$0==s { skip=0; next }
skip { next }
{ print }
' "$f" > "$f.jsc-tmp" 2>/dev/null || { rm -f "$f.jsc-tmp"; echo "無法改寫 $f" >&2; rc=4; continue; }
mv "$f.jsc-tmp" "$f" 2>/dev/null || { rm -f "$f.jsc-tmp"; echo "無法覆寫 $f" >&2; rc=4; continue; }
else
( printf '\n%s\n' "$block" >> "$f" ) 2>/dev/null || { echo "無法寫入 $f" >&2; rc=4; continue; }
fi
# 寫完重讀驗證:說寫好了卻沒寫進去,是最難查的失敗
if [ "$cmd" = set ]; then
grep -qF "$key" "$f" 2>/dev/null || { echo "$f 寫入後讀不到 $key" >&2; rc=4; continue; }
fi
printf 'wrote\t%s\n' "$f"
done < "$rc_list"
rm -f "$rc_list" "$merged"
printf 'backup\t%s\n' "$backup_dir"
[ "$rc" = 0 ] || exit 4
printf 'note\t%s\n' "新的設定要開新的 shell 或重新 source rc 檔才生效;本輪工作階段可先手動 export"
exit 0