Files
nodeagent/apps/cli/src/options.ts
T
jkooe 6b2fa29710 feat(cli): nodeagent rollback —— 从 LKG 回滚被控端(ARCH T03 / PRD P0-2)
- 新增 apps/cli/src/cmd/rollback.ts:读 LKG → 指纹比对定位备份 →
  覆盖写入口(同 deploy 姿势)→ 写后校验(失败用回滚源还原)→
  受控重启 → probeOnce 复核 build.hash == LKG(90s 墙钟)
- 纯函数 planRollback/pickBackupByHash/hashMatches/backupNameRegex +
  流式 hashRemoteFile(client 注入,可假测);hash_full 全量优先、
  老记录回落 12 位前缀(A-P0-3/R-2 向后兼容)
- --dry-run 只读零写;--backup 逃生门直指备份路径;退出码 0/1/2 分明
- HELP/README 写明能力圈边界:RPC 命令需被控端可达,「完全起不来」
  走看门狗自动恢复或带外恢复;不提供自动回滚(DELIVERY-PIPELINE §6)
- 19 条单测:无 LKG 零调用 / already-lkg 零写 / 写后校验失败自动还原 /
  无匹配零写 / dry-run 只读 / 多候选 mtime 新→早 / Windows 路径与元字符
2026-10-25 23:45:02 +08:00

