Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions server/templates/cli.sh
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,16 @@ run_install() {

# 安装到 bin 目录:优先可写目录,其次免密 sudo(sudo -n 不会卡住无人值守的 Agent),最后 ~/.local/bin
install_to_bin() {
local src="$1" bin_dir="${ASH_BIN_DIR:-}" use_sudo=""
local src="$1" bin_dir="${ASH_BIN_DIR:-}" use_sudo="" current
current="$(command -v ash 2>/dev/null || true)"
if [ -z "$bin_dir" ]; then
local current
current="$(command -v ash 2>/dev/null || true)"
if [ -n "$current" ] && [ -w "$(dirname "$current")" ]; then bin_dir="$(dirname "$current")"
# 目录不可写、但 ash 文件本身可写(常见:root 建的 /usr/local/bin 里放着本人的 ash):原地覆盖。
# 否则会在别处另装一份,而 PATH 里更靠前的旧版本永远不会被替换
elif [ -n "$current" ] && [ -f "$current" ] && [ -w "$current" ]; then
cat "$src" > "$current" && rm -f "$src"
echo "✓ 已更新 $current"
return
elif [ -w /usr/local/bin ]; then bin_dir=/usr/local/bin
elif command -v sudo >/dev/null 2>&1 && sudo -n true 2>/dev/null; then bin_dir=/usr/local/bin; use_sudo="sudo -n"
else bin_dir="$HOME/.local/bin"; fi
Expand All @@ -130,6 +135,11 @@ install_to_bin() {
*":$bin_dir:"*) ;;
*) echo "⚠️ $bin_dir 不在 PATH 中,请执行:export PATH=\"$bin_dir:\$PATH\"(并写入 shell 配置)" ;;
esac
hash -r 2>/dev/null || true
current="$(command -v ash 2>/dev/null || true)"
if [ -n "$current" ] && [ "$current" != "$bin_dir/ash" ]; then
echo "⚠️ PATH 里更靠前的 $current 不是刚安装的版本,运行 ash 仍会用到它:请删除它,或把 $bin_dir 放到 PATH 更前面"
fi
}

fetch_self() {
Expand Down Expand Up @@ -429,6 +439,8 @@ EOF
echo "正在更新 ash…"
fetch_self
echo "✅ ash 已更新到最新版本"
# 本文件可能刚被原地改写:立即退出,别让 bash 从改写后的文件里接着读
exit 0
;;
help|-h|--help) show_help ;;
*) show_help; exit 1 ;;
Expand Down
32 changes: 32 additions & 0 deletions server/tests/agent-flow.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,38 @@ test('agent flow (review enabled)', async (t) => {
const help = spawnSync(path.join(bin, 'ash'), ['help'], { encoding: 'utf8' });
assert.match(help.stdout, /ash push <技能目录\|SKILL\.md> \[--update\]/);
});

await t.test('update replaces an ash in a read-only bin dir in place instead of shadowing it', async (t2) => {
const work = tmpDir('ash-update-');
const locked = path.join(work, 'locked-bin');
fs.mkdirSync(locked);
const stale = path.join(locked, 'ash');
// 本人拥有的 ash 放在不可写的目录里(带一行标记,用来判断是否被替换)
const cli = await (await fetch(`${origin}/cli.sh`)).text();
fs.writeFileSync(stale, cli.replace(/^(#![^\n]*\n)/, '$1# stale\n'), { mode: 0o755 });
fs.chmodSync(locked, 0o555);
t2.after(() => fs.chmodSync(locked, 0o755));
const env = { ...process.env, HOME: work, PATH: `${locked}:${process.env.PATH}` };
delete env.ASH_BIN_DIR;
const run = spawnSync('ash', ['update'], { env, encoding: 'utf8' });
assert.equal(run.status, 0, run.stderr + run.stdout);
assert.match(run.stdout, new RegExp(`已更新 ${stale}`));
assert.doesNotMatch(fs.readFileSync(stale, 'utf8'), /# stale/, 'the file on PATH was replaced');
assert.ok(!fs.existsSync(path.join(work, '.local', 'bin', 'ash')), 'no shadow copy elsewhere');
assert.doesNotMatch(run.stdout + run.stderr, /command not found|syntax error/, 'rewriting the running script does not derail bash');
});

await t.test('install warns when an earlier ash on PATH shadows the new one', async () => {
const work = tmpDir('ash-shadow-');
const early = path.join(work, 'early');
const target = path.join(work, 'target');
fs.mkdirSync(early);
fs.writeFileSync(path.join(early, 'ash'), '#!/bin/sh\necho old\n', { mode: 0o755 });
const script = await (await fetch(`${origin}/setup.sh`)).text();
const run = spawnSync('bash', { input: script, env: { ...process.env, HOME: work, ASH_BIN_DIR: target, PATH: `${early}:${target}:${process.env.PATH}` }, encoding: 'utf8' });
assert.equal(run.status, 0, run.stderr);
assert.match(run.stdout, new RegExp(`PATH 里更靠前的 ${path.join(early, 'ash')}`));
});
});

test('agent flow with review disabled still forces high-risk pushes into review', async (t) => {
Expand Down
Loading