@nathan33/nao-skill 0.12.0 → 0.13.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.
- package/.agents/scripts/nao-fleet.sh +115 -41
- package/.agents/skills/nao-fleet/SKILL.md +1 -1
- package/.agents/templates/AGENTS.md.example +1 -1
- package/README.md +9 -5
- package/bin/nao-skill.js +123 -41
- package/package.json +1 -1
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
# nao-fleet.sh ensure -m <model> <别名>... 显式指定模型(须命中白名单)
|
|
11
11
|
# nao-fleet.sh ensure --task <编号> <别名>[@<repo>] 任务派生会话:--name <别名>-<编号>(并行隔离,避免同名冲突)
|
|
12
12
|
# nao-fleet.sh ensure --force <别名>... 忽略"已在运行"判重
|
|
13
|
-
# nao-fleet.sh close
|
|
14
|
-
#
|
|
13
|
+
# nao-fleet.sh close <别名|派生会话名> [--task <编号>] [--force]
|
|
14
|
+
# 回收已完成会话(派生名如 rd-be-T1;跨仓定位;定位失败/名册报在线但无句柄 ⇒ 非 0 + 诊断)
|
|
15
|
+
# (闸门:在跑 turn / tasks-state 未推进 → 拒绝,--force 跳过)
|
|
15
16
|
#
|
|
16
17
|
# 角色别名 → 角色卡:见 .agents/roles.yaml(单一事实来源)
|
|
17
18
|
# 当前:pm / arch-designer(arch) / rd-fe / rd-be / qa / rd-infra(infra)
|
|
@@ -132,7 +133,7 @@ load_manifest() {
|
|
|
132
133
|
while IFS=$'\t' read -r k v w rest; do
|
|
133
134
|
case "$k" in
|
|
134
135
|
ROLE) ROLE_ORDER+=("$v"); ROLE_CARDS["$v"]="$w"; ROLE_WS["$v"]="$rest" ;;
|
|
135
|
-
ALIAS) ALIAS_ROLE["$
|
|
136
|
+
ALIAS) ALIAS_ROLE["$w"]="$v" ;; # 别名 → 角色 id(消费方 resolve_role/close 按此方向取值)
|
|
136
137
|
esac
|
|
137
138
|
done <<< "$out"
|
|
138
139
|
}
|
|
@@ -206,6 +207,39 @@ check_roles_indent() {
|
|
|
206
207
|
' "$MANIFEST"
|
|
207
208
|
}
|
|
208
209
|
|
|
210
|
+
# 别名解析守卫(#19):独立重解析 roles.yaml(不依赖可能被写反的 ALIAS_ROLE),校验
|
|
211
|
+
# ① 每个声明的别名可解析为「声明它的角色」(对 ALIAS_ROLE 键值方向敏感)
|
|
212
|
+
# ② 每个角色 canonical id 已登记于 aliases(BR2:canonical 须可解析为自身)
|
|
213
|
+
# ③ 别名不跨角色重名
|
|
214
|
+
check_alias_resolution() {
|
|
215
|
+
local rc=0 ga gr
|
|
216
|
+
local -A owner=()
|
|
217
|
+
while IFS=$'\t' read -r ga gr; do
|
|
218
|
+
[[ -n "$ga" && -n "$gr" ]] || continue
|
|
219
|
+
if [[ -n "${owner[$ga]:-}" && "${owner[$ga]}" != "$gr" ]]; then
|
|
220
|
+
printf ' ✗ 别名 %s 跨角色重名:%s 与 %s\n' "$ga" "${owner[$ga]}" "$gr"; rc=1
|
|
221
|
+
fi
|
|
222
|
+
owner["$ga"]="$gr"
|
|
223
|
+
if [[ "${ALIAS_ROLE[$ga]:-}" != "$gr" ]]; then
|
|
224
|
+
printf ' ✗ 别名 %s 解析异常:roles.yaml 声明属 %s,实际解析为 %s(ALIAS_ROLE 键值方向可能反写)\n' \
|
|
225
|
+
"$ga" "$gr" "${ALIAS_ROLE[$ga]:-<未登记>}"; rc=1
|
|
226
|
+
fi
|
|
227
|
+
done < <(awk -F'\t' '
|
|
228
|
+
/^ [^ ][^:]*:[[:space:]]*$/ { c=$0; sub(/^ /,"",c); sub(/:[[:space:]]*$/,"",c); next }
|
|
229
|
+
c!="" && /^ aliases:[[:space:]]*\[/ {
|
|
230
|
+
v=$0; sub(/^ aliases:[[:space:]]*\[/,"",v); sub(/\].*/,"",v)
|
|
231
|
+
n=split(v, a, /,/)
|
|
232
|
+
for (i=1;i<=n;i++){ gsub(/^[[:space:]]+|[[:space:]]+$/, "", a[i]); if(a[i]!="") print a[i] "\t" c }
|
|
233
|
+
}' "$MANIFEST")
|
|
234
|
+
for gr in "${ROLE_ORDER[@]}"; do
|
|
235
|
+
if [[ "${owner[$gr]:-}" != "$gr" ]]; then
|
|
236
|
+
printf ' ✗ 角色 %s 的 canonical id 未登记于 aliases(BR2:canonical 须可解析为自身)\n' "$gr"; rc=1
|
|
237
|
+
fi
|
|
238
|
+
done
|
|
239
|
+
(( rc == 0 )) && printf ' ✓ 别名解析守卫:全部别名可解析且无跨角色重名\n'
|
|
240
|
+
return $rc
|
|
241
|
+
}
|
|
242
|
+
|
|
209
243
|
# qq-notify 主动推送器契约:存在 + 可执行 + EOL 全 LF + 无 Tab + node --check 语法
|
|
210
244
|
check_qq_notify() {
|
|
211
245
|
local rc=0 f="$SKILLS_DIR/.agents/scripts/qq-notify"
|
|
@@ -372,6 +406,15 @@ intercom_online() {
|
|
|
372
406
|
return 1
|
|
373
407
|
}
|
|
374
408
|
|
|
409
|
+
# 名册中该会话的 cwd(跨仓 close 需按目标仓定位;无记录则空)
|
|
410
|
+
intercom_cwd_for() {
|
|
411
|
+
local n c
|
|
412
|
+
while IFS=$'\t' read -r n c; do
|
|
413
|
+
[[ "$n" == "$1" ]] && { printf '%s\n' "$c"; return 0; }
|
|
414
|
+
done < <(intercom_roster)
|
|
415
|
+
return 0
|
|
416
|
+
}
|
|
417
|
+
|
|
375
418
|
# 会话所在的 tmux pane。三层后备(标题可被外部改写,如 "pi:c"):
|
|
376
419
|
# ① 终端标题契约 "π - <会话名> - <repo basename>"(pi 自设;最快)
|
|
377
420
|
# ② pi-intercom 名册 tmuxPane(注册时读 $TMUX_PANE;与标题改名无关,权威)
|
|
@@ -484,6 +527,17 @@ find_pane_for() {
|
|
|
484
527
|
return 0
|
|
485
528
|
}
|
|
486
529
|
|
|
530
|
+
# 跨仓 pane 定位:不限定 repo(标题任意仓 basename / 名册 tmuxPane 不限 cwd)
|
|
531
|
+
# close 的目标可能在别的仓(roles.yaml workspace 恒为本仓)⇒ 不能用 find_pane_for 的 repo 约束。
|
|
532
|
+
find_pane_for_any_repo() {
|
|
533
|
+
local name="$1" pane=""
|
|
534
|
+
pane="$(find_pane_by_title "$name" "")"
|
|
535
|
+
[[ -n "$pane" ]] && { printf '%s\n' "$pane"; return 0; }
|
|
536
|
+
pane="$(intercom_pane_for "$name" "")"
|
|
537
|
+
if [[ -n "$pane" ]] && tmux_pane_live "$pane"; then printf '%s\n' "$pane"; return 0; fi
|
|
538
|
+
return 0
|
|
539
|
+
}
|
|
540
|
+
|
|
487
541
|
# 该会话名对应的 pi 进程 PID(经 pane 句柄取 pane_pid 及其子进程中 comm=pi 者)
|
|
488
542
|
pi_pids_for() {
|
|
489
543
|
local pane root pid
|
|
@@ -882,6 +936,7 @@ cmd_check() {
|
|
|
882
936
|
echo "== 角色清单 roles.yaml =="
|
|
883
937
|
load_manifest
|
|
884
938
|
printf ' ✓ 解析成功,%d 个角色: %s\n' "${#ROLE_ORDER[@]}" "${ROLE_ORDER[*]}"
|
|
939
|
+
check_alias_resolution || rc=1
|
|
885
940
|
|
|
886
941
|
echo "== 常驻角色卡(阈值 ${CARD_MAX_LINES} 行,frontmatter 校验)=="
|
|
887
942
|
local a role version updated
|
|
@@ -1106,14 +1161,39 @@ cmd_status() {
|
|
|
1106
1161
|
# ---------------------------------------------------------------------------
|
|
1107
1162
|
# 回收已完成会话:闸门(tasks-state 已推进 + 无在跑 turn)+ 落地(pane/会话/screen/进程)
|
|
1108
1163
|
cmd_close() {
|
|
1109
|
-
local force="$1" task="$2" target="$3"
|
|
1110
|
-
[[ -n "$target" ]] || die "close 需要目标:角色别名或派生会话名(如 rd-be / rd-be-T1
|
|
1111
|
-
local name repo cls bus pane pids
|
|
1164
|
+
local force="$1" task="$2" target="${3:-}"
|
|
1165
|
+
[[ -n "$target" ]] || die "close 需要目标:角色别名或派生会话名(如 rd-be / rd-be-T1 / qa-T1)"
|
|
1166
|
+
local name repo cls bus pane pids st tgt a best=""
|
|
1112
1167
|
intercom_list_json >/dev/null 2>&1 || true # 预热名册缓存
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1168
|
+
|
|
1169
|
+
# ---- 目标解析:别名/canonical id | 派生会话名 <角色>-<编号> ----
|
|
1170
|
+
# 派生名(如 qa-T508)可直接作目标;也兼容 close --task T508 qa 与 close --task T508 qa-T508。
|
|
1171
|
+
tgt=""
|
|
1172
|
+
if [[ -n "${ALIAS_ROLE[$target]:-}" ]]; then
|
|
1173
|
+
tgt="$target"
|
|
1174
|
+
elif [[ -n "$task" && "$target" == *-"$task" && -n "${ALIAS_ROLE[${target%-"$task"}]:-}" ]]; then
|
|
1175
|
+
tgt="${target%-"$task"}"
|
|
1176
|
+
else
|
|
1177
|
+
# 未带 --task 的派生名:取最长可解析前缀为角色,余下为任务编号
|
|
1178
|
+
for a in "${!ALIAS_ROLE[@]}"; do
|
|
1179
|
+
[[ "$target" == "$a"-* ]] || continue
|
|
1180
|
+
if [[ -z "$best" || ${#a} -gt ${#best} ]]; then best="$a"; fi
|
|
1181
|
+
done
|
|
1182
|
+
if [[ -n "$best" ]]; then tgt="$best"; task="${target#"$best"-}"; fi
|
|
1183
|
+
fi
|
|
1184
|
+
if [[ -z "$tgt" ]]; then
|
|
1185
|
+
die "未知角色或派生会话名: $target(可用角色: ${ROLE_ORDER[*]};派生名形如 rd-be-T1 / qa-T1,亦可用 close --task <编号> <角色>)"
|
|
1186
|
+
fi
|
|
1187
|
+
resolve_role "$tgt"
|
|
1188
|
+
name="$NAME"
|
|
1189
|
+
[[ -n "$task" ]] && name="${NAME}-${task}"
|
|
1190
|
+
# 目标仓:名册 cwd 优先(跨仓会话真实仓),否则回退 roles.yaml workspace / 当前目录
|
|
1191
|
+
repo="$(intercom_cwd_for "$name")"
|
|
1192
|
+
[[ -n "$repo" ]] || repo="${ROLE_WS[$NAME]:-$PWD}"
|
|
1193
|
+
|
|
1194
|
+
if [[ -z "$task" ]]; then
|
|
1195
|
+
warn "常驻会话 $name:任务闭环后应 ensure --force 重开,而非回收(仅在本批不再需要该角色时回收)"
|
|
1196
|
+
else
|
|
1117
1197
|
# 闸门①:tasks-state 未推进 → 拒绝(验收未过需原会话返工,回收会丢上下文)
|
|
1118
1198
|
cls="$(task_state_class "$task")"
|
|
1119
1199
|
if [[ "$cls" == "active" && "$force" != true ]]; then
|
|
@@ -1124,20 +1204,25 @@ cmd_close() {
|
|
|
1124
1204
|
[[ "$cls" == "absent" ]] && warn "tasks-state 全文无 $task 记录(仅按会话名回收)"
|
|
1125
1205
|
[[ "$cls" == "mentioned" ]] && log "tasks-state 仅散文提及 $task(非表内记录,不阻塞回收)"
|
|
1126
1206
|
[[ "$cls" == "nofile" ]] && log "未启用 $TASKS_STATE(跳过状态闸门)"
|
|
1127
|
-
elif [[ -n "${ALIAS_ROLE[$target]:-}" ]]; then
|
|
1128
|
-
name="${ALIAS_ROLE[$target]}"; repo="${ROLE_WS[$name]:-$PWD}"
|
|
1129
|
-
warn "常驻会话 $name:任务闭环后应 ensure --force 重开,而非回收(仅在本批不再需要该角色时回收)"
|
|
1130
|
-
else
|
|
1131
|
-
name="$target"
|
|
1132
|
-
[[ "$name" =~ ^[A-Za-z0-9][A-Za-z0-9_-]*$ ]] || die "非法会话名: $name"
|
|
1133
|
-
# 非别名的目标必须是 <已知角色>-<编号>,否则视为拼错(防静默 no-op)
|
|
1134
|
-
ok=0
|
|
1135
|
-
for r in "${ROLE_ORDER[@]}"; do [[ "$name" == "$r-"* ]] && ok=1; done
|
|
1136
|
-
(( ok )) || die "未知角色或派生会话名: $name(可用: ${ROLE_ORDER[*]};派生名形如 rd-be-T1 / rd-infra-T1)"
|
|
1137
|
-
repo="$PWD"
|
|
1138
1207
|
fi
|
|
1139
1208
|
|
|
1140
|
-
|
|
1209
|
+
# ---- 宿主句柄定位(跨仓):pane 按标题任意仓 basename / 名册 tmuxPane 不限定 cwd ----
|
|
1210
|
+
pane="$(find_pane_for_any_repo "$name")"
|
|
1211
|
+
if [[ -z "$pane" ]]; then
|
|
1212
|
+
# nao-<会话名> detached tmux / screen 会话(不依赖 repo)
|
|
1213
|
+
if command -v tmux >/dev/null 2>&1 && tmux has-session -t "nao-$name" 2>/dev/null; then
|
|
1214
|
+
if tmux kill-session -t "nao-$name" 2>/dev/null; then log "已回收 $name(tmux 会话 nao-$name)@ $repo"; return 0; fi
|
|
1215
|
+
fi
|
|
1216
|
+
if command -v screen >/dev/null 2>&1 && screen -ls 2>/dev/null | grep -q "nao-$name"; then
|
|
1217
|
+
if screen -S "nao-$name" -X quit 2>/dev/null; then log "已回收 $name(screen 会话 nao-$name)@ $repo"; return 0; fi
|
|
1218
|
+
fi
|
|
1219
|
+
# 无任何可杀句柄:名册若报在线 ⇒ 定位失败,必须非静默(禁止「未运行」而 pane 仍在)
|
|
1220
|
+
if intercom_online "$name"; then
|
|
1221
|
+
warn "无法回收 $name:pi-intercom 名册报在线(cwd=${repo})但未定位到存活 pane/会话"
|
|
1222
|
+
warn " 标题契约 / 名册 tmuxPane 均未命中;tmuxPane 可能滞后或未注册"
|
|
1223
|
+
warn " 请用 intercom({action:'list'}) 人工核对;确认已退出后忽略,或按 pane_id 手工处理"
|
|
1224
|
+
return 1
|
|
1225
|
+
fi
|
|
1141
1226
|
if offline_verifiable; then
|
|
1142
1227
|
log "$name 未运行(无需回收)"; return 0
|
|
1143
1228
|
fi
|
|
@@ -1148,48 +1233,37 @@ cmd_close() {
|
|
|
1148
1233
|
fi
|
|
1149
1234
|
|
|
1150
1235
|
# 闸门②:在跑 turn(pane 标记 + roster 状态双重信号)
|
|
1151
|
-
pane="$(find_pane_for "$name" "$repo")"
|
|
1152
1236
|
bus=""
|
|
1153
1237
|
# 显式双分支赋值:pane 空闲时 bus 必须落为 ""。
|
|
1154
1238
|
# 曾用 `pane_busy "$pane" && bus=…`:pane_busy 为假时 && 短路,bus 在 set -u 下未绑定 → 行 1007 崩溃。
|
|
1155
|
-
if
|
|
1239
|
+
if pane_busy "$pane"; then
|
|
1156
1240
|
bus="tmux pane $pane 显示在跑 turn"
|
|
1157
1241
|
fi
|
|
1158
|
-
st="$(intercom_status_for "$name"
|
|
1242
|
+
st="$(intercom_status_for "$name")"
|
|
1159
1243
|
if [[ -z "$bus" ]]; then
|
|
1160
1244
|
case "$st" in
|
|
1161
1245
|
""|idle|\?) ;; # 无状态或空闲 → 不阻塞
|
|
1162
1246
|
*) bus="pi-intercom 名册状态=$st(在跑 turn)" ;;
|
|
1163
1247
|
esac
|
|
1164
1248
|
fi
|
|
1165
|
-
# 既无 pane 也无名册状态 ⇒ 确实无法判定 → 拒绝(不静默 no-op)
|
|
1166
|
-
if [[ -z "$bus" && -z "$pane" && -z "$st" ]]; then
|
|
1167
|
-
if command -v tmux >/dev/null 2>&1; then
|
|
1168
|
-
bus="tmux 宿主但未定位到该会话的 pane(标题契约 / 名册 tmuxPane / 仓库兜底均未命中)"
|
|
1169
|
-
else
|
|
1170
|
-
bus="非 tmux 宿主,无法确认是否在跑 turn"
|
|
1171
|
-
fi
|
|
1172
|
-
fi
|
|
1173
1249
|
if [[ -n "$bus" && "$force" != true ]]; then
|
|
1174
1250
|
warn "拒绝回收 $name:$bus"
|
|
1175
1251
|
warn " 等它停;回执与产物核对见 checklists/pm.md「会话回收 / 收窗核对」(或 --force 强制)"
|
|
1176
1252
|
return 1
|
|
1177
1253
|
fi
|
|
1178
1254
|
|
|
1179
|
-
|
|
1255
|
+
# 回收前以 pane 真实 cwd 校正日志中的仓(名册可能缺记录)
|
|
1256
|
+
local pcwd
|
|
1257
|
+
pcwd="$(tmux display-message -p -t "$pane" '#{pane_current_path}' 2>/dev/null || true)"
|
|
1258
|
+
[[ -n "$pcwd" ]] && repo="$pcwd"
|
|
1259
|
+
if tmux kill-pane -t "$pane" 2>/dev/null; then
|
|
1180
1260
|
log "已回收 $name(tmux pane $pane)@ $repo"; return 0
|
|
1181
1261
|
fi
|
|
1182
|
-
if command -v tmux >/dev/null 2>&1 && tmux has-session -t "nao-$name" 2>/dev/null && tmux kill-session -t "nao-$name" 2>/dev/null; then
|
|
1183
|
-
log "已回收 $name(tmux 会话 nao-$name)@ $repo"; return 0
|
|
1184
|
-
fi
|
|
1185
|
-
if command -v screen >/dev/null 2>&1 && screen -ls 2>/dev/null | grep -q "nao-$name" && screen -S "nao-$name" -X quit 2>/dev/null; then
|
|
1186
|
-
log "已回收 $name(screen 会话 nao-$name)@ $repo"; return 0
|
|
1187
|
-
fi
|
|
1188
1262
|
pids="$(pi_pids_for "$name" "$repo" | tr '\n' ' ')"
|
|
1189
1263
|
if [[ -n "$pids" ]] && kill $pids 2>/dev/null; then
|
|
1190
1264
|
log "已回收 $name(结束进程: $pids)@ $repo"; return 0
|
|
1191
1265
|
fi
|
|
1192
|
-
warn "
|
|
1266
|
+
warn "已定位到 $name 的 pane $pane 但回收失败(pane 可能刚好退出;核对 pane 已消失后可忽略)"; return 1
|
|
1193
1267
|
}
|
|
1194
1268
|
|
|
1195
1269
|
# ---------------------------------------------------------------------------
|
|
@@ -31,7 +31,7 @@ nao 是一套「多角色 AI 开发舰队」:6 个分工明确的常驻会话
|
|
|
31
31
|
2. **拉起舰队**:`bash .agents/scripts/nao-fleet.sh ensure pm arch-designer rd-fe rd-be qa rd-infra`
|
|
32
32
|
- 任务派生(并行隔离):`ensure --task <编号> <别名>`
|
|
33
33
|
- 状态 / 回收:`status` / `close --task <编号> <别名>`
|
|
34
|
-
3. **迁移旧版安装**:`npx @nathan33/nao-skill migrate`(旧文件备份到 `.agents/.nao-obsolete
|
|
34
|
+
3. **迁移旧版安装**:`npx @nathan33/nao-skill migrate`(旧文件备份到 `.agents/.nao-obsolete/`)。是否写 shim 由**判定式**决定(旧脚本 / 已有 shim / 全仓 `nao-fleet|nao-skill|NAO_SKILLS` 引用 ⇒ 写);纯文档仓判定为跳过并显式告知,可用 `--shim` / `--no-shim` 覆盖。
|
|
35
35
|
|
|
36
36
|
## 硬约束(勿违反)
|
|
37
37
|
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
|
|
37
37
|
## 指针(勿复制内容)
|
|
38
38
|
|
|
39
|
-
> 项目内 `.agents/` **只有 shim**(`.agents/scripts/nao-fleet.sh`);机制单一事实来源在 **nao 机制包**内,包根由 `NAO_SKILLS` 给出(fleet 注入到会话系统提示)。所有机制引用一律以 `$NAO_SKILLS/.agents/`
|
|
39
|
+
> 项目内 `.agents/` **只有 shim**(`.agents/scripts/nao-fleet.sh`);机制单一事实来源在 **nao 机制包**内,包根由 `NAO_SKILLS` 给出(fleet 注入到会话系统提示)。所有机制引用一律以 `$NAO_SKILLS/.agents/` 为根解析。**例外:无脚本、无引用的纯文档仓不建 shim**(迁移判定式自动跳过,可用 `--shim` 覆盖)。
|
|
40
40
|
|
|
41
41
|
- 团队级协议/角色清单:`$NAO_SKILLS/.agents/common/` + `$NAO_SKILLS/.agents/roles.yaml`
|
|
42
42
|
- 角色卡:`$NAO_SKILLS/.agents/prompts/`
|
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ nao-skills 是一套 **角色卡 + 协作规矩 + 小工具**。装上以后,
|
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
26
|
cd 你的项目
|
|
27
|
-
pi install --local npm:@nathan33/nao-skill@0.
|
|
27
|
+
pi install --local npm:@nathan33/nao-skill@0.13.0 # 声明 pin + 把机制包物化到项目内 .pi/npm
|
|
28
28
|
npx @nathan33/nao-skill init # 写入转发 shim + 生成 AGENTS.md
|
|
29
29
|
```
|
|
30
30
|
|
|
@@ -39,9 +39,11 @@ nao-skills 是一套 **角色卡 + 协作规矩 + 小工具**。装上以后,
|
|
|
39
39
|
**老项目迁移**(曾在项目里铺开整套 `.agents/` 的):
|
|
40
40
|
|
|
41
41
|
```bash
|
|
42
|
-
npx @nathan33/nao-skill migrate # 旧文件备份到 .agents/.nao-obsolete
|
|
42
|
+
npx @nathan33/nao-skill migrate # 旧文件备份到 .agents/.nao-obsolete/,再按需写入转发入口
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
+
迁移默认按**判定式**决定是否写转发入口(shim):仓里还有旧的 `nao-fleet.sh`、或已存在 shim、或全仓有 `nao-fleet` / `nao-skill` / `NAO_SKILLS` 引用,就写;**无脚本、无引用的纯文档仓会被判定跳过**,并打印一行告知。要改变判定结果,显式加 `--shim`(强制写)或 `--no-shim`(强制不写),两者不能同时给(`exit 2`)。旧版的 `.agents/.nao-migrated` 标记**已废弃**:迁移不再读写它,可安全删除。`.agents/.nao-version` 的语义是「本机已安装机制包版本」(供升级检测),**不表达迁移状态**——迁移状态由「是否还有旧资产」推断。
|
|
46
|
+
|
|
45
47
|
## 用起来(3 步)
|
|
46
48
|
|
|
47
49
|
```bash
|
|
@@ -49,6 +51,8 @@ npx @nathan33/nao-skill migrate # 旧文件备份到 .agents/.nao-obsolete/,
|
|
|
49
51
|
.agents/scripts/nao-fleet.sh ensure pm arch rd-fe rd-be qa rd-infra # 拉起 6 个角色会话
|
|
50
52
|
```
|
|
51
53
|
|
|
54
|
+
角色名可以写**别名**或**规范名**(两者等价):`arch` = `arch-designer`、`infra` = `rd-infra`;上例里两者混用也照常拉起。完整别名表见 `.agents/roles.yaml` 或 `nao-fleet.sh --help`。
|
|
55
|
+
|
|
52
56
|
然后**在 PM 会话里说需求**就行,剩下的它自己流转。大概长这样:
|
|
53
57
|
|
|
54
58
|
```text
|
|
@@ -104,7 +108,7 @@ $NAO_SKILLS/.agents/scripts/qq-notify --dry-run "连通性自检" # 只取
|
|
|
104
108
|
|
|
105
109
|
**它会在 GitHub 上开 PR 吗?** 会——每个需求一条分支、一个 PR(Draft 早开、CI 早跑),你确认验收后由研发角色 squash 合并,main 上只留 1 条可读提交。没有远端或 `gh` 时降级为本地分支,流程不变。
|
|
106
110
|
|
|
107
|
-
**跑完的会话要留着吗?** 后台派生会话验收完可以回收:`nao-fleet.sh close --task T1 rd-be
|
|
111
|
+
**跑完的会话要留着吗?** 后台派生会话验收完可以回收:`nao-fleet.sh close --task T1 rd-be`(等价写法 `nao-fleet.sh close rd-be-T1`,直接用派生会话名即可);常驻角色留着复用。`nao-fleet.sh status` 会提示哪些是残留。目标在别的项目里也能回收——`close` 会按名册/窗口标题跨项目定位;定位不到时会**非 0 退出并打印诊断**,不会假装已回收。
|
|
108
112
|
|
|
109
113
|
**终端窗口太挤怎么办?** tmux 宿主支持三种布局:默认 `main-row2`(主会话占左,其余往右分列)、`main-col`(其余在右列竖排)、`grid`(等大网格)。窗口太窄时 `main-row2` 会**自动回退** `main-col`、仍不够再退 `grid`,并打印带数字的提示(不中断拉起);用 `NAO_TMUX_LAYOUT` / `NAO_TMUX_MAIN_WIDTH` / `NAO_TMUX_MIN_PANE_WIDTH` 调整,详见 `bash .agents/scripts/nao-fleet.sh --help`。
|
|
110
114
|
|
|
@@ -132,7 +136,7 @@ $NAO_SKILLS/.agents/scripts/qq-notify --dry-run "连通性自检" # 只取
|
|
|
132
136
|
|
|
133
137
|
- **旧的完整性判据已失效**:以前下游靠「项目内 `.agents/**` 与上游逐字节 sha 一致」证明机制没漏没改;现在项目内不再有机制副本,这条判据不再适用。
|
|
134
138
|
- **新的完整性判据:以 pin 的包版本为准**。`.pi/settings.json` 记录版本,`pi install/update` 从包物化;包里是什么就是什么,不存在「项目内被就地改动」的问题。转发入口是生成的固定文本,可用 `npx @nathan33/nao-skill init` 幂等重写核对。
|
|
135
|
-
- **版本权威口径**:`.pi/settings.json` 里的 pin 是**唯一权威**(`pi install/update` 据此物化到 `.pi/npm`);`.agents/.nao-version`
|
|
139
|
+
- **版本权威口径**:`.pi/settings.json` 里的 pin 是**唯一权威**(`pi install/update` 据此物化到 `.pi/npm`);`.agents/.nao-version` 的语义是**本机已安装机制包版本**(供升级检测),**不表达迁移状态**、**不作为版本依据**;两者不一致时**以 pin 为准**。
|
|
136
140
|
- **fmt 边界**:包内 `.agents/**` 仍以上游原始存储形态发布(部分 YAML / Markdown / MTS 未按 oxfmt / prettier 归一)。迁移期仍持有旧版全套 `.agents/**` 的仓库,继续把 `.agents/**` 排除出 fmt 校验即可,⛔ **不要为过 fmt 就地格式化**;新形态项目内只有转发入口,无需再处理。
|
|
137
141
|
|
|
138
142
|
### 物化与离线(CI / 无 pi 环境必读)
|
|
@@ -147,7 +151,7 @@ $NAO_SKILLS/.agents/scripts/qq-notify --dry-run "连通性自检" # 只取
|
|
|
147
151
|
|
|
148
152
|
- **迁移期零改动**:旧的全套 `.agents/scripts/nao-fleet.sh` 照常可用,这些仓库的 CI 目前不调用 `check`,不受影响。
|
|
149
153
|
- 想切到新形态时,在各仓执行 `npx @nathan33/nao-skill migrate`:只删**已知 nao 资产**(共享的 `.agents/skills/` 里项目自有的技能会保留),并把 `skills-lock.json` 里重复的 `frontend-design` 来源收敛为「机制包唯一来源」。
|
|
150
|
-
-
|
|
154
|
+
- **纯文档仓(如 `nao-todo-minimal`)特例**:无脚本、无 `nao-fleet`/`nao-skill` 引用 ⇒ 迁移判定为**纯文档迁移,不建转发入口**(可显式 `--shim` 强制;也可 `--no-shim` 强制不建);它的 `.agents/commands/`、`.agents/prompts/` 不在 pi 的 skill 扫描面(pi 只扫 `.agents/skills/**`)。
|
|
151
155
|
|
|
152
156
|
## 想深入
|
|
153
157
|
|
package/bin/nao-skill.js
CHANGED
|
@@ -42,8 +42,6 @@ const SHIM_SRC = fileURLToPath(new URL('./shim/nao-fleet.sh', import.meta.url));
|
|
|
42
42
|
const OBSOLETE = ['.agents/skills/checklists'];
|
|
43
43
|
// 安装版本标记(install/update 写入,供下次检测升级)
|
|
44
44
|
const VERSION_FILE = '.agents/.nao-version';
|
|
45
|
-
// 迁移一次性提示标记(BR4:每版本最多提示一次)
|
|
46
|
-
const MIGRATED_FILE = '.agents/.nao-migrated';
|
|
47
45
|
|
|
48
46
|
// 旧版全套安装的已知 nao 资产(迁移时**精准**备份+移除;共享目录不得整目录删)
|
|
49
47
|
const LEGACY_TOP_DIRS = ['prompts', 'common', 'checklists', 'templates'];
|
|
@@ -287,10 +285,67 @@ function detectLegacyAssets(target) {
|
|
|
287
285
|
return hits;
|
|
288
286
|
}
|
|
289
287
|
|
|
288
|
+
// ---------------------------------------------------------------------------
|
|
289
|
+
// F4 shim 判定式:needsShim = A ∨ A′ ∨ B(Node 实现,无 shell grep / 无网络时间依赖)
|
|
290
|
+
// A 旧版真脚本存在 · A′ 已存在 shim · B 存在外部调用点
|
|
291
|
+
// IGNORE = .git/** · node_modules/** · .pi/** · .agents/.nao-obsolete/** · 二进制 · >1MiB
|
|
292
|
+
// B 额外排除待移除旧资产(legacyAssetPaths);.pi/** 必须排除,否则已物化仓恒真
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
const SHIM_SCAN_IGNORE_TOP = new Set(['.git', 'node_modules', '.pi']);
|
|
295
|
+
const SHIM_REF_RE = /nao-fleet|nao-skill|NAO_SKILLS/;
|
|
296
|
+
|
|
297
|
+
function detectShimNeed(target) {
|
|
298
|
+
const shim = join(target, '.agents', 'scripts', 'nao-fleet.sh');
|
|
299
|
+
if (existsSync(shim)) return { needed: true, reason: isShimFile(shim) ? 'shim-present' : 'legacy-script' };
|
|
300
|
+
const legacyRel = detectLegacyAssets(target);
|
|
301
|
+
const isLegacyPath = (rel) => legacyRel.some((l) => (l.endsWith('/') ? rel.startsWith(l) : rel === l));
|
|
302
|
+
const stack = [target];
|
|
303
|
+
while (stack.length) {
|
|
304
|
+
const dir = stack.pop();
|
|
305
|
+
let entries;
|
|
306
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { continue; }
|
|
307
|
+
for (const e of entries) {
|
|
308
|
+
const p = join(dir, e.name);
|
|
309
|
+
const rel = relative(target, p);
|
|
310
|
+
if (e.isSymbolicLink()) continue;
|
|
311
|
+
if (e.isDirectory()) {
|
|
312
|
+
if (SHIM_SCAN_IGNORE_TOP.has(rel.split('/')[0]) || e.name === 'node_modules') continue;
|
|
313
|
+
if (rel === '.agents/.nao-obsolete' || rel.startsWith('.agents/.nao-obsolete/')) continue;
|
|
314
|
+
stack.push(p);
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
if (!e.isFile() || isLegacyPath(rel)) continue;
|
|
318
|
+
let st;
|
|
319
|
+
try { st = statSync(p); } catch { continue; }
|
|
320
|
+
if (st.size > 1024 * 1024) continue;
|
|
321
|
+
let buf;
|
|
322
|
+
try { buf = readFileSync(p); } catch { continue; }
|
|
323
|
+
if (buf.includes(0)) continue; // 二进制(含 NUL 字节)
|
|
324
|
+
if (SHIM_REF_RE.test(buf.toString('utf8'))) return { needed: true, reason: `reference:${rel}` };
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return { needed: false, reason: 'no-reference' };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function resolveShim(target, mode) {
|
|
331
|
+
if (mode === 'shim') return { needed: true, reason: 'forced' };
|
|
332
|
+
if (mode === 'no-shim') return { needed: false, reason: 'forced-off' };
|
|
333
|
+
return detectShimNeed(target);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function applyShim(target, decision, verbose) {
|
|
337
|
+
if (decision.needed) { installShim(target, verbose); return; }
|
|
338
|
+
if (decision.reason === 'forced-off') {
|
|
339
|
+
warn('按 --no-shim 跳过 shim 安装(项目内将没有 .agents/scripts/nao-fleet.sh;需要时用 --shim 覆盖)。');
|
|
340
|
+
} else {
|
|
341
|
+
warn('未发现 nao 脚本/引用 → 判定为纯文档迁移,未写 shim(可用 --shim 强制)。');
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
290
345
|
// ---------------------------------------------------------------------------
|
|
291
346
|
// init:干净项目 —— 写 shim + AGENTS.md(不复制机制;机制在 .pi/npm 包内)
|
|
292
347
|
// ---------------------------------------------------------------------------
|
|
293
|
-
function init(target, force, verbose) {
|
|
348
|
+
function init(target, force, verbose, shimMode = 'shim', decision = null) {
|
|
294
349
|
const legacy = detectLegacyAssets(target);
|
|
295
350
|
if (legacy.length) {
|
|
296
351
|
if (!force) {
|
|
@@ -301,9 +356,9 @@ function init(target, force, verbose) {
|
|
|
301
356
|
}
|
|
302
357
|
// F2:--force 不得绕过迁移守卫(否则 shim 覆盖 + 旧副本残留 = 混装,违 BR5/AC1 双注册)
|
|
303
358
|
log('init --force 检测到旧版 .agents/:改走 migrate(备份 + 精准移除 + shim),避免混装。');
|
|
304
|
-
return migrate(target, true, verbose);
|
|
359
|
+
return migrate(target, true, verbose, shimMode === 'no-shim' ? 'no-shim' : 'shim');
|
|
305
360
|
}
|
|
306
|
-
|
|
361
|
+
applyShim(target, decision || resolveShim(target, shimMode), verbose);
|
|
307
362
|
writeVersion(target);
|
|
308
363
|
if (!hasNaoPackageDeclared(target)) {
|
|
309
364
|
warn(`未在 .pi/settings.json 发现 npm:${PKG.name} 声明;请先执行 pi install --local npm:${PKG.name}@<pin>。`);
|
|
@@ -399,69 +454,82 @@ function removeJsonProperty(raw, key) {
|
|
|
399
454
|
while (vs < raw.length && /\s/.test(raw[vs])) vs++;
|
|
400
455
|
const ve = findJsonValueEnd(raw, vs);
|
|
401
456
|
if (ve < 0) return null;
|
|
402
|
-
//
|
|
457
|
+
// 行级删除(保缩进/其余字节):仅当该属性独占一行(行首即 key)时适用
|
|
403
458
|
let lineStart = m.index;
|
|
404
459
|
while (lineStart > 0 && raw[lineStart - 1] !== '\n') lineStart--;
|
|
405
460
|
let nl = raw.indexOf('\n', lineStart);
|
|
406
461
|
if (nl === -1) nl = raw.length;
|
|
407
|
-
if (
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
462
|
+
if (raw.slice(lineStart, nl).trim().startsWith(`"${key}"`)) {
|
|
463
|
+
// 本属性值闭合所在行之后(含换行)
|
|
464
|
+
let lineEnd = ve;
|
|
465
|
+
while (lineEnd < raw.length && raw[lineEnd] !== '\n') lineEnd++;
|
|
466
|
+
if (lineEnd < raw.length) lineEnd++;
|
|
467
|
+
// 是否还有后续属性(值后跟逗号)
|
|
468
|
+
let after = ve;
|
|
469
|
+
while (after < raw.length && (raw[after] === ' ' || raw[after] === '\t')) after++;
|
|
470
|
+
if (raw[after] === ',') {
|
|
471
|
+
return raw.slice(0, lineStart) + raw.slice(lineEnd);
|
|
472
|
+
}
|
|
473
|
+
// 末位属性:删本行,并去掉前一属性末尾的逗号
|
|
474
|
+
let out = raw.slice(0, lineStart) + raw.slice(lineEnd);
|
|
475
|
+
let p = lineStart;
|
|
476
|
+
while (p > 0 && /[ \t\r\n]/.test(out[p - 1])) p--;
|
|
477
|
+
if (out[p - 1] === ',') out = out.slice(0, p - 1) + out.slice(p);
|
|
478
|
+
return out;
|
|
479
|
+
}
|
|
480
|
+
// F7 属性级回退(单行/异形格式):删「属性 token + 一个分隔逗号」,其余字节不动
|
|
413
481
|
let after = ve;
|
|
414
|
-
while (after < raw.length && (raw[after]
|
|
415
|
-
if (raw[after] === ',')
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
while (p > 0 && /[ \t\r\n]/.test(out[p - 1])) p--;
|
|
422
|
-
if (out[p - 1] === ',') out = out.slice(0, p - 1) + out.slice(p);
|
|
423
|
-
return out;
|
|
482
|
+
while (after < raw.length && /\s/.test(raw[after])) after++;
|
|
483
|
+
if (raw[after] === ',') return raw.slice(0, m.index) + raw.slice(after + 1);
|
|
484
|
+
// 末位属性:删前一个分隔逗号 + 本属性 token
|
|
485
|
+
let p = m.index;
|
|
486
|
+
while (p > 0 && /[ \t\r\n]/.test(raw[p - 1])) p--;
|
|
487
|
+
if (raw[p - 1] === ',') return raw.slice(0, p - 1) + raw.slice(ve);
|
|
488
|
+
return null;
|
|
424
489
|
}
|
|
425
490
|
|
|
426
491
|
// §12-C:nao 包为唯一来源 —— 移除 skills-lock.json 里重复的 frontend-design 条目(原文件先备份)
|
|
427
492
|
// F1:文本级删除单条,保留原缩进与其余字节(4 空格 / 2 空格均不重排)
|
|
493
|
+
// F7:三态返回 ok | absent | failed;failed 时恰一行 warn(不静默)且不改写
|
|
428
494
|
function stripLockDuplicate(target, stamp, verbose) {
|
|
429
495
|
const lock = join(target, 'skills-lock.json');
|
|
430
|
-
if (!existsSync(lock)) return
|
|
496
|
+
if (!existsSync(lock)) return 'absent';
|
|
431
497
|
const raw = readFileSync(lock, 'utf8');
|
|
432
498
|
let data;
|
|
433
|
-
try { data = JSON.parse(raw); } catch { return
|
|
434
|
-
if (!data.skills || !data.skills['frontend-design']) return
|
|
499
|
+
try { data = JSON.parse(raw); } catch { return 'absent'; }
|
|
500
|
+
if (!data.skills || !data.skills['frontend-design']) return 'absent';
|
|
501
|
+
const failWarn = () => warn('skills-lock.json 存在重复 frontend-design,但无法安全文本删除:请手工移除该条目;其余迁移步骤不受影响。');
|
|
435
502
|
const next = removeJsonProperty(raw, 'frontend-design');
|
|
436
|
-
if (next == null) return
|
|
503
|
+
if (next == null) { failWarn(); return 'failed'; }
|
|
437
504
|
try {
|
|
438
505
|
const parsed = JSON.parse(next);
|
|
439
|
-
if (!parsed.skills || 'frontend-design' in parsed.skills) return
|
|
440
|
-
} catch { return
|
|
506
|
+
if (!parsed.skills || 'frontend-design' in parsed.skills) { failWarn(); return 'failed'; }
|
|
507
|
+
} catch { failWarn(); return 'failed'; }
|
|
441
508
|
const bak = join(target, '.agents', '.nao-obsolete', stamp, 'skills-lock.json');
|
|
442
509
|
mkdirSync(join(bak, '..'), { recursive: true });
|
|
443
510
|
cpSync(lock, bak);
|
|
444
511
|
writeFileSync(lock, next);
|
|
445
512
|
if (verbose) log(' strip skills-lock.json → 文本级移除 frontend-design(其余字节不变)');
|
|
446
|
-
return
|
|
513
|
+
return 'ok';
|
|
447
514
|
}
|
|
448
515
|
|
|
449
|
-
function migrate(target, force, verbose) {
|
|
516
|
+
function migrate(target, force, verbose, shimMode = 'auto') {
|
|
450
517
|
const legacy = detectLegacyAssets(target);
|
|
451
518
|
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
519
|
+
// F4 判定式必须在备份/移除之前采样(B 需排除待移除旧资产)
|
|
520
|
+
const decision = resolveShim(target, shimMode);
|
|
521
|
+
// B2:去重置于早退判断之前 —— 无 legacy 资产(走 init 路径)时同样生效
|
|
522
|
+
stripLockDuplicate(target, stamp, verbose);
|
|
452
523
|
if (!legacy.length) {
|
|
453
524
|
log('未发现旧版全套 .agents/ 机制副本;按 init 处理。');
|
|
454
|
-
return init(target, force, verbose);
|
|
525
|
+
return init(target, force, verbose, shimMode, decision);
|
|
455
526
|
}
|
|
456
527
|
log(`迁移:发现 ${legacy.length} 处旧版 nao 资产,备份到 .agents/.nao-obsolete/${stamp}/ 后移除。`);
|
|
457
528
|
const removed = migrateLegacy(target, stamp, verbose);
|
|
458
|
-
|
|
459
|
-
installShim(target, verbose);
|
|
529
|
+
applyShim(target, decision, verbose);
|
|
460
530
|
writeVersion(target);
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
log(`✔ 迁移完成:移除 ${removed.length} 项已知 nao 资产(备份在 .agents/.nao-obsolete/${stamp}/);shim 已就位。`);
|
|
464
|
-
log(' 旧命令照跑:bash .agents/scripts/nao-fleet.sh check / ensure ...');
|
|
531
|
+
log(`✔ 迁移完成:移除 ${removed.length} 项已知 nao 资产(备份在 .agents/.nao-obsolete/${stamp}/)。`);
|
|
532
|
+
if (decision.needed) log(' 旧命令照跑:bash .agents/scripts/nao-fleet.sh check / ensure ...');
|
|
465
533
|
}
|
|
466
534
|
|
|
467
535
|
// ---------------------------------------------------------------------------
|
|
@@ -484,8 +552,8 @@ function help() {
|
|
|
484
552
|
console.log(`nao-skill v${PKG.version} — nao 多角色 AI 开发舰队(pi 原生分发)
|
|
485
553
|
|
|
486
554
|
用法:
|
|
487
|
-
nao-skill init [dir] [-v]
|
|
488
|
-
nao-skill migrate [dir] [-v]
|
|
555
|
+
nao-skill init [dir] [-v] [--shim|--no-shim] 干净项目:写 shim + AGENTS.md(默认写)
|
|
556
|
+
nao-skill migrate [dir] [-v] [--shim|--no-shim] 迁移旧版全套 .agents/:备份 .nao-obsolete/ + 移除 + shim
|
|
489
557
|
nao-skill exec <参数...> 转发给包内 nao-fleet.sh(CI/脚本用;NAO_SKILLS=包根)
|
|
490
558
|
nao-skill plugins list 列出已知 pi 插件与安装状态
|
|
491
559
|
nao-skill plugins install <名...> 安装指定插件(intercom/ask-me/subagents/web-access/codegraph)
|
|
@@ -493,6 +561,11 @@ function help() {
|
|
|
493
561
|
nao-skill --version 显示版本
|
|
494
562
|
nao-skill --help 显示本帮助
|
|
495
563
|
|
|
564
|
+
shim 判定式(migrate 默认;init 默认装):
|
|
565
|
+
needsShim = A ∨ A′ ∨ B(Node 扫描,跳过 >1MiB 与二进制,排除 .git/.pi/node_modules/.nao-obsolete)
|
|
566
|
+
A 旧版真脚本存在 · A′ 已存在 shim · B 全仓有 nao-fleet|nao-skill|NAO_SKILLS 引用(排除待移除旧资产)
|
|
567
|
+
判定为纯文档迁移时不写 shim,并打印显式告知;用 --shim / --no-shim 覆盖(互斥 → exit 2)
|
|
568
|
+
|
|
496
569
|
兼容(旧流程,建议迁移到 init/migrate):
|
|
497
570
|
nao-skill install [dir] [--force] 复制全套 .agents/ 到项目(--plugins 顺带装插件)
|
|
498
571
|
nao-skill update [dir] 源优先覆盖升级旧版全套安装
|
|
@@ -517,20 +590,29 @@ if (cmd === '--version' || cmd === '-v' || cmd === 'version') {
|
|
|
517
590
|
let target = process.cwd();
|
|
518
591
|
let force = false;
|
|
519
592
|
let verbose = false;
|
|
593
|
+
let wantShim = false;
|
|
594
|
+
let wantNoShim = false;
|
|
520
595
|
const rest = args.slice(1);
|
|
521
596
|
for (let i = 0; i < rest.length; i++) {
|
|
522
597
|
if (rest[i] === '--force' || rest[i] === '-f') force = true;
|
|
523
598
|
else if (rest[i] === '--verbose' || rest[i] === '-v') verbose = true;
|
|
599
|
+
else if (rest[i] === '--shim') wantShim = true;
|
|
600
|
+
else if (rest[i] === '--no-shim') wantNoShim = true;
|
|
524
601
|
else if (rest[i] === '--target') target = resolve(rest[++i]);
|
|
525
602
|
else if (!rest[i].startsWith('-')) target = resolve(rest[i]);
|
|
526
603
|
else { warn(`未知参数: ${rest[i]}`); process.exit(2); }
|
|
527
604
|
}
|
|
605
|
+
if (wantShim && wantNoShim) {
|
|
606
|
+
warn('--shim 与 --no-shim 互斥,不能同时使用。');
|
|
607
|
+
process.exit(2);
|
|
608
|
+
}
|
|
528
609
|
if (!statIsDir(target)) {
|
|
529
610
|
warn(`目标目录无效: ${target}`);
|
|
530
611
|
process.exit(2);
|
|
531
612
|
}
|
|
532
|
-
|
|
533
|
-
|
|
613
|
+
const shimMode = wantShim ? 'shim' : wantNoShim ? 'no-shim' : (cmd === 'init' ? 'shim' : 'auto');
|
|
614
|
+
if (cmd === 'init') init(target, force, verbose, shimMode);
|
|
615
|
+
else migrate(target, force, verbose, shimMode);
|
|
534
616
|
} else if (cmd === 'exec') {
|
|
535
617
|
execFleet(args.slice(1));
|
|
536
618
|
} else if (cmd === 'install' || cmd === 'i') {
|