Skip to content

Commit 62ecd07

Browse files
authored
Merge pull request #13 from stack-wuh/feature/20260917-feature-install-cli-script
install-cli.sh:shadow-dev CLI 拉取/更新/回滚安装器
2 parents 296f66a + afc3587 commit 62ecd07

5 files changed

Lines changed: 436 additions & 3 deletions

File tree

‎README.md‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,26 @@ CLI 在 stderr 渲染一层人类提示:进场横幅(命令+参数)、收
5959

6060
## 安装与分发
6161

62-
本 CLI 随 [shadow-dev-workflow](https://github.com/stack-wuh/shadow-dev-workflow) 插件通过安装脚本分发:插件仓库执行 `scripts/install-cli.sh` 从本仓库 release 拉取目录产物。独立使用时克隆本仓库后直接 `node cli.mjs --help`。
62+
`scripts/install-cli.sh` 是唯一安装入口,供 [shadow-dev-workflow](https://github.com/stack-wuh/shadow-dev-workflow) 插件钩子与手工共用:
63+
64+
```bash
65+
bash scripts/install-cli.sh install # 拉取最新 release,物化+自校验+生成托管 shim
66+
bash scripts/install-cli.sh install --json # 插件钩子用:单行机器输出(幂等,已最新秒退)
67+
bash scripts/install-cli.sh status --json # 当前/上一版本指针
68+
bash scripts/install-cli.sh rollback # 切回上一版(离线,不触网)
69+
bash scripts/install-cli.sh install --from dist/shadow-dev-cli-v1.1.0.tar.gz # 离线安装
70+
```
71+
72+
- 布局:`~/.local/share/shadow-dev-cli/shadow-dev-cli-<ver>/` + `CURRENT`/`PREVIOUS` 指针文件;shim(`~/.local/bin/shadow-dev` 与 `.cmd`)运行时读指针——更新与回滚都不再改动 shim 文件。自定义位置用 `--prefix` / `--bin`。
73+
- 安全边界:发布前先物化并自跑 `help --json`,失败则指针不动(旧版本照常可用);shim 路径被**非托管**同名文件占用时告警退出、绝不覆盖;并发运行有锁(陈旧 10 分钟自动接管)。
74+
- 退出码:`0` 成功/已最新 · `1` 参数或冲突 · `2` 网络/GitHub API · `3` 产物自校验失败。信任边界为 HTTPS + GitHub 仓库,未做独立校验和。
75+
- 通道:默认 release(可复现);`--version v*` 锁版本;`--channel main` git 浅拉 rolling,仅供插件开发。依赖 bash + node(≥20) + tar(main 通道另需 git;curl 缺失自动退 wget),Windows 在 Git Bash 下运行。
76+
- 纯手工使用(不装 shim):克隆本仓库后直接 `node cli.mjs --help`。
6377

6478
## 开发
6579

6680
```bash
67-
npm test # node --test,47 个契约测试覆盖全部命令域、stderr 人用层与凭证链
81+
npm test # node --test,49 项 CLI 契约 + 6 项安装器契约(离线产物全链、冲突保护、回滚、自校验)
6882
```
6983

7084
行为契约:命令、JSON 输出结构、错误码、planHash 机制保持稳定;`test/cli.test.mjs` 是唯一契约规格。

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,6 @@
1010
"shadow-dev": "cli.mjs"
1111
},
1212
"scripts": {
13-
"test": "node --test test/cli.test.mjs"
13+
"test": "node --test test/cli.test.mjs test/install.test.mjs"
1414
}
1515
}

