fix(cli): 程序名只替换模板占位符,不再改写用户数据(M82 r2 复审 P2)
r1 把 `rename` 套在**插值后的整条文本**上,于是用户数据被改坏:账户名恰好是 `ull` 会变成
`unlucky-launcher`,路径 `D:/tmp/ull/.ULL/...` 会变成 `D:/tmp/unlucky-launcher/.ULL/...`,
回显的子命令 `ull` 也会被改名。
改成模板占位符:文案里写 `%prog%`,**只在渲染时**(print_report / print_error / 帮助)替换成
argv[0] 的 basename。占位符只存在于静态模板里,插值进来的账户名 / 路径 / 子命令碰不到它。
用 `%prog%` 而不是 `{prog}`:这些模板要活在 `format!` 里,`{prog}` 会被 format! 当成参数名
编译失败。
同时补上 r1 漏的反方向:**stdout 的正常输出**(Report.human 与 warnings)现在也做替换,
以前只有 stderr 的错误做了,两边不一致。
回归测试:`user_data_that_says_ull_is_never_rewritten` 断言「账户名 `ull`」「路径 `\ull\`」
「未知命令 `ull`」在 fill 之后逐字不变。
占位符替换覆盖 21 个文件的 63 处文案;测试夹具里形如 `ull-mcp-diagnose-<pid>` 的临时目录名
已回滚为字面量(那不是文案)。
This commit is contained in:
1 parent
c9e6236a87
commit
2f8bbd1746
20 files changed
+99
-105
No files matched your search
+19
-19
@@ -7,10 +7,10 @@
|
||||
#[must_use]
|
||||
pub const fn general() -> &'static str {
|
||||
"\
|
||||
ull · Unlucky-Launcher 命令行(D18)
|
||||
%prog% · Unlucky-Launcher 命令行(D18)
|
||||
|
||||
用法:
|
||||
ull [全局选项] <命令> [参数]
|
||||
%prog% [全局选项] <命令> [参数]
|
||||
|
||||
命令:
|
||||
instances list 列出实例与已安装版本
|
||||
@@ -55,14 +55,14 @@ pub fn topic(name: &str) -> Option<&'static str> {
|
||||
match name {
|
||||
"instances" => Some(
|
||||
"\
|
||||
ull instances list
|
||||
%prog% instances list
|
||||
|
||||
列出 profiles.json 里的实例与 versions/ 下已安装的版本。
|
||||
--json 输出 {\"data\":{\"instances\":[…],\"installed_versions\":[…],\"data_root\":…}}",
|
||||
),
|
||||
"install" => Some(
|
||||
"\
|
||||
ull install <version> [--loader X] [--loader-version V] [--name N] [--dry-run]
|
||||
%prog% install <version> [--loader X] [--loader-version V] [--name N] [--dry-run]
|
||||
|
||||
解析版本清单与继承链 → 组下载计划 → 落盘(versions/ assets/ libraries/)→ 登记实例
|
||||
(profiles.json + config/instances/<id>.json)。已存在且校验通过的文件会跳过,
|
||||
@@ -81,7 +81,7 @@ versions/<原版>-<加载器>-<版本>/<…>.json(inheritsFrom 原版 + 主类
|
||||
),
|
||||
"launch" => Some(
|
||||
"\
|
||||
ull launch <instance> [--dry-run] [--account <name>] [--java <path>] [--username <name>]
|
||||
%prog% launch <instance> [--dry-run] [--account <name>] [--java <path>] [--username <name>]
|
||||
[--max-memory <MB>]
|
||||
|
||||
<instance> 可以是实例 id、实例名,或直接是版本 id。
|
||||
@@ -92,10 +92,10 @@ accessToken 一律脱敏(输出里出现的是 <redacted:token>)。",
|
||||
),
|
||||
"accounts" => Some(
|
||||
"\
|
||||
ull accounts list
|
||||
ull accounts add offline <name>
|
||||
ull accounts add msa
|
||||
ull accounts remove <name>
|
||||
%prog% accounts list
|
||||
%prog% accounts add offline <name>
|
||||
%prog% accounts add msa
|
||||
%prog% accounts remove <name>
|
||||
|
||||
账户存在 <数据根>/.ULL/data/auth/:accounts.json(可公开元数据)与 credentials.dat(凭据)
|
||||
**分文件**存放,先写凭据后写元数据、原子写、凭据文件 POSIX 侧 0600。
|
||||
@@ -104,25 +104,25 @@ add msa 走微软 device-code(用户码打在 stderr,去 https://www.microso
|
||||
需要先配置 ULL_MSA_CLIENT_ID(Azure 公共客户端 id)。
|
||||
authlib-injector 账户的密码登录留给 GUI(密码进 argv 会泄露,CLI 不开这条通道)。",
|
||||
),
|
||||
"sessions" => Some("ull sessions list [--limit N] 列出会话索引(按开始时间倒序,缺省 20 条)"),
|
||||
"logs" => Some("ull logs <sid> [--tail N] <sid> 允许唯一前缀;输出会话 JSONL 末尾 N 条事件(缺省 200)"),
|
||||
"sessions" => Some("%prog% sessions list [--limit N] 列出会话索引(按开始时间倒序,缺省 20 条)"),
|
||||
"logs" => Some("%prog% logs <sid> [--tail N] <sid> 允许唯一前缀;输出会话 JSONL 末尾 N 条事件(缺省 200)"),
|
||||
"diag" => Some(
|
||||
"ull diag <sid> [--tail N] 把会话的退出信号与日志喂给规则引擎(内嵌规则集),输出结构化诊断",
|
||||
"%prog% diag <sid> [--tail N] 把会话的退出信号与日志喂给规则引擎(内嵌规则集),输出结构化诊断",
|
||||
),
|
||||
"config" => Some(
|
||||
"\
|
||||
ull config get [<key>] [--show-secret]
|
||||
ull config set <key> <value>
|
||||
%prog% config get [<key>] [--show-secret]
|
||||
%prog% config set <key> <value>
|
||||
|
||||
读写 <数据根>/.ULL/config/launcher.json,走 core::setting 的 schema(类型/枚举校验、
|
||||
原子写)。敏感键 get 时脱敏,--show-secret 才给明文;值先按 JSON 解析,失败按字符串。",
|
||||
),
|
||||
"java" => Some(
|
||||
"\
|
||||
ull java list 探测 JAVA_HOME / PATH / 托管目录 / 实例覆盖里的 Java
|
||||
%prog% java list 探测 JAVA_HOME / PATH / 托管目录 / 实例覆盖里的 Java
|
||||
并打印每个 JDK 的实际落点(managed 项带目录)
|
||||
ull java install <major> [--vendor X] 安装托管 JDK(D13/D23):回退链查版本 → 下载 → 解压 → 验证
|
||||
ull java vendors 列出四家发行版的镜像/支持版本/当前优先级顺序
|
||||
%prog% java install <major> [--vendor X] 安装托管 JDK(D13/D23):回退链查版本 → 下载 → 解压 → 验证
|
||||
%prog% java vendors 列出四家发行版的镜像/支持版本/当前优先级顺序
|
||||
|
||||
落点(D23 三模式,随 core.data_mode 与设置键 java.installRoot 走):
|
||||
portable(默认) <数据根>/.ULL/data/java-managed/jdk-<major>-<vendor>/
|
||||
@@ -137,8 +137,8 @@ ull java vendors 列出四家发行版的镜像/支持版
|
||||
),
|
||||
"agent" => Some(
|
||||
"\
|
||||
ull agent serve [--stdio] [--port N] [--host H] [--allow-write|--readonly]
|
||||
ull agent config [--port N]
|
||||
%prog% agent serve [--stdio] [--port N] [--host H] [--allow-write|--readonly]
|
||||
%prog% agent config [--port N]
|
||||
|
||||
把 agent 的工具注册表挂成 **MCP server**(02 文档 §1 的第二形态:同一注册表、同一安全策略、
|
||||
同一审计)。默认走 Streamable HTTP,只绑回环地址(127.0.0.1,默认端口 47823);
|
||||
|
||||
@@ -92,7 +92,8 @@ fn parse_command(rest: &[String], globals: &Globals) -> Result<Command, CliError
|
||||
"accounts" => accounts::parse_accounts(tail),
|
||||
"agent" => agent::parse_agent(tail),
|
||||
other => {
|
||||
Err(CliError::usage(format!("未知命令 `{other}`")).with_hint("ull --help 查看命令列表"))
|
||||
Err(CliError::usage(format!("未知命令 `{other}`"))
|
||||
.with_hint("%prog% --help 查看命令列表"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -194,7 +195,7 @@ fn parse_diag(tail: &[String]) -> Result<Command, CliError> {
|
||||
fn parse_config(tail: &[String]) -> Result<Command, CliError> {
|
||||
let Some(sub) = tail.first().map(String::as_str) else {
|
||||
return Err(CliError::usage("config 需要子命令 get|set")
|
||||
.with_hint("ull config get <key> / ull config set <key> <value>"));
|
||||
.with_hint("%prog% config get <key> / %prog% config set <key> <value>"));
|
||||
};
|
||||
let rest = tail.get(1..).unwrap_or_default();
|
||||
match sub {
|
||||
@@ -212,7 +213,7 @@ fn parse_config(tail: &[String]) -> Result<Command, CliError> {
|
||||
let parsed = parse_flags(rest, &[], &[])?;
|
||||
if parsed.positionals.len() != 2 {
|
||||
return Err(CliError::usage(
|
||||
"config set 需要键路径与值:ull config set <key> <value>",
|
||||
"config set 需要键路径与值:%prog% config set <key> <value>",
|
||||
));
|
||||
}
|
||||
Ok(Command::ConfigSet(ConfigSetArgs {
|
||||
@@ -262,8 +263,8 @@ fn parse_java(tail: &[String]) -> Result<Command, CliError> {
|
||||
fn require_single_positional(parsed: &ParsedArgs, usage: &str) -> Result<String, CliError> {
|
||||
match parsed.positionals.as_slice() {
|
||||
[only] => Ok(only.clone()),
|
||||
[] => Err(CliError::usage(format!("缺少参数:ull {usage}"))),
|
||||
_ => Err(CliError::usage(format!("参数过多:ull {usage}"))),
|
||||
[] => Err(CliError::usage(format!("缺少参数:%prog% {usage}"))),
|
||||
_ => Err(CliError::usage(format!("参数过多:%prog% {usage}"))),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ fn parse_add(rest: &[String]) -> Result<Command, CliError> {
|
||||
let Some(kind) = rest.first().map(String::as_str) else {
|
||||
return Err(
|
||||
CliError::usage("accounts add 需要轨道:offline|msa").with_hint(
|
||||
"ull accounts add offline <玩家名> / ull accounts add msa(设备码交互式登录)",
|
||||
"%prog% accounts add offline <玩家名> / %prog% accounts add msa(设备码交互式登录)",
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -16,7 +16,7 @@ use super::{ensure_no_positionals, parse_flags};
|
||||
pub fn parse_agent(tail: &[String]) -> Result<Command, CliError> {
|
||||
let Some(sub) = tail.first().map(String::as_str) else {
|
||||
return Err(CliError::usage("agent 需要子命令 serve|config")
|
||||
.with_hint("ull agent serve / ull agent config"));
|
||||
.with_hint("%prog% agent serve / %prog% agent config"));
|
||||
};
|
||||
let rest = tail.get(1..).unwrap_or_default();
|
||||
match sub {
|
||||
|
||||
@@ -4,7 +4,18 @@
|
||||
//! 「`<名字> --help`」这类提示不能写死 `ull` —— 用户敲的是 `unlucky-launcher`,把他指去敲
|
||||
//! 一个可能不存在的名字只会让人困惑。
|
||||
//!
|
||||
//! 取不到(argv[0] 为空,某些 exec 场景)就回落 [`FALLBACK`],输出因此与老版本逐字相同。
|
||||
//! 做法(M82 r2 定案):文案里放**占位符**,渲染时替换;**绝不对插值后的整条文本做词替换**。
|
||||
//! 上一版按「独立词 `ull`」替换整条消息,于是用户数据被改坏过:账户名叫 `ull` 的会变成
|
||||
//! `unlucky-launcher`,路径 `D:/tmp/ull/.ULL` 会变成 `D:/tmp/unlucky-launcher/.ULL`。
|
||||
//! 占位符只在模板里出现,插值进来的用户名/路径碰不到它。
|
||||
//!
|
||||
//! 取不到 argv[0](某些 exec 场景)就回落 [`FALLBACK`],输出因此与老版本逐字相同。
|
||||
|
||||
/// 文案里的程序名占位符。
|
||||
///
|
||||
/// 用 `%prog%` 而不是 `{prog}`:这些模板要活在 `format!` 里,`{prog}` 会被 `format!` 当成
|
||||
/// 参数名直接编译失败,而 `%prog%` 原样穿过去,到渲染时才替换。
|
||||
pub const PLACEHOLDER: &str = "%prog%";
|
||||
|
||||
/// 取不到 argv[0] 时的回落名(也是 `crates/cli` 产出的那个 bin)。
|
||||
pub const FALLBACK: &str = "ull";
|
||||
@@ -46,43 +57,21 @@ pub fn name() -> String {
|
||||
name_in(argv0.as_deref())
|
||||
}
|
||||
|
||||
/// 把文本里**独立出现**的 `ull` 换成程序名;`program` 就是 `ull` 时原样返回。
|
||||
/// 把模板里的 [`PLACEHOLDER`] 换成程序名。
|
||||
///
|
||||
/// 只换两侧都不是 ASCII 字母数字的 `ull`:`ull --help` 里的换,`null` / `pull` 里的不换。
|
||||
/// 帮助文案因此不用为了改名重写成动态字符串。
|
||||
/// **只认占位符**:文本里其它位置的 `ull`(账户名、路径段、回显的子命令)原样保留——这是
|
||||
/// 与「按词替换整条文本」的分界线,也是本函数唯一要保证的事。
|
||||
#[must_use]
|
||||
pub fn rename(text: &str, program: &str) -> String {
|
||||
if program == FALLBACK || !text.contains(FALLBACK) {
|
||||
pub fn fill(text: &str) -> String {
|
||||
if !text.contains(PLACEHOLDER) {
|
||||
return text.to_owned();
|
||||
}
|
||||
let mut out = String::with_capacity(text.len() + program.len());
|
||||
let mut copied = 0;
|
||||
let mut index = 0;
|
||||
while let Some(found) = text[index..].find(FALLBACK) {
|
||||
let start = index + found;
|
||||
let end = start + FALLBACK.len();
|
||||
let before_ok = !text[..start]
|
||||
.chars()
|
||||
.next_back()
|
||||
.is_some_and(|ch| ch.is_ascii_alphanumeric());
|
||||
let after_ok = !text[end..]
|
||||
.chars()
|
||||
.next()
|
||||
.is_some_and(|ch| ch.is_ascii_alphanumeric());
|
||||
if before_ok && after_ok {
|
||||
out.push_str(&text[copied..start]);
|
||||
out.push_str(program);
|
||||
copied = end;
|
||||
}
|
||||
index = end;
|
||||
}
|
||||
out.push_str(&text[copied..]);
|
||||
out
|
||||
text.replace(PLACEHOLDER, &name())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{name_in, rename, FALLBACK};
|
||||
use super::{fill, name_in, FALLBACK, PLACEHOLDER};
|
||||
|
||||
#[test]
|
||||
fn argv0_is_reduced_to_a_bare_name() {
|
||||
@@ -106,26 +95,26 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_only_touches_standalone_occurrences() {
|
||||
fn fill_replaces_only_the_placeholder() {
|
||||
assert_eq!(
|
||||
rename("ull --help 查看命令列表", "unlucky-launcher"),
|
||||
"unlucky-launcher --help 查看命令列表"
|
||||
);
|
||||
// 别的词里的 ull 不动(防止把 null/pull 改成 n/p + 名字)
|
||||
assert_eq!(
|
||||
rename("value is null, pull it", "unlucky-launcher"),
|
||||
"value is null, pull it"
|
||||
);
|
||||
// 中文紧邻不算字母数字,照换
|
||||
assert_eq!(
|
||||
rename("用法:ull [全局选项]", "unlucky-launcher"),
|
||||
"用法:unlucky-launcher [全局选项]"
|
||||
fill(&format!("{PLACEHOLDER} --help 查看命令列表")),
|
||||
format!("{} --help 查看命令列表", super::name())
|
||||
);
|
||||
// 没有占位符就原样返回(不碰任何词)。
|
||||
assert_eq!(fill("ull --help"), "ull --help");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_is_identity_for_ull() {
|
||||
let text = "ull --help";
|
||||
assert_eq!(rename(text, FALLBACK), text);
|
||||
fn user_data_that_says_ull_is_never_rewritten() {
|
||||
// 回归:账户名恰好是 `ull` / 路径里有 `\ull\` / 回显的子命令是 `ull`,
|
||||
// 一个字节都不能被改成程序名(M82 r1 的真缺陷)。
|
||||
for text in [
|
||||
"找不到账户 `ull`",
|
||||
r"索引文件:D:/tmp/ull\.ULL\data\agent\sessions\index.jsonl",
|
||||
"未知命令 `ull`",
|
||||
"过滤词 null 与 pull 也不算",
|
||||
] {
|
||||
assert_eq!(fill(text), text, "{text} 被改了");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,8 +28,8 @@ pub fn list(ctx: &Ctx) -> Result<Report, CliError> {
|
||||
let mut human = Vec::new();
|
||||
if store.is_empty() {
|
||||
human.push("还没有账户。".to_string());
|
||||
human.push(" 离线:ull accounts add offline <玩家名>".to_string());
|
||||
human.push(" 微软:ull accounts add msa(设备码交互式登录)".to_string());
|
||||
human.push(" 离线:%prog% accounts add offline <玩家名>".to_string());
|
||||
human.push(" 微软:%prog% accounts add msa(设备码交互式登录)".to_string());
|
||||
} else {
|
||||
human.push(format!("账户({} 个):", store.accounts().len()));
|
||||
for meta in store.accounts() {
|
||||
|
||||
@@ -122,7 +122,7 @@ pub fn config_report(ctx: &Ctx, port: Option<u16>) -> Result<Report, CliError> {
|
||||
human.push("Cursor(.cursor/mcp.json):".to_string());
|
||||
human.extend(pretty_lines(&samples.cursor));
|
||||
human.push(
|
||||
"启动:ull agent serve(默认 Streamable HTTP) / ull agent serve --stdio".to_string(),
|
||||
"启动:%prog% agent serve(默认 Streamable HTTP) / %prog% agent serve --stdio".to_string(),
|
||||
);
|
||||
Ok(Report::new(data, human))
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ fn get_one(ctx: &Ctx, key: &str, show_secret: bool) -> Result<Report, CliError>
|
||||
let Some(value) = store.effective(key) else {
|
||||
return Err(
|
||||
CliError::user("unknown_key", format!("配置键 `{key}` 不在 schema 里"))
|
||||
.with_hint("`ull config get`(不带键)可以看到全部生效键"),
|
||||
.with_hint("`%prog% config get`(不带键)可以看到全部生效键"),
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ pub fn resolve(
|
||||
format!("有 {} 个账户,需要指定用哪个", many.len()),
|
||||
)
|
||||
.with_hint(format!(
|
||||
"ull launch <instance> --account <名字>;候选:{}",
|
||||
"%prog% launch <instance> --account <名字>;候选:{}",
|
||||
names_of(&store)
|
||||
)))
|
||||
}
|
||||
@@ -75,7 +75,7 @@ pub fn resolve(
|
||||
"已选中账户 {},`--username` 只用于没有账户时的离线占位",
|
||||
meta.display_name()
|
||||
))
|
||||
.with_hint("去掉 --username,或用 `ull accounts add offline <名字>` 建账户"));
|
||||
.with_hint("去掉 --username,或用 `%prog% accounts add offline <名字>` 建账户"));
|
||||
}
|
||||
|
||||
let mut notes = Vec::new();
|
||||
@@ -103,7 +103,7 @@ fn offline_fallback(username: Option<&str>) -> Identity {
|
||||
},
|
||||
account: None,
|
||||
notes: vec![
|
||||
"没有账户,按离线占位启动(ull accounts add offline <名字> 可建账户)".to_string(),
|
||||
"没有账户,按离线占位启动(%prog% accounts add offline <名字> 可建账户)".to_string(),
|
||||
],
|
||||
}
|
||||
}
|
||||
@@ -120,7 +120,7 @@ fn microsoft_identity(
|
||||
"account_credentials",
|
||||
format!("账户 {} 的微软凭据缺失或类型不符", meta.display_name()),
|
||||
)
|
||||
.with_hint("重新登录:ull accounts add msa"));
|
||||
.with_hint("重新登录:%prog% accounts add msa"));
|
||||
};
|
||||
|
||||
if dry_run {
|
||||
@@ -193,7 +193,7 @@ fn names_of(store: &AccountStore) -> String {
|
||||
|
||||
fn unknown_account(key: &str, store: &AccountStore) -> CliError {
|
||||
let hint = if store.is_empty() {
|
||||
"还没有账户:ull accounts add offline <名字> 或 ull accounts add msa".to_string()
|
||||
"还没有账户:%prog% accounts add offline <名字> 或 %prog% accounts add msa".to_string()
|
||||
} else {
|
||||
format!("可用账户:{}", names_of(store))
|
||||
};
|
||||
|
||||
@@ -44,7 +44,7 @@ pub(super) fn run(ctx: &Ctx, args: &InstallArgs, kind: LoaderKind) -> Result<Rep
|
||||
let version = args.version.trim().to_string();
|
||||
if version.is_empty() {
|
||||
return Err(CliError::usage(
|
||||
"install 需要版本 id:ull install <version> --loader fabric",
|
||||
"install 需要版本 id:%prog% install <version> --loader fabric",
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ pub(super) fn execute(
|
||||
),
|
||||
)
|
||||
.with_hint(format!(
|
||||
"`ull java install {}` 装一个托管 JDK,或把 JAVA_HOME 指向可用的 JDK(托管目录 {})",
|
||||
"`%prog% java install {}` 装一个托管 JDK,或把 JAVA_HOME 指向可用的 JDK(托管目录 {})",
|
||||
plan.java_requirement.minimum,
|
||||
managed.display()
|
||||
)));
|
||||
|
||||
@@ -113,7 +113,7 @@ pub(super) fn ensure_name_free(ctx: &Ctx, name: &str, version: &str) -> Result<(
|
||||
"instance_exists",
|
||||
format!("同名实例已存在:{name}(绑定版本 {})", profile.version_id),
|
||||
)
|
||||
.with_hint("用 --name 换个名字,或 `ull instances list` 看看现有实例")),
|
||||
.with_hint("用 --name 换个名字,或 `%prog% instances list` 看看现有实例")),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
@@ -145,7 +145,7 @@ pub(super) fn failure_error(report: &InstallReport, ctx: &Ctx) -> CliError {
|
||||
}
|
||||
}
|
||||
CliError::user(code, message).with_hint(format!(
|
||||
"重跑 `ull install` 会续传并跳过已就绪文件;游戏根 {}",
|
||||
"重跑 `%prog% install` 会续传并跳过已就绪文件;游戏根 {}",
|
||||
ctx.game().root().display()
|
||||
))
|
||||
}
|
||||
@@ -37,7 +37,7 @@ pub(super) fn run(ctx: &Ctx, args: &InstallArgs) -> Result<Report, CliError> {
|
||||
let version = args.version.trim().to_string();
|
||||
if version.is_empty() {
|
||||
return Err(CliError::usage(
|
||||
"install 需要版本 id:ull install <version>",
|
||||
"install 需要版本 id:%prog% install <version>",
|
||||
));
|
||||
}
|
||||
let name = args
|
||||
@@ -139,7 +139,7 @@ pub(super) fn plan_and_install(
|
||||
format!("版本 {version} 装不全:{reasons}"),
|
||||
)
|
||||
.with_hint(
|
||||
"旧版(pre-1.6)清单兜底待补;可先用官方启动器装好该版本再用 `ull launch`",
|
||||
"旧版(pre-1.6)清单兜底待补;可先用官方启动器装好该版本再用 `%prog% launch`",
|
||||
));
|
||||
}
|
||||
let sink = report::progress_sink(progress, json_progress, plan.file_count());
|
||||
|
||||
@@ -59,7 +59,7 @@ pub fn list(ctx: &Ctx) -> Result<Report, CliError> {
|
||||
ctx.data.launcher_home.display()
|
||||
)];
|
||||
if body.is_empty() {
|
||||
human.push(" (空)用 `ull install <version>` 登记第一个实例".to_string());
|
||||
human.push(" (空)用 `%prog% install <version>` 登记第一个实例".to_string());
|
||||
}
|
||||
human.extend(body);
|
||||
if installed.is_empty() {
|
||||
|
||||
@@ -111,7 +111,7 @@ pub fn list(ctx: &Ctx) -> Result<Report, CliError> {
|
||||
let mut human = vec![format!("Java {} 个", candidates.len())];
|
||||
if candidates.is_empty() {
|
||||
human.push(
|
||||
"(没找到可用 Java)用 `ull java install 21` 装一个,或设 JAVA_HOME / `ull launch --java <path>`"
|
||||
"(没找到可用 Java)用 `%prog% java install 21` 装一个,或设 JAVA_HOME / `%prog% launch --java <path>`"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
@@ -209,7 +209,7 @@ pub fn install(ctx: &Ctx, major: u32, vendor: Option<JdkVendor>) -> Result<Repor
|
||||
for note in installed.chain.degradations() {
|
||||
human.push(format!("降级:{note}"));
|
||||
}
|
||||
human.push("`ull launch` 会自动优先用它".to_string());
|
||||
human.push("`%prog% launch` 会自动优先用它".to_string());
|
||||
Ok(Report::new(data, human))
|
||||
}
|
||||
|
||||
|
||||
@@ -330,7 +330,7 @@ fn map_launch_error(error: &LaunchError, ctx: &Ctx) -> CliError {
|
||||
|
||||
fn instance_hint(profiles: &ProfileList) -> String {
|
||||
if profiles.profiles.is_empty() {
|
||||
"用 `ull instances list` 查看实例,或 `ull install <version>` 登记一个".to_string()
|
||||
"用 `%prog% instances list` 查看实例,或 `%prog% install <version>` 登记一个".to_string()
|
||||
} else {
|
||||
let names: Vec<&str> = profiles
|
||||
.profiles
|
||||
|
||||
@@ -52,7 +52,7 @@ pub fn welcome() -> Report {
|
||||
#[must_use]
|
||||
pub fn help(topic: Option<&str>) -> Report {
|
||||
let text = match topic.and_then(help::topic) {
|
||||
Some(text) => program::rename(text, &program::name()),
|
||||
Some(text) => program::fill(text),
|
||||
None => general_help(),
|
||||
};
|
||||
let data = json!({
|
||||
@@ -64,5 +64,5 @@ pub fn help(topic: Option<&str>) -> Report {
|
||||
|
||||
/// 总帮助,按被调用的二进制名自述(`ull` 自己跑时逐字不变)。
|
||||
fn general_help() -> String {
|
||||
program::rename(help::general(), &program::name())
|
||||
program::fill(help::general())
|
||||
}
|
||||
@@ -75,14 +75,14 @@ pub(crate) fn map_auth_error(error: unlucky_core::auth::AuthError) -> CliError {
|
||||
let hint = match &error {
|
||||
AuthError::Canceled => Some("登录被取消,可以重跑一次".to_string()),
|
||||
AuthError::Timeout { .. } => {
|
||||
Some("重新执行 `ull accounts add msa` 拿新的用户码".to_string())
|
||||
Some("重新执行 `%prog% accounts add msa` 拿新的用户码".to_string())
|
||||
}
|
||||
AuthError::CredentialExpired => Some("重新登录:ull accounts add msa".to_string()),
|
||||
AuthError::CredentialExpired => Some("重新登录:%prog% accounts add msa".to_string()),
|
||||
AuthError::Unsupported(_) => {
|
||||
Some("微软登录需要设置 ULL_MSA_CLIENT_ID(Azure 公共客户端 id)".to_string())
|
||||
}
|
||||
AuthError::Conflict { .. } => Some("用 `ull accounts list` 查看已有账户".to_string()),
|
||||
AuthError::NotFound { .. } => Some("用 `ull accounts list` 查看账户名".to_string()),
|
||||
AuthError::Conflict { .. } => Some("用 `%prog% accounts list` 查看已有账户".to_string()),
|
||||
AuthError::NotFound { .. } => Some("用 `%prog% accounts list` 查看账户名".to_string()),
|
||||
AuthError::Denied { code, .. } if code == "no_profile" || code == "no_license" => {
|
||||
Some("去 minecraft.net 检查该账号的 Java 版拥有权与档案".to_string())
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ pub fn list(ctx: &Ctx, limit: usize) -> Result<Report, CliError> {
|
||||
index.display()
|
||||
)];
|
||||
if shown.is_empty() {
|
||||
human.push(" (空)先用 `ull launch <实例>` 跑一局".to_string());
|
||||
human.push(" (空)先用 `%prog% launch <实例>` 跑一局".to_string());
|
||||
}
|
||||
for entry in &shown {
|
||||
human.push(format!(
|
||||
|
||||
@@ -77,11 +77,11 @@ pub fn print_report(
|
||||
}
|
||||
if !quiet {
|
||||
for line in &outcome.report.human {
|
||||
writeln!(out, "{line}")?;
|
||||
writeln!(out, "{}", fill_program(line))?;
|
||||
}
|
||||
}
|
||||
for warning in &outcome.warnings {
|
||||
writeln!(err, "提示: {warning}")?;
|
||||
writeln!(err, "提示: {}", fill_program(warning))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -101,16 +101,20 @@ pub fn print_error(
|
||||
writeln!(out, "{}", serde_json::to_string_pretty(&error.to_json())?)?;
|
||||
return Ok(());
|
||||
}
|
||||
writeln!(err, "错误: {}", rename_program(error.message()))?;
|
||||
writeln!(err, "错误: {}", fill_program(error.message()))?;
|
||||
if let Some(hint) = error.hint() {
|
||||
writeln!(err, "提示: {}", rename_program(hint))?;
|
||||
writeln!(err, "提示: {}", fill_program(hint))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 文案里的 `ull` 按被调用的二进制名显示(M82):`unlucky-launcher lst` 不该把人指去敲 `ull`。
|
||||
fn rename_program(text: &str) -> String {
|
||||
crate::cli::program::rename(text, &crate::cli::program::name())
|
||||
/// 渲染文案里的程序名占位符(M82)。
|
||||
///
|
||||
/// **只替换占位符**,不对整条文本做词替换:消息里插值进来的账户名 / 路径 / 回显的子命令
|
||||
/// 一个字节都不动(r1 曾把账户名 `ull` 改成程序名、把 `D:/tmp/ull/.ULL` 改成
|
||||
/// `D:/tmp/unlucky-launcher/.ULL`)。
|
||||
fn fill_program(text: &str) -> String {
|
||||
crate::cli::program::fill(text)
|
||||
}
|
||||
|
||||
/// 长任务的实时进度行(**只进 stderr**,不参与 [`Report`])。
|
||||
|
||||
Reference in new issue
Block a user