228 lines
9.4 KiB
TypeScript
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.
import { closeSync, existsSync, openSync, readFileSync, readSync, renameSync, statSync, writeFileSync, writeSync } from 'node:fs';
import fsSync from 'node:fs';
import net from 'node:net';
import os from 'node:os';
import path, { join } from 'node:path';
import { parseArgs } from 'node:util';
import {
NodeAgentClient,
loadMacroFile,
runMacro,
ClientError,
loadConfig,
saveConfig,
configPath,
toWsUrl,
loadKeys,
createKeys,
keysFilePath,
discoverOnce,
resolveTarget,
resolveNodeSelector,
emptyConfig,
type ClientConfig,
type ResolvedTarget,
type NodeProfile,
} from '@nodeagent/client';
import {
CapabilityNames,
matchPattern,
DEFAULT_DISCOVERY_PORT,
type CapabilityDescriptor,
type InvokeResult,
} from '@nodeagent/protocol';
import { type Options } from './types.js';
// 命令行参数解析(从 index.ts 拆出)
export function parseOptions(rest: string[]): { opts: Options; positionals: string[] } {
const { values, positionals } = parseArgs({
args: rest,
options: {
// ⚠️ 这两个是 L-01 同类问题的「漏网之鱼」,由 cli-options 守卫扫出(v2.0.3):
// `--help` 原来靠 positionals[0]===undefined 偶然命中;`--no-tls` 则绕过解析器
// 直接在 cmd/conn.ts 里扫 `process.argv`。两者都是「能跑,但没接进解析器」。
help: { type: 'boolean', default: false },
'no-tls': { type: 'boolean', default: false },
json: { type: 'boolean', default: false },
insecure: { type: 'boolean', default: false },
port: { type: 'string' },
key: { type: 'string' },
id: { type: 'string' },
limit: { type: 'string' },
args: { type: 'string' },
config: { type: 'boolean', default: false },
// v2 图形操作
out: { type: 'string' },
format: { type: 'string' },
scale: { type: 'string' },
region: { type: 'string' },
button: { type: 'string' },
duration: { type: 'string' },
interval: { type: 'string' },
tail: { type: 'boolean' },
compare: { type: 'boolean' },
'anchor-path': { type: 'string' },
note: { type: 'string' },
level: { type: 'string' },
target: { type: 'string' },
source: { type: 'string' },
where: { type: 'string' },
any_of: { type: 'string' },
title: { type: 'string' },
text: { type: 'string' },
process: { type: 'string' },
state: { type: 'string' },
condition: { type: 'string' },
// 媒体键定向投递的目标进程 PID(v1.2)
pid: { type: 'string' },
hold: { type: 'string' },
route: { type: 'string' },
'auth-mode': { type: 'string' },
since: { type: 'string' },
type: { type: 'string' },
'client-id': { type: 'string' },
wait: { type: 'string' },
'discovery-port': { type: 'string' },
node: { type: 'string' },
name: { type: 'string' },
recursive: { type: 'boolean', default: false },
pattern: { type: 'string' },
'create-dirs': { type: 'boolean', default: false },
'hub-token': { type: 'string' },
'hub-node': { type: 'string' },
// v7 受控重启
delay: { type: 'string' },
// v10 后台任务 / 剪贴板
'timeout-ms': { type: 'string' },
// ⚠️ L-01(v2.0.3 修):`--timeout` 曾经**没在这里声明**。因为 `strict: false`,
// 未声明选项会被当成 boolean 塞进 `values.timeout`(`true`),而它的值
// `30000` 则掉进 `positionals` —— 于是 `--timeout` 静默失效,
// 并且**污染位置参数**(可能把子命令/主机名解析错)。
// 帮助文本与 `cmd/gui.ts` 的 await 命令用的都是 `--timeout`,故必须显式声明。
timeout: { type: 'string' },
kill: { type: 'boolean', default: false },
offset: { type: 'string' },
set: { type: 'string' },
'image-file': { type: 'string' },
fps: { type: 'string' },
nodes: { type: 'string' },
path: { type: 'string' },
kind: { type: 'string' },
var: { type: 'string', multiple: true },
mode: { type: 'string' },
iface: { type: 'string' },
ip: { type: 'string' },
mask: { type: 'string' },
gateway: { type: 'string' },
dns: { type: 'string' },
command: { type: 'string' },
'confirm-within': { type: 'string' },
'task-name': { type: 'string' },
yes: { type: 'boolean', default: false },
url: { type: 'string' },
sha256: { type: 'string' },
'dry-run': { type: 'boolean', default: false },
psk: { type: 'boolean', default: false },
'discover-secret': { type: 'string' },
'forget-cert': { type: 'boolean', default: false },
check: { type: 'boolean', default: false },
force: { type: 'boolean', default: false },
// v2.1.0 rollback 逃生门(--backup <远端备份路径>)
backup: { type: 'string' },
mute: { type: 'string' },
volume: { type: 'string' },
seconds: { type: 'string' },
'interval-ms': { type: 'string' },
},
allowPositionals: true,
strict: false,
});
return {
opts: {
help: Boolean(values['help']),
noTls: Boolean(values['no-tls']),
json: Boolean(values['json']),
insecure: Boolean(values['insecure']),
port: values['port'] as string | undefined,
key: values['key'] as string | undefined,
id: values['id'] as string | undefined,
limit: values['limit'] as string | undefined,
args: values['args'] as string | undefined,
out: values['out'] as string | undefined,
format: values['format'] as string | undefined,
scale: values['scale'] as string | undefined,
region: values['region'] as string | undefined,
button: values['button'] as string | undefined,
duration: values['duration'] as string | undefined,
interval: values['interval'] as string | undefined,
tail: values['tail'] as boolean | undefined,
compare: values['compare'] as boolean | undefined,
anchorPath: values['anchor-path'] as string | undefined,
note: values['note'] as string | undefined,
level: values['level'] as string | undefined,
target: values['target'] as string | undefined,
source: values['source'] as string | undefined,
where: values['where'] as string | undefined,
any_of: values['any_of'] as string | undefined,
condition: values['condition'] as string | undefined,
state: values['state'] as string | undefined,
process: values['process'] as string | undefined,
text: values['text'] as string | undefined,
title: values['title'] as string | undefined,
pid: values['pid'] as string | undefined,
hold: values['hold'] as string | undefined,
route: values['route'] as string | undefined,
authMode: values['auth-mode'] as string | undefined,
since: values['since'] as string | undefined,
type: values['type'] as string | undefined,
clientId: values['client-id'] as string | undefined,
wait: values['wait'] as string | undefined,
discoveryPort: values['discovery-port'] as string | undefined,
node: values['node'] as string | undefined,
delay: values['delay'] as string | undefined,
// 两个写法都接受:`--timeout` 是帮助文本/`await` 命令里的正式写法,
// `--timeout-ms` 是早期在 `bg` 命令里引入的别名。二者同时给时以 `--timeout` 为准。
timeoutMs: (values['timeout'] ?? values['timeout-ms']) as string | undefined,
kill: Boolean(values['kill']),
offset: values['offset'] as string | undefined,
set: values['set'] as string | undefined,
imageFile: values['image-file'] as string | undefined,
fps: values['fps'] as string | undefined,
nodes: values['nodes'] as string | undefined,
path: values['path'] as string | undefined,
kind: values['kind'] as string | undefined,
vars: (values['var'] as string[] | undefined) ?? [],
mode: values['mode'] as string | undefined,
iface: values['iface'] as string | undefined,
ip: values['ip'] as string | undefined,
mask: values['mask'] as string | undefined,
gateway: values['gateway'] as string | undefined,
dns: values['dns'] as string | undefined,
command: values['command'] as string | undefined,
confirmWithin: values['confirm-within'] as string | undefined,
taskName: values['task-name'] as string | undefined,
yes: Boolean(values['yes']),
url: values['url'] as string | undefined,
sha256: values['sha256'] as string | undefined,
dryRun: Boolean(values['dry-run']),
psk: Boolean(values['psk']),
discoverSecret: values['discover-secret'] as string | undefined,
forgetCert: Boolean(values['forget-cert']),
check: Boolean(values['check']),
force: Boolean(values['force']),
backup: values['backup'] as string | undefined,
mute: values['mute'] as string | undefined,
volume: values['volume'] as string | undefined,
seconds: values['seconds'] as string | undefined,
intervalMs: values['interval-ms'] as string | undefined,
name: values['name'] as string | undefined,
recursive: Boolean(values['recursive']),
pattern: values['pattern'] as string | undefined,
createDirs: Boolean(values['create-dirs']),
hubToken: values['hub-token'] as string | undefined,
hubNode: values['hub-node'] as string | undefined,
},
positionals,
};
}