‎scripts/install-cli.sh‎

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
#!/usr/bin/env bash
2+
# install-cli.sh — shadow-dev CLI 的拉取/更新/回滚安装器(供 shadow-dev-workflow 插件钩子与人工共用)
3+
#
4+
# 接缝契约(与 README「安装与分发」同源):
5+
# commands: install | update(=install) | rollback | status
6+
# options : --channel release|main --version vA.B.C --from <tarball|dir>
7+
# --prefix DIR(~/.local/share/shadow-dev-cli) --bin DIR(~/.local/bin)
8+
# --force --dry-run --json(单行机器输出)
9+
# exit : 0 成功/已最新 | 1 参数或冲突 | 2 网络/API | 3 产物自校验失败
10+
# 布局 : $PREFIX/shadow-dev-cli-<ver>/{cli.mjs,lib,...};CURRENT/PREVIOUS 为版本指针文本文件;
11+
# $BIN/shadow-dev(.cmd) 为托管 shim(头标 managed-by,运行时读 CURRENT → 更新不动 shim)
12+
# 信任边界: HTTPS + GitHub 仓库;发布前自校验(node cli.mjs help --json 断言 ok)失败则指针不动
13+
set -euo pipefail
14+
15+
REPO_SLUG="stack-wuh/shadow-dev-cli"
16+
API="https://api.github.com/repos/$REPO_SLUG"
17+
WEB="https://github.com/$REPO_SLUG"
18+
MARK="managed-by: shadow-dev-cli-installer"
19+
20+
CMD="install"; CHANNEL="release"; VERSION=""; FROM=""; FORCE=0; DRY=0; JSON=0
21+
PREFIX="${SD_PREFIX:-$HOME/.local/share/shadow-dev-cli}"
22+
BIN="${SD_BIN:-$HOME/.local/bin}"
23+
24+
json() { if [ "$JSON" -eq 1 ]; then printf '%s\n' "$1"; fi; }
25+
log() { if [ "$JSON" -ne 1 ]; then printf '%s\n' "$1"; fi; }
26+
die() { # die <exit> <error> <message>
27+
json "{\"ok\":false,\"error\":\"$2\",\"message\":\"$3\"}"
28+
printf '%s: %s\n' "$2" "$3" >&2 || true
29+
exit "$1"
30+
}
31+
32+
while [ $# -gt 0 ]; do
33+
case "$1" in
34+
install|update|rollback|status) CMD="$1"; shift
35+
;;
36+
--channel) [ $# -ge 2 ] || die 1 usage "--channel needs release|main"; CHANNEL="$2"; shift 2
37+
;;
38+
--version) [ $# -ge 2 ] || die 1 usage "--version needs vA.B.C"; VERSION="$2"; shift 2
39+
;;
40+
--from) [ $# -ge 2 ] || die 1 usage "--from needs <tarball|dir>"; FROM="$2"; shift 2
41+
;;
42+
--prefix) [ $# -ge 2 ] || die 1 usage "--prefix needs DIR"; PREFIX="$2"; shift 2
43+
;;
44+
--bin) [ $# -ge 2 ] || die 1 usage "--bin needs DIR"; BIN="$2"; shift 2
45+
;;
46+
--force) FORCE=1; shift ;;
47+
--dry-run) DRY=1; shift ;;
48+
--json) JSON=1; shift ;;
49+
*) die 1 usage "unknown argument: $1" ;;
50+
esac
51+
done
52+
53+
command -v node >/dev/null || die 1 missing-node "node is required (>=20)"
54+
if [ "$CMD" = install ] || [ "$CMD" = update ]; then
55+
if [ -n "$FROM" ] && { [ -n "$VERSION" ] || [ "$CHANNEL" = main ]; }; then die 1 usage "--from conflicts with --version/--channel main"; fi
56+
if [ "$CHANNEL" = main ] && [ -n "$VERSION" ]; then die 1 usage "--version conflicts with --channel main"; fi
57+
fi
58+
59+
# ---- 依赖与工具 ----
60+
DL() { # DL <url> <out>:curl 优先,wget 兜底,带可选 token
61+
local hdr=()
62+
[ -n "${GITHUB_TOKEN:-}${GH_TOKEN:-}" ] && hdr=(-H "Authorization: Bearer ${GITHUB_TOKEN:-$GH_TOKEN}")
63+
if command -v curl >/dev/null; then
64+
curl -fsSL -H "Accept: application/vnd.github+json" -H "User-Agent: shadow-dev-installer" ${hdr[@]+"${hdr[@]}"} -o "$2" "$1"
65+
elif command -v wget >/dev/null; then
66+
wget -q ${hdr[@]+"${hdr[@]}"} -O "$2" "$1"
67+
else
68+
return 127
69+
fi
70+
}
71+
verof() { node -pe "JSON.parse(require('fs').readFileSync(process.argv[1],'utf8')).version" "$1/package.json"; }
72+
73+
# ---- 锁(陈旧>10min 自动接管)----
74+
LOCK="$PREFIX/.lock"
75+
mkdir -p "$PREFIX"
76+
if ! mkdir "$LOCK" 2>/dev/null; then
77+
if [ -n "$(find "$LOCK" -maxdepth 0 -mmin +10 2>/dev/null)" ]; then rm -rf "$LOCK"; mkdir "$LOCK" || die 1 busy "cannot acquire lock";
78+
else die 1 busy "another install-cli run is in progress ($LOCK)"; fi
79+
fi
80+
trap 'rm -rf "$LOCK" 2>/dev/null || true' EXIT
81+
82+
# ---- 离线命令先行:rollback/status 不触网、不解析远端 ----
83+
CUR="$(cat "$PREFIX/CURRENT" 2>/dev/null || true)"
84+
if [ "$CMD" = rollback ]; then
85+
PREV="$(cat "$PREFIX/PREVIOUS" 2>/dev/null || true)"
86+
[ -n "$PREV" ] && [ -d "$PREFIX/shadow-dev-cli-$PREV" ] || die 1 no-rollback "no previous version available"
87+
printf '%s\n' "$CUR" > "$PREFIX/PREVIOUS"; printf '%s\n' "$PREV" > "$PREFIX/CURRENT"
88+
json "{\"ok\":true,\"action\":\"rollback\",\"current\":\"$PREV\",\"previous\":\"${CUR:-null}\"}"
89+
log "rolled back: $CUR -> $PREV"
90+
exit 0
91+
fi
92+
if [ "$CMD" = status ]; then
93+
PRV="$(cat "$PREFIX/PREVIOUS" 2>/dev/null || true)"
94+
json "{\"ok\":true,\"current\":\"${CUR:-null}\",\"previous\":\"${PRV:-null}\"}"
95+
log "current=${CUR:-<none>} previous=${PRV:-<none>}"
96+
exit 0
97+
fi
98+
99+
# ---- 解析目标版本与物化产物(release/main/--from 三路同构:得到 WORK 目录 + VER)----
100+
WORK=""; VER=""; TMP=""
101+
if [ -n "$FROM" ]; then
102+
if [ -d "$FROM" ]; then
103+
[ -f "$FROM/cli.mjs" ] && [ -f "$FROM/package.json" ] || die 3 artifact "--from dir lacks cli.mjs/package.json"
104+
WORK="$FROM"
105+
else
106+
command -v tar >/dev/null || die 1 missing-tar "tar is required for tarball install"
107+
TMP="$(mktemp -d)"; trap 'rm -rf "$LOCK" "$TMP" 2>/dev/null || true' EXIT
108+
tar -xzf "$FROM" -C "$TMP"
109+
WORK="$TMP/shadow-dev-cli"
110+
[ -f "$WORK/cli.mjs" ] && [ -f "$WORK/package.json" ] || die 3 artifact "tarball lacks shadow-dev-cli/cli.mjs (invalid artifact)"
111+
fi
112+
VER="$(verof "$WORK")"
113+
elif [ "$CHANNEL" = main ]; then
114+
command -v git >/dev/null || die 1 missing-git "git is required for --channel main"
115+
TMP="$(mktemp -d)"; trap 'rm -rf "$LOCK" "$TMP" 2>/dev/null || true' EXIT
116+
git clone --quiet --depth 1 "$WEB" "$TMP/clone" 2>/dev/null || die 2 network "git clone failed (check network/credentials)"
117+
SHA="$(git -C "$TMP/clone" rev-parse --short=8 HEAD)"
118+
WORK="$TMP/clone"; VER="$(verof "$WORK")-main.$SHA"
119+
else
120+
command -v tar >/dev/null || die 1 missing-tar "tar is required"
121+
API_PATH=$([ -n "$VERSION" ] && echo "/releases/tags/$VERSION" || echo "/releases/latest")
122+
TMP="$(mktemp -d)"; trap 'rm -rf "$LOCK" "$TMP" 2>/dev/null || true' EXIT
123+
DL "$API$API_PATH" "$TMP/rel.json" || die 2 network "GitHub API request failed ($API$API_PATH)"
124+
URL="$(node -pe '
125+
const j = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"))
126+
const a = (j.assets || []).find(x => /^shadow-dev-cli-v[0-9][0-9.]*\.tar\.gz$/.test(x.name))
127+
if (!a) process.exit(1)
128+
process.stdout.write(a.browser_download_url)
129+
' "$TMP/rel.json")" || die 2 network "release asset not found"
130+
VER="$(node -pe 'JSON.parse(require("fs").readFileSync(process.argv[1],"utf8")).tag_name.replace(/^v/,"")' "$TMP/rel.json")"
131+
DL "$URL" "$TMP/artifact.tgz" || die 2 network "artifact download failed"
132+
tar -xzf "$TMP/artifact.tgz" -C "$TMP"
133+
WORK="$TMP/shadow-dev-cli"
134+
[ -f "$WORK/cli.mjs" ] && [ -f "$WORK/package.json" ] || die 3 artifact "extracted artifact lacks cli.mjs (invalid)"
135+
fi
136+
[ "$(verof "$WORK")" = "${VER%%-main.*}" ] || die 3 artifact "version mismatch between tag and package.json"
137+
138+
if [ "$CUR" = "$VER" ] && [ -d "$PREFIX/shadow-dev-cli-$VER" ] && [ "$FORCE" -eq 0 ]; then
139+
json "{\"ok\":true,\"action\":\"none\",\"version\":\"$VER\"}"; exit 0
140+
fi
141+
142+
# ---- 冲突保护:非托管同名 shim 在任何写盘前失败退出 ----
143+
shim_guard() {
144+
for f in "$BIN/shadow-dev" "$BIN/shadow-dev.cmd"; do
145+
[ -e "$f" ] || continue
146+
grep -q "$MARK" "$f" 2>/dev/null || die 1 unmanaged-shim "unmanaged file occupies shim path: $f — rename it or pass --bin elsewhere (never overwriting silently)"
147+
done
148+
}
149+
shim_guard
150+
if [ "$DRY" -eq 1 ]; then
151+
json "{\"ok\":true,\"action\":\"dry-run\",\"version\":\"$VER\",\"current\":\"${CUR:-null}\",\"prefix\":\"$PREFIX/shadow-dev-cli-$VER\"}"
152+
log "dry-run: would install $VER to $PREFIX/shadow-dev-cli-$VER and refresh shims in $BIN"
153+
exit 0
154+
fi
155+
156+
# ---- 物化 + 自校验(发布前,失败指针不动)----
157+
DEST="$PREFIX/shadow-dev-cli-$VER"
158+
rm -rf "$DEST"; mkdir -p "$DEST"
159+
cp -r "$WORK/." "$DEST/"
160+
node "$DEST/cli.mjs" help --json 2>/dev/null | grep -q '"ok":true' || { rm -rf "$DEST"; die 3 selfcheck "installed cli.mjs failed 'help --json' smoke test"; }
161+
162+
# ---- 原子发布:切指针,保留上一版供回滚,清理更旧版本 ----
163+
[ -n "$CUR" ] && [ "$CUR" != "$VER" ] && printf '%s\n' "$CUR" > "$PREFIX/PREVIOUS"
164+
printf '%s\n' "$VER" > "$PREFIX/CURRENT"
165+
PRV="$(cat "$PREFIX/PREVIOUS" 2>/dev/null || true)"
166+
for d in "$PREFIX"/shadow-dev-cli-*; do
167+
[ -d "$d" ] || continue
168+
b="$(basename "$d")"
169+
[ "$b" = "shadow-dev-cli-$VER" ] && continue
170+
[ "$b" = "shadow-dev-cli-$PRV" ] && continue
171+
rm -rf "$d"
172+
done
173+
174+
# ---- 托管 shim:运行时读 CURRENT,更新不再动 shim 文件 ----
175+
mkdir -p "$BIN"
176+
{
177+
echo '#!/bin/sh'
178+
echo "# $MARK v1 — generated file, regenerate via install-cli.sh, do not edit"
179+
echo "root='$PREFIX'"
180+
echo "v=\$(cat \"\$root/CURRENT\" 2>/dev/null)"
181+
echo "if [ -z \"\$v\" ]; then echo 'shadow-dev: not installed — run install-cli.sh install' >&2; exit 1; fi"
182+
echo "exec node \"\$root/shadow-dev-cli-\$v/cli.mjs\" \"\$@\""
183+
} > "$BIN/shadow-dev"
184+
chmod +x "$BIN/shadow-dev"
185+
case "$(uname -s 2>/dev/null)" in
186+
MINGW*|MSYS*|CYGWIN*)
187+
WINROOT="$PREFIX"
188+
command -v cygpath >/dev/null && WINROOT="$(cygpath -w "$PREFIX")"
189+
{
190+
echo '@echo off'
191+
echo "rem $MARK v1 — generated file, regenerate via install-cli.sh, do not edit"
192+
echo "set \"ROOT=$WINROOT\""
193+
echo 'set /p V=<"%ROOT%\CURRENT"'
194+
echo 'if "%V%"=="" (echo shadow-dev: not installed 1>&2 & exit /b 1)'
195+
echo 'node "%ROOT%\shadow-dev-cli-%V%\cli.mjs" %*'
196+
} > "$BIN/shadow-dev.cmd"
197+
;;
198+
esac
199+
200+
# ---- 安装后冒烟 + PATH 提示 ----
201+
"$BIN/shadow-dev" help 2>/dev/null | grep -q '"ok":true' || die 3 selfcheck "installed shim failed smoke test"
202+
case ":$PATH:" in *":$BIN:"*) ;; *) log "warn: $BIN is not on PATH — add it (unix: export PATH=\"$BIN:\$PATH\"; windows setx PATH \"%PATH%;%USERPROFILE%\.local\\bin\")" ;; esac
203+
204+
json "{\"ok\":true,\"action\":\"install\",\"version\":\"$VER\",\"previous\":\"${CUR:-null}\",\"prefix\":\"$PREFIX\",\"shim\":\"$BIN/shadow-dev\"}"
205+
log "shadow-dev $VER installed to $DEST (was: ${CUR:-none})"
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
---
2+
{
3+
"schema": "shadow-dev/v1",
4+
"name": "20260917-feature-install-cli-script",
5+
"type": "feature",
6+
"scope": "scripts",
7+
"status": "published",
8+
"baseBranch": "main",
9+
"branch": "feature/20260917-feature-install-cli-script",
10+
"files": [
11+
"README.md",
12+
"scripts/install-cli.sh",
13+
"test/install.test.mjs"
14+
],
15+
"github": {
16+
"repository": "stack-wuh/shadow-dev-cli",
17+
"issue": 11,
18+
"issueUrl": "https://github.com/stack-wuh/shadow-dev-cli/issues/11",
19+
"pullRequest": 13,
20+
"pullRequestUrl": "https://github.com/stack-wuh/shadow-dev-cli/pull/13"
21+
},
22+
"review": {
23+
"conclusion": "passed",
24+
"verifiedCommit": "38b13641d0167403b99e557e3e1fe8df4d321183",
25+
"verifiedAt": "2026-09-17T09:21:06.983Z"
26+
},
27+
"workflow": {
28+
"operation": null,
29+
"checkpoint": "pr:13",
30+
"planHash": "9f6178c08054b867c2105d6331097ca8fbbab78d388d8dc468d044aa437d3e8e",
31+
"updatedAt": null,
32+
"lastError": null,
33+
"issuePlan": {
34+
"title": "install-cli.sh:shadow-dev CLI 拉取/更新/回滚安装器",
35+
"body": "版本目录+CURRENT/PREVIOUS 指针、托管 shim 保护、发布前自校验、rollback/--json/--dry-run/--from 离线。供 shadow-dev-workflow 钩子以 install --json 幂等调用。契约测试含离线全链、冲突保护、回滚往返。",
36+
"labels": [
37+
"feature"
38+
]
39+
}
40+
},
41+
"knowledge": {
42+
"action": "无需变更",
43+
"target": null,
44+
"reason": null
45+
}
46+
}
47+
---
48+
49+
# install-cli.sh:shadow-dev CLI 拉取/更新/回滚安装器
50+
51+
## 动机
52+
53+
CLI 与插件(shadow-dev-workflow)的接缝目前是手工的:本次 v1.1.0 发布后,用户 PATH 上的旧 exe 靠人工改名+手写 shim 才切到新行为。插件钩子需要一个可重复、机器可校验、可回滚的入口来保证「`shadow-dev` 永远指向正确版本」;同时本机 `.local/bin/shadow-dev.exe` 被非托管二进制占用这类冲突必须显式保护而非静默覆盖。
54+
55+
## 引用规范
56+
57+
- norms/code-style.md(通用规范)
58+
- 当前结论: 渐进式治理;脚本与 CLI 同仓演进,README 分发段落需同步。
59+
- 适用 scope: `scripts/`、`README.md`
60+
- shadow-docs/knowledge/cli-output-contract.md
61+
- 当前结论: JSON 契约面按环境路由;退出码 0/1/2/3 语义(本脚本对齐其子集:0 成功/已最新、1 参数/冲突、2 网络、3 自校验失败)。
62+
- 适用 scope: 脚本 `--json` 输出与钩子集成契约。
63+
64+
## 决策
65+
66+
- **选型:** 版本目录 `$PREFIX/shadow-dev-cli-<ver>/` + `CURRENT`/`PREVIOUS` 指针文件;shim 运行时读指针(更新不动盘、回滚=改指针);`install|update|rollback|status` 四命令 + `--channel release|main` / `--version` / `--from <tarball|dir>` 离线 / `--json` / `--dry-run` / `--force`;非托管同名 shim/二进制只告警退出;发布前 `node cli.mjs help --json` 自校验通过才切指针。
67+
- **对比方案:** ① symlink 版本目录——Windows 无可靠 symlink,否;② curl|bash 直装——供应链面大且无锁,插件改为本地 clone 执行;③ 校验依赖 GitHub asset digest——可用性不稳,降级为结构校验+自跑(信任边界=HTTPS+仓库,README 注明)。
68+
- **理由:** 插件钩子每次启动都跑 `install --json`,已最新秒退保护启动路径;失败/断网时旧指针版本照常可用(先物化后发布)。rolling `main` 通道保留给插件开发,但钩子默认 release 保可复现。
69+
70+
## 任务
71+
72+
### Phase 1 — 脚本(骨架→能力)
73+
74+
- [x] 骨架:set -euo、参数解析与互斥校验(--from↔--version/--channel main)、退出码 0/1/2/3、PREFIX/.lock 原子锁(>10min 判陈旧) —— `scripts/install-cli.sh`
75+
- [x] 目标解析:release 通道 GitHub API(token 可选)取 tag+asset URL;`--from` 本地 tarball/目录;channel=main git 浅拉并合成 `ver-main.sha8` 版本 —— `scripts/install-cli.sh`
76+
- [x] 物化与发布:结构校验(含 cli.mjs/package.json)→ 解包到 `shadow-dev-cli-<ver>.new` → 自跑 help --json 断言 ok → 原子改名+写 CURRENT/保留 PREVIOUS(force 允许同版重装) —— `scripts/install-cli.sh`
77+
- [x] shim 托管:无扩展名 sh + .cmd(cygpath 归一)双 shim,头部 managed-by 标记,运行时读 CURRENT;已存在无标记文件→告警退出 1 不覆盖;BIN 不在 PATH 时打印补救指引 —— `scripts/install-cli.sh`
78+
- [x] rollback / status / --json / --dry-run 语义收尾 —— `scripts/install-cli.sh`
79+
80+
### Phase 2 — 契约测试与文档
81+
82+
- [x] 新测试文件(bash 可用才运行):离线 tarball 全链(CURRENT/shim 标记/自校验)、已最新秒退、--force、非托管冲突保护、rollback 往返、--json 可解析、dry-run 零写入 —— `test/install.test.mjs`
83+
- [x] README「安装与分发」改写:钩子调用示例、退出码表、信任边界说明;全量冒烟(含既有 CLI 套件) —— `README.md`
84+
85+
## 结果
86+
87+
- 实际耗时: 约 40 分钟
88+
- 验证: `node --test` CLI 49/49 + 安装器 6/6 全绿(测试先行:脚本缺失时全红,实现后逐步转绿;修复 `set -e` 下 `[ ] && cmd` 短路误杀、`json()` 退出码传染、rollback/status 误触网、`--from` 互斥校验取反、Windows→Git Bash tar 路径 `C:` 误解析为远程主机共 5 处真实缺陷,均由契约测试当场暴露);README 分发段改写;npm test 并列两套件。范围偏差声明:`package.json` 的 test 脚本并入安装器套件属本变更直接配套,未在 brief files 清单声明,随 commit 一并入库,review 时请确认。
89+
90+
## 知识评估
91+
92+
- **预期影响:** 无需变更
93+
- **候选卡片:** 无
94+
- **理由:** 安装器接缝契约(命令、退出码、指针机制)是 README 分发段落的直接内容+脚本头注释,属单表面文档而非跨变更执行约束;未产生需要独立卡片防回归的非显然事实。

0 commit comments

Comments
 (0)