Skip to main content

sz_rust_cli/
cli.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2024-2026 SZ-Rust Team
3//
4//! CLI 命令定义 — 基于 clap derive
5//!
6//! 对齐 PHP ThinkPHP 6 `think` 命令体系,借鉴 Laravel Artisan 风格。
7//!
8//! ## PHP 对齐
9//!
10//! PHP `think` 命令使用 Symfony Console 组件,通过 `configure()` 定义参数和选项。
11//! Rust 端使用 clap derive 宏,通过结构体字段定义参数和选项。
12//!
13//! ## 命令对照表
14//!
15//! | PHP | Rust | 说明 |
16//! |-----|------|------|
17//! | `php think make:model User` | `sz-rust make:model User` | 生成 Model |
18//! | `php think make:controller User` | `sz-rust make:controller User` | 生成 Controller |
19//! | `php think make:migration CreateUsers` | `sz-rust make:migration create_users` | 生成迁移文件 |
20//! | `php think make:seeder UserSeeder` | `sz-rust make:seeder user_seeder` | 生成填充文件 |
21//! | `php think make:validate User` | `sz-rust make:validate User` | 生成验证器 |
22//! | `php think make:event User` | `sz-rust make:event User` | 生成事件 |
23//! | `php think make:listener UserListener` | `sz-rust make:listener UserListener` | 生成监听器 |
24//! | `php think make:command Hello` | `sz-rust make:command Hello` | 生成命令 |
25//! | `php think make:service UserService` | `sz-rust make:service UserService` | 生成服务 |
26//! | `php think migrate` | `sz-rust migrate` | 执行迁移 |
27//! | `php think migrate:rollback` | `sz-rust migrate --rollback` | 回滚迁移 |
28//! | `php think migrate:status` | `sz-rust migrate:status` | 迁移状态 |
29//! | `php think db:seed` | `sz-rust db:seed` | 数据填充 |
30//! | `php think route:list` | `sz-rust route:list` | 路由列表 |
31//! | `php think cache:clear` | `sz-rust cache:clear` | 清空缓存 |
32//! | `php think optimize:route` | `sz-rust optimize:route` | 路由缓存 |
33//! | `php think optimize:config` | `sz-rust optimize:config` | 配置缓存 |
34//! | `php think optimize:schema` | `sz-rust optimize:schema` | 数据表字段缓存 |
35//! | `php think route:clear` | `sz-rust route:clear` | 清除路由缓存 |
36
37use clap::{Parser, Subcommand};
38
39use crate::cmd;
40use crate::error::CliError;
41
42/// SZ-Rust 命令行工具
43///
44/// 替代 PHP `think` 命令,提供代码生成、数据库迁移、路由查看、缓存管理等功能。
45#[derive(Parser, Debug)]
46#[command(
47    name = "sz-rust",
48    bin_name = "sz-rust",
49    version,
50    about = "SZ-Rust 命令行工具 — 替代 PHP think 命令",
51    long_about = "SZ-Rust CLI 对齐 PHP ThinkPHP 6 think 命令体系,提供 make:migration / make:model / make:controller / migrate / route:list / cache:clear 等命令。"
52)]
53pub struct Cli {
54    /// 子命令
55    #[command(subcommand)]
56    pub command: Option<Command>,
57}
58
59/// 顶层命令枚举
60///
61/// 对齐 PHP `think` 的命令分组(make / migrate / route / cache)。
62#[derive(Subcommand, Debug)]
63pub enum Command {
64    /// 代码生成命令组(make:migration / make:model / make:controller / make:guard / make:scaffold)
65    #[command(name = "make")]
66    Make {
67        /// make 子命令
68        #[command(subcommand)]
69        make_command: cmd::make::MakeCommand,
70    },
71
72    /// 数据库迁移命令(migrate / migrate:status / migrate:rollback)
73    #[command(name = "migrate")]
74    Migrate {
75        /// 迁移子命令参数
76        #[command(flatten)]
77        args: cmd::migrate::MigrateArgs,
78    },
79
80    /// 迁移状态查询(对齐 PHP `php think migrate:status`)
81    #[command(name = "migrate:status")]
82    MigrateStatus {
83        /// 迁移目录(默认 `migrations`)
84        #[arg(short = 'p', long, default_value = "migrations")]
85        path: String,
86
87        /// 数据库类型(默认 `postgres`,对齐 sz-orm `DbType`)
88        #[arg(long, default_value = "postgres")]
89        db_type: String,
90
91        /// 打印每个迁移的 SQL 内容
92        #[arg(long)]
93        show_sql: bool,
94
95        /// 数据库连接 URL(启用在线模式,查询真实状态)
96        ///
97        /// 省略时为离线模式,所有迁移状态显示为 `Pending*`。
98        #[arg(long)]
99        url: Option<String>,
100    },
101
102    /// 路由列表(对齐 PHP `php think route:list`)
103    #[command(name = "route:list")]
104    RouteList {
105        /// 输出格式(table / json)
106        #[arg(short = 'f', long, default_value = "table")]
107        format: String,
108    },
109
110    /// 清空缓存(对齐 PHP `php think cache:clear`)
111    #[command(name = "cache:clear")]
112    CacheClear {
113        /// 指定缓存存储名(默认清空所有)
114        #[arg(short = 's', long)]
115        store: Option<String>,
116    },
117
118    /// 数据填充(对齐 PHP `php think db:seed`)
119    ///
120    /// 从 `seeds/` 目录加载 `.sql` 文件并执行。提供 `--url` 时连接数据库真实执行,
121    /// 否则为离线模式(仅打印待执行内容)。
122    #[command(name = "db:seed")]
123    Seed {
124        /// 填充目录(默认 `seeds`)
125        #[arg(short = 'p', long, default_value = "seeds")]
126        path: String,
127
128        /// 数据库类型(默认 `postgres`,对齐 sz-orm `DbType`)
129        #[arg(long, default_value = "postgres")]
130        db_type: String,
131
132        /// 打印每个填充文件的 SQL 内容
133        #[arg(long)]
134        show_sql: bool,
135
136        /// 数据库连接 URL(启用在线模式)
137        ///
138        /// 省略时为离线模式,仅打印待执行的 SQL。
139        #[arg(long)]
140        url: Option<String>,
141
142        /// 指定填充器文件名(不含扩展名,如 `001_users_seed`)
143        ///
144        /// 省略时执行目录下所有 `.sql` 文件。
145        #[arg(short = 'c', long)]
146        class: Option<String>,
147    },
148
149    /// 调度器命令组(scheduler:list / scheduler:run / scheduler:start)
150    #[command(name = "scheduler")]
151    Scheduler {
152        /// scheduler 子命令
153        #[command(subcommand)]
154        scheduler_command: cmd::scheduler::SchedulerCommand,
155    },
156
157    /// 生成路由缓存(对齐 PHP `php think optimize:route`)
158    ///
159    /// 收集路由元数据,序列化为 JSON 写入 `runtime/cache/route_cache.json`。
160    #[command(name = "optimize:route")]
161    OptimizeRoute,
162
163    /// 生成配置缓存(对齐 PHP `php think optimize:config`)
164    ///
165    /// 扫描 `config/` 目录,合并所有配置,序列化为 JSON 写入 `runtime/cache/config_cache.json`。
166    #[command(name = "optimize:config")]
167    OptimizeConfig,
168
169    /// 生成数据表字段缓存(对齐 PHP `php think optimize:schema`)
170    ///
171    /// 读取 `config/database.yml` 数据库连接配置,生成 schema 缓存索引文件
172    /// (`runtime/schema_cache.json` + `runtime/schema_cache.php`)。
173    /// 业务方运行时通过 `SchemaCache::remember_schema()` 填充具体字段信息。
174    #[command(name = "optimize:schema")]
175    OptimizeSchema,
176
177    /// 清除路由缓存(对齐 PHP `php think route:clear`)
178    ///
179    /// 删除 `runtime/cache/route_cache.json` 文件。
180    #[command(name = "route:clear")]
181    RouteClear,
182
183    /// 插件市场命令组(plugin search/install/publish/uninstall/update/list/login)
184    #[command(name = "plugin")]
185    Plugin {
186        /// plugin 子命令
187        #[command(subcommand)]
188        plugin_command: cmd::plugin::PluginCommand,
189    },
190
191    /// Admin 后台管理插件命令组(admin migrate/list-routes/list-capabilities/init)
192    #[command(name = "admin")]
193    Admin {
194        /// admin 子命令
195        #[command(subcommand)]
196        admin_command: cmd::admin::AdminCommand,
197    },
198
199    /// 启动 HTTP 服务(对齐 PHP php think run)
200    #[command(name = "serve")]
201    Serve {
202        /// 启用 admin 插件(加载 /api/admin/* 路由 + Capability 注册)
203        #[arg(long)]
204        with_admin: bool,
205
206        /// 启用 tenant_middleware(从 X-Tenant-Id Header 提取租户 ID)
207        #[arg(long)]
208        with_tenant: bool,
209
210        /// 启用 data_scope_middleware(注入数据权限上下文)
211        #[arg(long)]
212        with_data_scope: bool,
213
214        /// 监听地址(默认 0.0.0.0:8080)
215        #[arg(long, default_value = "0.0.0.0:8080")]
216        addr: String,
217
218        /// 启用配置热重载(监听 config/ 目录文件变更)
219        #[arg(long)]
220        watch_config: bool,
221
222        /// worker 线程数(默认 CPU 核心数,上限 1024)
223        #[arg(long)]
224        workers: Option<u16>,
225
226        /// 优雅关闭超时秒数(默认 30,上限 300)
227        #[arg(long)]
228        grace_timeout: Option<u16>,
229
230        /// TLS 证书文件路径
231        #[arg(long)]
232        tls_cert: Option<std::path::PathBuf>,
233
234        /// TLS 私钥文件路径
235        #[arg(long)]
236        tls_key: Option<std::path::PathBuf>,
237
238        /// 启用访问日志中间件
239        #[arg(long)]
240        access_log: bool,
241
242        /// 启用健康检查端点(默认启用)
243        #[arg(long, default_value_t = true)]
244        health: bool,
245
246        /// 禁用健康检查端点
247        #[arg(long, conflicts_with = "health")]
248        no_health: bool,
249    },
250
251    /// 触发运行中的 serve 进程重载配置(Windows 替代 SIGUSR1/SIGHUP)
252    ///
253    /// Unix 平台建议使用 `kill -USR1 <pid>` 或 `kill -HUP <pid>`。
254    /// 首版仅输出提示信息,完整实现需后续版本支持。
255    #[command(name = "serve:reload")]
256    ServeReload,
257
258    /// 触发运行中的 serve 进程切换日志级别(Windows 替代 SIGUSR2)
259    ///
260    /// Unix 平台建议使用 `kill -USR2 <pid>`。
261    /// 首版仅输出提示信息,完整实现需后续版本支持。
262    #[command(name = "serve:log-level")]
263    ServeLogLevel,
264}
265
266impl Cli {
267    /// 执行命令
268    ///
269    /// 根据 `command` 字段分发到对应的命令处理器。
270    ///
271    /// # 返回
272    ///
273    /// - `Ok(0)`:成功
274    /// - `Ok(code)`:命令指定的退出码(非 0 表示部分失败)
275    /// - `Err(_)`:内部错误
276    pub async fn execute(&self) -> Result<i32, CliError> {
277        match &self.command {
278            None => {
279                // 无子命令,打印帮助
280                println!("SZ-Rust CLI — 使用 --help 查看可用命令");
281                Ok(0)
282            }
283            Some(Command::Make { make_command }) => {
284                cmd::make::execute(make_command).await.map(|_| 0)
285            }
286            Some(Command::Migrate { args }) => cmd::migrate::execute_migrate(args).await.map(|_| 0),
287            Some(Command::MigrateStatus {
288                path,
289                db_type,
290                show_sql,
291                url,
292            }) => cmd::migrate::execute_status_full(path, db_type, *show_sql, url.as_deref())
293                .await
294                .map(|_| 0),
295            Some(Command::RouteList { format }) => {
296                cmd::route::execute_route_list(format).map(|_| 0)
297            }
298            Some(Command::CacheClear { store }) => {
299                cmd::cache::execute_cache_clear(store.as_deref()).map(|_| 0)
300            }
301            Some(Command::Seed {
302                path,
303                db_type,
304                show_sql,
305                url,
306                class,
307            }) => {
308                let args = cmd::seed::SeedArgs {
309                    path: path.clone(),
310                    db_type: db_type.clone(),
311                    show_sql: *show_sql,
312                    url: url.clone(),
313                    class: class.clone(),
314                };
315                cmd::seed::execute_seed(&args).map(|_| 0)
316            }
317            Some(Command::Scheduler { scheduler_command }) => {
318                cmd::scheduler::execute(scheduler_command).map(|_| 0)
319            }
320            Some(Command::OptimizeRoute) => {
321                cmd::optimize::execute_optimize_route().await.map(|_| 0)
322            }
323            Some(Command::OptimizeConfig) => {
324                cmd::optimize::execute_optimize_config().await.map(|_| 0)
325            }
326            Some(Command::OptimizeSchema) => {
327                cmd::optimize::execute_optimize_schema().await.map(|_| 0)
328            }
329            Some(Command::RouteClear) => cmd::optimize::execute_route_clear().await.map(|_| 0),
330            Some(Command::Plugin { plugin_command }) => cmd::plugin::execute(plugin_command).await,
331            Some(Command::Admin { admin_command }) => cmd::admin::execute(admin_command).await,
332            Some(Command::Serve {
333                with_admin,
334                with_tenant,
335                with_data_scope,
336                addr,
337                watch_config,
338                workers,
339                grace_timeout,
340                tls_cert,
341                tls_key,
342                access_log,
343                health,
344                no_health,
345            }) => {
346                let args = cmd::serve::ServeArgs {
347                    with_admin: *with_admin,
348                    with_tenant: *with_tenant,
349                    with_data_scope: *with_data_scope,
350                    addr: addr.clone(),
351                    watch_config: *watch_config,
352                    workers: *workers,
353                    grace_timeout: *grace_timeout,
354                    tls_cert: tls_cert.clone(),
355                    tls_key: tls_key.clone(),
356                    access_log: *access_log,
357                    health: *health && !*no_health,
358                };
359                tokio::task::spawn_blocking(move || cmd::serve::execute(args))
360                    .await
361                    .map_err(|e| CliError::Generic(format!("serve 任务执行失败: {e}")))?
362            }
363            Some(Command::ServeReload) => {
364                println!("Windows 信号替代方案暂未实现,请使用 --watch-config 或重启服务");
365                Ok(0)
366            }
367            Some(Command::ServeLogLevel) => {
368                println!("Windows 信号替代方案暂未实现,请使用 --watch-config 或重启服务");
369                Ok(0)
370            }
371        }
372    }
373}
374
375#[cfg(test)]
376#[allow(clippy::await_holding_lock)]
377mod tests {
378    use super::*;
379    use clap::Parser;
380
381    #[test]
382    fn test_parse_make_model() {
383        let cli = Cli::parse_from(["sz-rust", "make", "model", "User"]);
384        match cli.command {
385            Some(Command::Make { make_command }) => {
386                assert!(matches!(make_command, cmd::make::MakeCommand::Model { .. }));
387            }
388            _ => panic!("expected Make command"),
389        }
390    }
391
392    #[test]
393    fn test_parse_make_controller() {
394        let cli = Cli::parse_from(["sz-rust", "make", "controller", "User"]);
395        match cli.command {
396            Some(Command::Make { make_command }) => {
397                assert!(matches!(
398                    make_command,
399                    cmd::make::MakeCommand::Controller { .. }
400                ));
401            }
402            _ => panic!("expected Make command"),
403        }
404    }
405
406    #[test]
407    fn test_parse_make_migration() {
408        let cli = Cli::parse_from(["sz-rust", "make", "migration", "create_users"]);
409        match cli.command {
410            Some(Command::Make { make_command }) => {
411                assert!(matches!(
412                    make_command,
413                    cmd::make::MakeCommand::Migration { .. }
414                ));
415            }
416            _ => panic!("expected Make command"),
417        }
418    }
419
420    #[test]
421    fn test_parse_optimize_schema() {
422        let cli = Cli::parse_from(["sz-rust", "optimize:schema"]);
423        assert!(matches!(cli.command, Some(Command::OptimizeSchema)));
424    }
425
426    #[test]
427    fn test_parse_make_validate() {
428        let cli = Cli::parse_from(["sz-rust", "make", "validate", "User"]);
429        match cli.command {
430            Some(Command::Make { make_command }) => {
431                assert!(matches!(
432                    make_command,
433                    cmd::make::MakeCommand::Validate { .. }
434                ));
435            }
436            _ => panic!("expected Make command"),
437        }
438    }
439
440    #[test]
441    fn test_parse_make_seeder() {
442        let cli = Cli::parse_from(["sz-rust", "make", "seeder", "001_users"]);
443        match cli.command {
444            Some(Command::Make { make_command }) => {
445                assert!(matches!(
446                    make_command,
447                    cmd::make::MakeCommand::Seeder { .. }
448                ));
449            }
450            _ => panic!("expected Make command"),
451        }
452    }
453
454    #[test]
455    fn test_parse_migrate() {
456        let cli = Cli::parse_from(["sz-rust", "migrate"]);
457        assert!(matches!(cli.command, Some(Command::Migrate { .. })));
458    }
459
460    #[test]
461    fn test_parse_migrate_status() {
462        let cli = Cli::parse_from(["sz-rust", "migrate:status"]);
463        assert!(matches!(cli.command, Some(Command::MigrateStatus { .. })));
464    }
465
466    #[test]
467    fn test_parse_route_list() {
468        let cli = Cli::parse_from(["sz-rust", "route:list"]);
469        assert!(matches!(cli.command, Some(Command::RouteList { .. })));
470    }
471
472    #[test]
473    fn test_parse_cache_clear() {
474        let cli = Cli::parse_from(["sz-rust", "cache:clear"]);
475        assert!(matches!(cli.command, Some(Command::CacheClear { .. })));
476    }
477
478    #[test]
479    fn test_parse_cache_clear_with_store() {
480        let cli = Cli::parse_from(["sz-rust", "cache:clear", "--store", "redis"]);
481        match cli.command {
482            Some(Command::CacheClear { store }) => {
483                assert_eq!(store.as_deref(), Some("redis"));
484            }
485            _ => panic!("expected CacheClear command"),
486        }
487    }
488
489    #[test]
490    fn test_parse_scheduler() {
491        let cli = Cli::parse_from(["sz-rust", "scheduler", "list"]);
492        assert!(matches!(cli.command, Some(Command::Scheduler { .. })));
493    }
494
495    #[test]
496    fn test_parse_db_seed() {
497        let cli = Cli::parse_from(["sz-rust", "db:seed"]);
498        assert!(matches!(cli.command, Some(Command::Seed { .. })));
499    }
500
501    #[test]
502    fn test_parse_db_seed_with_options() {
503        let cli = Cli::parse_from([
504            "sz-rust",
505            "db:seed",
506            "--path",
507            "custom_seeds",
508            "--db-type",
509            "mysql",
510            "--show-sql",
511            "--url",
512            "mysql://user:pass@host:3306/db",
513            "--class",
514            "001_users",
515        ]);
516        match cli.command {
517            Some(Command::Seed {
518                path,
519                db_type,
520                show_sql,
521                url,
522                class,
523            }) => {
524                assert_eq!(path, "custom_seeds");
525                assert_eq!(db_type, "mysql");
526                assert!(show_sql);
527                assert_eq!(url.as_deref(), Some("mysql://user:pass@host:3306/db"));
528                assert_eq!(class.as_deref(), Some("001_users"));
529            }
530            _ => panic!("expected Seed command"),
531        }
532    }
533
534    #[tokio::test]
535    async fn test_execute_no_command_returns_ok() {
536        let cli = Cli { command: None };
537        let result = cli.execute().await;
538        assert!(result.is_ok());
539        assert_eq!(result.unwrap(), 0);
540    }
541
542    #[tokio::test]
543    async fn test_execute_make_model() {
544        let _lock = crate::cmd::test_support::acquire_global_lock();
545        let temp = tempfile::tempdir().unwrap();
546        let original = std::env::current_dir().unwrap();
547        std::env::set_current_dir(temp.path()).unwrap();
548
549        let cli = Cli {
550            command: Some(Command::Make {
551                make_command: cmd::make::MakeCommand::Model {
552                    name: "User".to_string(),
553                },
554            }),
555        };
556        let result = cli.execute().await;
557        std::env::set_current_dir(&original).unwrap();
558        assert!(result.is_ok());
559        assert!(temp.path().join("app/model/User.rs").exists());
560    }
561
562    #[tokio::test]
563    async fn test_execute_migrate_offline() {
564        let _lock = crate::cmd::test_support::acquire_global_lock();
565        let temp = tempfile::tempdir().unwrap();
566        let original = std::env::current_dir().unwrap();
567        std::env::set_current_dir(temp.path()).unwrap();
568
569        let cli = Cli {
570            command: Some(Command::Migrate {
571                args: cmd::migrate::MigrateArgs {
572                    rollback: false,
573                    path: temp.path().to_string_lossy().to_string(),
574                    db_type: "postgres".to_string(),
575                    show_sql: false,
576                    url: None,
577                },
578            }),
579        };
580        let result = cli.execute().await;
581        std::env::set_current_dir(&original).unwrap();
582        assert!(result.is_ok());
583    }
584
585    #[tokio::test]
586    async fn test_execute_migrate_status_offline() {
587        let _lock = crate::cmd::test_support::acquire_global_lock();
588        let temp = tempfile::tempdir().unwrap();
589        let original = std::env::current_dir().unwrap();
590        std::env::set_current_dir(temp.path()).unwrap();
591
592        let cli = Cli {
593            command: Some(Command::MigrateStatus {
594                path: temp.path().to_string_lossy().to_string(),
595                db_type: "postgres".to_string(),
596                show_sql: false,
597                url: None,
598            }),
599        };
600        let result = cli.execute().await;
601        std::env::set_current_dir(&original).unwrap();
602        assert!(result.is_ok());
603    }
604
605    #[tokio::test]
606    async fn test_execute_route_list() {
607        let cli = Cli {
608            command: Some(Command::RouteList {
609                format: "table".to_string(),
610            }),
611        };
612        let result = cli.execute().await;
613        assert!(result.is_ok());
614    }
615
616    #[tokio::test]
617    async fn test_execute_cache_clear() {
618        let _lock = crate::cmd::test_support::acquire_global_lock();
619        let temp = tempfile::tempdir().unwrap();
620        let original = std::env::current_dir().unwrap();
621        std::env::set_current_dir(temp.path()).unwrap();
622
623        let cli = Cli {
624            command: Some(Command::CacheClear { store: None }),
625        };
626        let result = cli.execute().await;
627        std::env::set_current_dir(&original).unwrap();
628        assert!(result.is_ok());
629    }
630
631    #[tokio::test]
632    async fn test_execute_seed_offline() {
633        let _lock = crate::cmd::test_support::acquire_global_lock();
634        let temp = tempfile::tempdir().unwrap();
635        let original = std::env::current_dir().unwrap();
636        std::env::set_current_dir(temp.path()).unwrap();
637
638        let cli = Cli {
639            command: Some(Command::Seed {
640                path: temp.path().to_string_lossy().to_string(),
641                db_type: "postgres".to_string(),
642                show_sql: false,
643                url: None,
644                class: None,
645            }),
646        };
647        let result = cli.execute().await;
648        std::env::set_current_dir(&original).unwrap();
649        assert!(result.is_ok());
650    }
651
652    #[tokio::test]
653    async fn test_execute_scheduler_list() {
654        let cli = Cli {
655            command: Some(Command::Scheduler {
656                scheduler_command: cmd::scheduler::SchedulerCommand::List {
657                    config: std::path::PathBuf::from("/nonexistent/scheduler.toml"),
658                },
659            }),
660        };
661        let result = cli.execute().await;
662        assert!(result.is_ok());
663    }
664
665    #[tokio::test]
666    async fn test_execute_optimize_route() {
667        let _lock = crate::cmd::test_support::acquire_global_lock();
668        let temp = tempfile::tempdir().unwrap();
669        let original = std::env::current_dir().unwrap();
670        std::env::set_current_dir(temp.path()).unwrap();
671
672        let cli = Cli {
673            command: Some(Command::OptimizeRoute),
674        };
675        let result = cli.execute().await;
676        std::env::set_current_dir(&original).unwrap();
677        assert!(result.is_ok());
678    }
679
680    #[tokio::test]
681    async fn test_execute_optimize_config() {
682        let _lock = crate::cmd::test_support::acquire_global_lock();
683        let temp = tempfile::tempdir().unwrap();
684        let original = std::env::current_dir().unwrap();
685        std::env::set_current_dir(temp.path()).unwrap();
686
687        // execute_optimize_config 需要 config 目录存在
688        std::fs::create_dir_all(temp.path().join("config")).unwrap();
689
690        let cli = Cli {
691            command: Some(Command::OptimizeConfig),
692        };
693        let result = cli.execute().await;
694        std::env::set_current_dir(&original).unwrap();
695        assert!(result.is_ok());
696    }
697
698    #[tokio::test]
699    async fn test_execute_optimize_schema() {
700        let _lock = crate::cmd::test_support::acquire_global_lock();
701        let temp = tempfile::tempdir().unwrap();
702        let original = std::env::current_dir().unwrap();
703        std::env::set_current_dir(temp.path()).unwrap();
704
705        let cli = Cli {
706            command: Some(Command::OptimizeSchema),
707        };
708        let result = cli.execute().await;
709        std::env::set_current_dir(&original).unwrap();
710        assert!(result.is_ok());
711    }
712
713    #[tokio::test]
714    async fn test_execute_route_clear() {
715        let _lock = crate::cmd::test_support::acquire_global_lock();
716        let temp = tempfile::tempdir().unwrap();
717        let original = std::env::current_dir().unwrap();
718        std::env::set_current_dir(temp.path()).unwrap();
719
720        let cli = Cli {
721            command: Some(Command::RouteClear),
722        };
723        let result = cli.execute().await;
724        std::env::set_current_dir(&original).unwrap();
725        assert!(result.is_ok());
726    }
727
728    #[tokio::test]
729    async fn test_execute_plugin_login_without_token() {
730        let cli = Cli {
731            command: Some(Command::Plugin {
732                plugin_command: cmd::plugin::PluginCommand::Login(cmd::plugin::LoginArgs {
733                    token: None,
734                    url: None,
735                }),
736            }),
737        };
738        let result = cli.execute().await;
739        assert!(result.is_ok());
740        assert_eq!(result.unwrap(), 1);
741    }
742
743    #[tokio::test]
744
745    async fn test_execute_serve_reload() {
746        let cli = Cli {
747            command: Some(Command::ServeReload),
748        };
749        let result = cli.execute().await;
750        assert!(result.is_ok());
751        assert_eq!(result.unwrap(), 0);
752    }
753
754    #[tokio::test]
755    async fn test_execute_serve_log_level() {
756        let cli = Cli {
757            command: Some(Command::ServeLogLevel),
758        };
759        let result = cli.execute().await;
760        assert!(result.is_ok());
761        assert_eq!(result.unwrap(), 0);
762    }
763
764    #[tokio::test]
765    async fn test_execute_admin_via_parse_list_routes() {
766        let cli = Cli::parse_from(["sz-rust", "admin", "list-routes"]);
767        let result = cli.execute().await;
768        assert!(result.is_ok());
769    }
770
771    #[tokio::test]
772    async fn test_execute_admin_via_parse_list_capabilities() {
773        let cli = Cli::parse_from(["sz-rust", "admin", "list-capabilities"]);
774        let result = cli.execute().await;
775        assert!(result.is_ok());
776    }
777
778    #[tokio::test]
779    async fn test_execute_admin_via_parse_migrate_offline() {
780        let cli = Cli::parse_from(["sz-rust", "admin", "migrate", "--show-sql"]);
781        let result = cli.execute().await;
782        assert!(result.is_ok());
783    }
784
785    #[tokio::test]
786    async fn test_execute_admin_via_parse_init_offline() {
787        let cli = Cli::parse_from(["sz-rust", "admin", "init"]);
788        let result = cli.execute().await;
789        assert!(result.is_ok());
790    }
791}