diff --git a/crates/cli/src/cli/help.rs b/crates/cli/src/cli/help.rs index 5f8b5a0..290a562 100644 --- a/crates/cli/src/cli/help.rs +++ b/crates/cli/src/cli/help.rs @@ -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 [--loader X] [--loader-version V] [--name N] [--dry-run] +%prog% install [--loader X] [--loader-version V] [--name N] [--dry-run] 解析版本清单与继承链 → 组下载计划 → 落盘(versions/ assets/ libraries/)→ 登记实例 (profiles.json + config/instances/.json)。已存在且校验通过的文件会跳过, @@ -81,7 +81,7 @@ versions/<原版>-<加载器>-<版本>/<…>.json(inheritsFrom 原版 + 主类 ), "launch" => Some( "\ -ull launch [--dry-run] [--account ] [--java ] [--username ] +%prog% launch [--dry-run] [--account ] [--java ] [--username ] [--max-memory ] 可以是实例 id、实例名,或直接是版本 id。 @@ -92,10 +92,10 @@ accessToken 一律脱敏(输出里出现的是 )。", ), "accounts" => Some( "\ -ull accounts list -ull accounts add offline -ull accounts add msa -ull accounts remove +%prog% accounts list +%prog% accounts add offline +%prog% accounts add msa +%prog% accounts remove 账户存在 <数据根>/.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 [--tail N] 允许唯一前缀;输出会话 JSONL 末尾 N 条事件(缺省 200)"), + "sessions" => Some("%prog% sessions list [--limit N] 列出会话索引(按开始时间倒序,缺省 20 条)"), + "logs" => Some("%prog% logs [--tail N] 允许唯一前缀;输出会话 JSONL 末尾 N 条事件(缺省 200)"), "diag" => Some( - "ull diag [--tail N] 把会话的退出信号与日志喂给规则引擎(内嵌规则集),输出结构化诊断", + "%prog% diag [--tail N] 把会话的退出信号与日志喂给规则引擎(内嵌规则集),输出结构化诊断", ), "config" => Some( "\ -ull config get [] [--show-secret] -ull config set +%prog% config get [] [--show-secret] +%prog% config set 读写 <数据根>/.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 [--vendor X] 安装托管 JDK(D13/D23):回退链查版本 → 下载 → 解压 → 验证 -ull java vendors 列出四家发行版的镜像/支持版本/当前优先级顺序 +%prog% java install [--vendor X] 安装托管 JDK(D13/D23):回退链查版本 → 下载 → 解压 → 验证 +%prog% java vendors 列出四家发行版的镜像/支持版本/当前优先级顺序 落点(D23 三模式,随 core.data_mode 与设置键 java.installRoot 走): portable(默认) <数据根>/.ULL/data/java-managed/jdk--/ @@ -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); diff --git a/crates/cli/src/cli/parse.rs b/crates/cli/src/cli/parse.rs index 1c1b4d6..0e8e349 100644 --- a/crates/cli/src/cli/parse.rs +++ b/crates/cli/src/cli/parse.rs @@ -92,7 +92,8 @@ fn parse_command(rest: &[String], globals: &Globals) -> Result 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 { fn parse_config(tail: &[String]) -> Result { let Some(sub) = tail.first().map(String::as_str) else { return Err(CliError::usage("config 需要子命令 get|set") - .with_hint("ull config get / ull config set ")); + .with_hint("%prog% config get / %prog% config set ")); }; let rest = tail.get(1..).unwrap_or_default(); match sub { @@ -212,7 +213,7 @@ fn parse_config(tail: &[String]) -> Result { let parsed = parse_flags(rest, &[], &[])?; if parsed.positionals.len() != 2 { return Err(CliError::usage( - "config set 需要键路径与值:ull config set ", + "config set 需要键路径与值:%prog% config set ", )); } Ok(Command::ConfigSet(ConfigSetArgs { @@ -262,8 +263,8 @@ fn parse_java(tail: &[String]) -> Result { fn require_single_positional(parsed: &ParsedArgs, usage: &str) -> Result { 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}"))), } } diff --git a/crates/cli/src/cli/parse/accounts.rs b/crates/cli/src/cli/parse/accounts.rs index c24b96f..b8f7389 100644 --- a/crates/cli/src/cli/parse/accounts.rs +++ b/crates/cli/src/cli/parse/accounts.rs @@ -41,7 +41,7 @@ fn parse_add(rest: &[String]) -> Result { 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(设备码交互式登录)", ), ); }; diff --git a/crates/cli/src/cli/parse/agent.rs b/crates/cli/src/cli/parse/agent.rs index 74df2f4..ef39915 100644 --- a/crates/cli/src/cli/parse/agent.rs +++ b/crates/cli/src/cli/parse/agent.rs @@ -16,7 +16,7 @@ use super::{ensure_no_positionals, parse_flags}; pub fn parse_agent(tail: &[String]) -> Result { 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 { diff --git a/crates/cli/src/cli/program.rs b/crates/cli/src/cli/program.rs index c5c230b..d6f6d5c 100644 --- a/crates/cli/src/cli/program.rs +++ b/crates/cli/src/cli/program.rs @@ -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} 被改了"); + } } } diff --git a/crates/cli/src/commands/accounts.rs b/crates/cli/src/commands/accounts.rs index 9f133d6..b4fffe0 100644 --- a/crates/cli/src/commands/accounts.rs +++ b/crates/cli/src/commands/accounts.rs @@ -28,8 +28,8 @@ pub fn list(ctx: &Ctx) -> Result { 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() { diff --git a/crates/cli/src/commands/agent/mod.rs b/crates/cli/src/commands/agent/mod.rs index 59edf29..06088e7 100644 --- a/crates/cli/src/commands/agent/mod.rs +++ b/crates/cli/src/commands/agent/mod.rs @@ -122,7 +122,7 @@ pub fn config_report(ctx: &Ctx, port: Option) -> Result { 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)) } diff --git a/crates/cli/src/commands/config.rs b/crates/cli/src/commands/config.rs index f6598f1..316cf53 100644 --- a/crates/cli/src/commands/config.rs +++ b/crates/cli/src/commands/config.rs @@ -53,7 +53,7 @@ fn get_one(ctx: &Ctx, key: &str, show_secret: bool) -> Result 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`(不带键)可以看到全部生效键"), ); }; diff --git a/crates/cli/src/commands/identity.rs b/crates/cli/src/commands/identity.rs index 3594e79..3944f17 100644 --- a/crates/cli/src/commands/identity.rs +++ b/crates/cli/src/commands/identity.rs @@ -59,7 +59,7 @@ pub fn resolve( format!("有 {} 个账户,需要指定用哪个", many.len()), ) .with_hint(format!( - "ull launch --account <名字>;候选:{}", + "%prog% launch --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)) }; diff --git a/crates/cli/src/commands/install/loader.rs b/crates/cli/src/commands/install/loader.rs index 9e979f6..4a7fb7c 100644 --- a/crates/cli/src/commands/install/loader.rs +++ b/crates/cli/src/commands/install/loader.rs @@ -44,7 +44,7 @@ pub(super) fn run(ctx: &Ctx, args: &InstallArgs, kind: LoaderKind) -> Result --loader fabric", + "install 需要版本 id:%prog% install --loader fabric", )); } diff --git a/crates/cli/src/commands/install/loader_processors.rs b/crates/cli/src/commands/install/loader_processors.rs index 338bf49..60d1347 100644 --- a/crates/cli/src/commands/install/loader_processors.rs +++ b/crates/cli/src/commands/install/loader_processors.rs @@ -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() ))); diff --git a/crates/cli/src/commands/install/mod.rs b/crates/cli/src/commands/install/mod.rs index b9a63b5..2d3553f 100644 --- a/crates/cli/src/commands/install/mod.rs +++ b/crates/cli/src/commands/install/mod.rs @@ -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() )) } diff --git a/crates/cli/src/commands/install/vanilla.rs b/crates/cli/src/commands/install/vanilla.rs index e01d8e2..b0a074a 100644 --- a/crates/cli/src/commands/install/vanilla.rs +++ b/crates/cli/src/commands/install/vanilla.rs @@ -37,7 +37,7 @@ pub(super) fn run(ctx: &Ctx, args: &InstallArgs) -> Result { let version = args.version.trim().to_string(); if version.is_empty() { return Err(CliError::usage( - "install 需要版本 id:ull install ", + "install 需要版本 id:%prog% install ", )); } 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()); diff --git a/crates/cli/src/commands/instances.rs b/crates/cli/src/commands/instances.rs index a5021e0..95a059d 100644 --- a/crates/cli/src/commands/instances.rs +++ b/crates/cli/src/commands/instances.rs @@ -59,7 +59,7 @@ pub fn list(ctx: &Ctx) -> Result { ctx.data.launcher_home.display() )]; if body.is_empty() { - human.push(" (空)用 `ull install ` 登记第一个实例".to_string()); + human.push(" (空)用 `%prog% install ` 登记第一个实例".to_string()); } human.extend(body); if installed.is_empty() { diff --git a/crates/cli/src/commands/java.rs b/crates/cli/src/commands/java.rs index 6e35269..1ab3622 100644 --- a/crates/cli/src/commands/java.rs +++ b/crates/cli/src/commands/java.rs @@ -111,7 +111,7 @@ pub fn list(ctx: &Ctx) -> Result { let mut human = vec![format!("Java {} 个", candidates.len())]; if candidates.is_empty() { human.push( - "(没找到可用 Java)用 `ull java install 21` 装一个,或设 JAVA_HOME / `ull launch --java `" + "(没找到可用 Java)用 `%prog% java install 21` 装一个,或设 JAVA_HOME / `%prog% launch --java `" .to_string(), ); } @@ -209,7 +209,7 @@ pub fn install(ctx: &Ctx, major: u32, vendor: Option) -> Result CliError { fn instance_hint(profiles: &ProfileList) -> String { if profiles.profiles.is_empty() { - "用 `ull instances list` 查看实例,或 `ull install ` 登记一个".to_string() + "用 `%prog% instances list` 查看实例,或 `%prog% install ` 登记一个".to_string() } else { let names: Vec<&str> = profiles .profiles diff --git a/crates/cli/src/commands/meta.rs b/crates/cli/src/commands/meta.rs index 6ad1799..49dafa9 100644 --- a/crates/cli/src/commands/meta.rs +++ b/crates/cli/src/commands/meta.rs @@ -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()) } diff --git a/crates/cli/src/commands/mod.rs b/crates/cli/src/commands/mod.rs index e6e385e..b39ca94 100644 --- a/crates/cli/src/commands/mod.rs +++ b/crates/cli/src/commands/mod.rs @@ -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()) } diff --git a/crates/cli/src/commands/sessions.rs b/crates/cli/src/commands/sessions.rs index a125914..e8865c0 100644 --- a/crates/cli/src/commands/sessions.rs +++ b/crates/cli/src/commands/sessions.rs @@ -57,7 +57,7 @@ pub fn list(ctx: &Ctx, limit: usize) -> Result { 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!( diff --git a/crates/cli/src/output.rs b/crates/cli/src/output.rs index a7f5fed..6affb07 100644 --- a/crates/cli/src/output.rs +++ b/crates/cli/src/output.rs @@ -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`])。