diff --git a/README.en.md b/README.en.md index fb5af637..b1888285 100644 --- a/README.en.md +++ b/README.en.md @@ -110,7 +110,7 @@ $env:OPENAI_API_KEY = "your_key_here" Then start with your workspace: ```bash -neocode --workdir /path/to/your/project +neocode -w /path/to/your/project ``` To launch the browser-based Web UI: @@ -154,7 +154,7 @@ Detailed docs are intentionally split out. README keeps entry links: neocode runner --gateway-address "your-gateway.com:8080" --token-file ~/.neocode/auth.json # Start feishu adapter (SDK mode, no public network required) -neocode feishu-adapter --ingress sdk --gateway-listen "127.0.0.1:8080" +neocode adapter feishu --ingress sdk --gateway-listen "127.0.0.1:8080" ``` --- diff --git a/README.md b/README.md index 7a3d3637..43466bb9 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ $env:OPENAI_API_KEY = "your_key_here" 然后在项目目录中启动: ```bash -neocode --workdir /path/to/your/project +neocode -w /path/to/your/project ``` 如果你希望使用浏览器 Web UI,可以直接运行: @@ -179,10 +179,10 @@ neocode model set gpt-4.1 neocode use # 切换 provider 并指定模型(会做模型归属校验) -neocode use --model +neocode use -m # 示例 -neocode use openai --model gpt-4.1 +neocode use openai -m gpt-4.1 ``` #### Local Runner diff --git a/docs/guides/configuration.md b/docs/guides/configuration.md index 64130bb0..d7b8eaac 100644 --- a/docs/guides/configuration.md +++ b/docs/guides/configuration.md @@ -331,7 +331,7 @@ $env:MODELSCOPE_API_KEY = "ms-..." 工作目录不写入 `config.yaml`,只通过启动参数覆盖: ```bash -go run ./cmd/neocode --workdir /path/to/workspace +go run ./cmd/neocode -w /path/to/workspace ``` 说明: @@ -373,7 +373,7 @@ feishu: ```bash export FEISHU_APP_SECRET="cli_secret_xxx" export FEISHU_SIGNING_SECRET="signing_secret_xxx" # 仅 webhook 模式需要 -neocode feishu-adapter +neocode adapter feishu ``` 说明: diff --git a/docs/guides/feishu-adapter.md b/docs/guides/feishu-adapter.md index 6db6f60c..b0617bdb 100644 --- a/docs/guides/feishu-adapter.md +++ b/docs/guides/feishu-adapter.md @@ -47,20 +47,20 @@ ```bash # 开发模式 (go run) -go run ./cmd/neocode feishu-adapter --ingress webhook +go run ./cmd/neocode adapter feishu --ingress webhook # 安装模式 (neocode) -neocode feishu-adapter --ingress webhook +neocode adapter feishu --ingress webhook ``` 通常还会覆盖地址参数: ```bash # 开发模式 (go run) -go run ./cmd/neocode feishu-adapter --ingress webhook --listen 127.0.0.1:18080 --event-path /feishu/events --card-path /feishu/cards +go run ./cmd/neocode adapter feishu --ingress webhook --listen 127.0.0.1:18080 --event-path /feishu/events --card-path /feishu/cards # 安装模式 (neocode) -neocode feishu-adapter --ingress webhook --listen 127.0.0.1:18080 --event-path /feishu/events --card-path /feishu/cards +neocode adapter feishu --ingress webhook --listen 127.0.0.1:18080 --event-path /feishu/events --card-path /feishu/cards ``` ### 4.2 SDK 模式(#557,本地无公网) @@ -69,10 +69,10 @@ neocode feishu-adapter --ingress webhook --listen 127.0.0.1:18080 --event-path / export FEISHU_APP_SECRET="cli_secret_xxx" # 开发模式 (go run) -go run ./cmd/neocode feishu-adapter --ingress sdk +go run ./cmd/neocode adapter feishu --ingress sdk # 安装模式 (neocode) -neocode feishu-adapter --ingress sdk +neocode adapter feishu --ingress sdk ``` SDK 模式下不要求公网回调地址,不要求 `adapter.listen/event_path/card_path`。 diff --git a/internal/cli/adapter_command.go b/internal/cli/adapter_command.go new file mode 100644 index 00000000..76205053 --- /dev/null +++ b/internal/cli/adapter_command.go @@ -0,0 +1,34 @@ +package cli + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// newAdapterCommand 创建适配器命令组,统一承载外部协作平台桥接能力。 +func newAdapterCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "adapter", + Short: "Manage collaboration adapters", + SilenceUsage: true, + Args: cobra.NoArgs, + } + cmd.AddCommand( + newFeishuAdapterCommand(), + ) + return cmd +} + +// newLegacyFeishuAdapterCommand 创建旧入口占位命令,提示用户迁移到新命令组。 +func newLegacyFeishuAdapterCommand() *cobra.Command { + return &cobra.Command{ + Use: "feishu-adapter", + Short: "Deprecated: use `adapter feishu`", + SilenceUsage: true, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return fmt.Errorf("command `feishu-adapter` has moved to `adapter feishu`") + }, + } +} diff --git a/internal/cli/cli_ux_test.go b/internal/cli/cli_ux_test.go new file mode 100644 index 00000000..f125fcdd --- /dev/null +++ b/internal/cli/cli_ux_test.go @@ -0,0 +1,103 @@ +package cli + +import ( + "bytes" + "context" + "strings" + "testing" + + "github.com/spf13/cobra" + + "neo-code/internal/app" +) + +func TestRootCommandSupportsWorkdirShortFlag(t *testing.T) { + originalLauncher := launchRootProgram + t.Cleanup(func() { launchRootProgram = originalLauncher }) + + var captured app.BootstrapOptions + launchRootProgram = func(ctx context.Context, opts app.BootstrapOptions) error { + captured = opts + return nil + } + + cmd := NewRootCommand() + cmd.SetArgs([]string{"-w", "/tmp/project"}) + if err := cmd.ExecuteContext(context.Background()); err != nil { + t.Fatalf("ExecuteContext() error = %v", err) + } + if captured.Workdir != "/tmp/project" { + t.Fatalf("workdir = %q, want %q", captured.Workdir, "/tmp/project") + } +} + +func TestUseCommandSupportsModelShortFlag(t *testing.T) { + svc := &mockSelectionService{} + cmd := newUseCommandWithResolver(staticSelectionResolver(svc)) + + originalRunner := runUseCommand + t.Cleanup(func() { runUseCommand = originalRunner }) + + called := false + runUseCommand = func(c *cobra.Command, gotSvc SelectionService, name string, opts useCommandOptions) error { + called = true + if opts.Model != "gpt-4.1" { + t.Fatalf("opts.Model = %q, want %q", opts.Model, "gpt-4.1") + } + return nil + } + + cmd.SetArgs([]string{"openai", "-m", "gpt-4.1"}) + if err := cmd.ExecuteContext(context.Background()); err != nil { + t.Fatalf("ExecuteContext() error = %v", err) + } + if !called { + t.Fatal("expected runUseCommand called") + } +} + +func TestRootCommandSupportsVersionFlags(t *testing.T) { + originalLauncher := launchRootProgram + originalVersionRunner := runVersionCommand + t.Cleanup(func() { launchRootProgram = originalLauncher }) + t.Cleanup(func() { runVersionCommand = originalVersionRunner }) + + launchRootProgram = func(context.Context, app.BootstrapOptions) error { + t.Fatal("launcher should not run when -v/--version is used") + return nil + } + runVersionCommand = func(context.Context, versionCommandOptions) (versionCommandResult, error) { + return versionCommandResult{ + CurrentVersion: "v1.0.0", + LatestVersion: "v1.0.0", + Comparable: true, + }, nil + } + + for _, args := range [][]string{{"-v"}, {"--version"}} { + cmd := NewRootCommand() + var out bytes.Buffer + cmd.SetOut(&out) + cmd.SetArgs(args) + if err := cmd.ExecuteContext(context.Background()); err != nil { + t.Fatalf("ExecuteContext(%v) error = %v", args, err) + } + text := out.String() + if !strings.Contains(text, "Current version: v1.0.0") { + t.Fatalf("output(%v) = %q, want current version", args, text) + } + } +} + +func TestLegacyFeishuAdapterCommandShowsMigrationHint(t *testing.T) { + cmd := NewRootCommand() + cmd.SetArgs([]string{"feishu-adapter"}) + err := cmd.ExecuteContext(context.Background()) + if err == nil { + t.Fatal("expected migration hint error") + } + if !strings.Contains(err.Error(), "adapter feishu") { + t.Fatalf("err = %v, want contains adapter feishu", err) + } +} + diff --git a/internal/cli/feishu_adapter_command.go b/internal/cli/feishu_adapter_command.go index e6b9c389..853ade3d 100644 --- a/internal/cli/feishu_adapter_command.go +++ b/internal/cli/feishu_adapter_command.go @@ -43,7 +43,7 @@ type feishuAdapterCommandOptions struct { func newFeishuAdapterCommand() *cobra.Command { options := &feishuAdapterCommandOptions{} cmd := &cobra.Command{ - Use: "feishu-adapter", + Use: "feishu", Short: "Start Feishu adapter bridge for gateway", SilenceUsage: true, Args: cobra.NoArgs, diff --git a/internal/cli/feishu_adapter_command_test.go b/internal/cli/feishu_adapter_command_test.go index 352e76a9..aa9e6c3b 100644 --- a/internal/cli/feishu_adapter_command_test.go +++ b/internal/cli/feishu_adapter_command_test.go @@ -119,23 +119,34 @@ func TestMergeFeishuOptionsAppliesAllCLIOverrides(t *testing.T) { } } -func TestNewRootCommandContainsFeishuAdapter(t *testing.T) { +func TestNewRootCommandContainsAdapterFeishu(t *testing.T) { root := NewRootCommand() - found := false + foundAdapter := false + foundFeishu := false for _, command := range root.Commands() { - if command.Name() == "feishu-adapter" { - found = true - if !shouldSkipGlobalPreload(command) { - t.Fatal("feishu-adapter should skip global preload") - } - if !shouldSkipSilentUpdateCheck(command) { - t.Fatal("feishu-adapter should skip silent update check") + if command.Name() != "adapter" { + continue + } + foundAdapter = true + for _, child := range command.Commands() { + if child.Name() == "feishu" { + foundFeishu = true + if !shouldSkipGlobalPreload(child) { + t.Fatal("adapter feishu should skip global preload") + } + if !shouldSkipSilentUpdateCheck(child) { + t.Fatal("adapter feishu should skip silent update check") + } + break } - break } + break + } + if !foundAdapter { + t.Fatal("expected adapter command in root") } - if !found { - t.Fatal("expected feishu-adapter command in root") + if !foundFeishu { + t.Fatal("expected feishu command under adapter") } } diff --git a/internal/cli/root.go b/internal/cli/root.go index c6aa68ef..8d672ef9 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -42,6 +42,7 @@ type GlobalFlags struct { Workdir string Session string WakeInputB64 string + Version bool } // Execute 执行 NeoCode 根命令入口,并在退出前等待静默更新检查收尾。 @@ -79,6 +80,10 @@ func NewRootCommand() *cobra.Command { return nil }, RunE: func(cmd *cobra.Command, args []string) error { + flags.Version = settings.GetBool("version") + if flags.Version { + return runVersionShortcut(cmd.Context(), cmd.OutOrStdout()) + } flags.Workdir = strings.TrimSpace(settings.GetString("workdir")) flags.Session = strings.TrimSpace(settings.GetString("session")) flags.WakeInputB64 = strings.TrimSpace(settings.GetString("wake-input-b64")) @@ -89,16 +94,19 @@ func NewRootCommand() *cobra.Command { }) }, } - cmd.PersistentFlags().String("workdir", "", "workdir override for current run") + cmd.PersistentFlags().BoolP("version", "v", false, "show current version and check for updates") + cmd.PersistentFlags().StringP("workdir", "w", "", "workdir override for current run") cmd.PersistentFlags().String("session", "", "session id to hydrate on startup") cmd.PersistentFlags().String("wake-input-b64", "", "internal wake startup payload") _ = cmd.PersistentFlags().MarkHidden("wake-input-b64") + _ = settings.BindPFlag("version", cmd.PersistentFlags().Lookup("version")) _ = settings.BindPFlag("workdir", cmd.PersistentFlags().Lookup("workdir")) _ = settings.BindPFlag("session", cmd.PersistentFlags().Lookup("session")) _ = settings.BindPFlag("wake-input-b64", cmd.PersistentFlags().Lookup("wake-input-b64")) cmd.AddCommand( newGatewayCommand(), - newFeishuAdapterCommand(), + newAdapterCommand(), + newLegacyFeishuAdapterCommand(), newRunnerCommand(), newWebCommand(), newDaemonCommand(), diff --git a/internal/cli/use_command.go b/internal/cli/use_command.go index 649a52ef..d976bedc 100644 --- a/internal/cli/use_command.go +++ b/internal/cli/use_command.go @@ -39,7 +39,7 @@ func newUseCommandWithResolver(resolver selectionServiceResolver) *cobra.Command return runUseCommand(cmd, svc, args[0], opts) }, } - cmd.Flags().StringVar(&opts.Model, "model", "", "model to select for the provider") + cmd.Flags().StringVarP(&opts.Model, "model", "m", "", "model to select for the provider") return cmd } diff --git a/internal/cli/version_command.go b/internal/cli/version_command.go index 4c539bfd..cf3ced5a 100644 --- a/internal/cli/version_command.go +++ b/internal/cli/version_command.go @@ -41,12 +41,7 @@ func newVersionCommand() *cobra.Command { SilenceUsage: true, Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - result, err := runVersionCommand(cmd.Context(), *options) - if err != nil { - return err - } - printVersionCommandResult(cmd.OutOrStdout(), result) - return nil + return runVersionShortcut(cmd.Context(), cmd.OutOrStdout(), *options) }, } @@ -54,6 +49,20 @@ func newVersionCommand() *cobra.Command { return cmd } +// runVersionShortcut 复用 version 子命令核心逻辑,供顶层 --version/-v 与子命令统一输出。 +func runVersionShortcut(ctx context.Context, out io.Writer, options ...versionCommandOptions) error { + resolved := versionCommandOptions{} + if len(options) > 0 { + resolved = options[0] + } + result, err := runVersionCommand(ctx, resolved) + if err != nil { + return err + } + printVersionCommandResult(out, result) + return nil +} + // defaultVersionCommandRunner 执行版本探测并构造可展示结果,探测失败不返回执行错误。 func defaultVersionCommandRunner(ctx context.Context, options versionCommandOptions) (versionCommandResult, error) { currentVersion := readCurrentVersion() diff --git a/www/en/guide/configuration.md b/www/en/guide/configuration.md index c3703acd..6e04d904 100644 --- a/www/en/guide/configuration.md +++ b/www/en/guide/configuration.md @@ -88,7 +88,7 @@ If the model list is empty, first check that the active provider's API key is se Use the launch argument for workspaces: ```bash -neocode --workdir /path/to/project +neocode -w /path/to/project ``` Inside NeoCode: diff --git a/www/en/guide/context-session-workspace.md b/www/en/guide/context-session-workspace.md index b61224fe..08aa72b9 100644 --- a/www/en/guide/context-session-workspace.md +++ b/www/en/guide/context-session-workspace.md @@ -14,7 +14,7 @@ The workspace decides which project the agent can inspect. A session stores one Launch NeoCode in a project: ```bash -neocode --workdir /path/to/project +neocode -w /path/to/project ``` View or switch workspace: diff --git a/www/en/guide/daily-use.md b/www/en/guide/daily-use.md index 23bf6bfe..88a8d6b0 100644 --- a/www/en/guide/daily-use.md +++ b/www/en/guide/daily-use.md @@ -12,7 +12,7 @@ A normal NeoCode workflow is: open a project, describe the goal, watch agent act Start in a workspace: ```bash -neocode --workdir /path/to/project +neocode -w /path/to/project ``` View or switch workspace inside NeoCode: diff --git a/www/en/guide/install.md b/www/en/guide/install.md index 68edbcde..df48b1c4 100644 --- a/www/en/guide/install.md +++ b/www/en/guide/install.md @@ -58,7 +58,7 @@ neocode Open a specific project: ```bash -neocode --workdir /path/to/your/project +neocode -w /path/to/your/project ``` Type natural language in the input box to chat. Type `/` to see local control commands. diff --git a/www/guide/configuration.md b/www/guide/configuration.md index 33086a40..feb0aaac 100644 --- a/www/guide/configuration.md +++ b/www/guide/configuration.md @@ -88,7 +88,7 @@ current_model: gemini-2.5-pro 工作区建议通过启动参数指定,不写进主配置: ```bash -neocode --workdir /path/to/project +neocode -w /path/to/project ``` ## Shell 和工具超时 diff --git a/www/guide/context-session-workspace.md b/www/guide/context-session-workspace.md index 6a3cdef8..82d1c1fa 100644 --- a/www/guide/context-session-workspace.md +++ b/www/guide/context-session-workspace.md @@ -16,7 +16,7 @@ NeoCode 的日常使用围绕三个概念:工作区、会话和上下文。 启动时指定工作区: ```bash -neocode --workdir /path/to/project +neocode -w /path/to/project ``` 建议一个会话只服务一个工作区。切换到另一个项目时,新建会话会更清楚。 diff --git a/www/guide/daily-use.md b/www/guide/daily-use.md index 1f67e726..dd27d9a1 100644 --- a/www/guide/daily-use.md +++ b/www/guide/daily-use.md @@ -16,7 +16,7 @@ NeoCode 默认在终端中运行。如果你更喜欢图形界面,可以通过 启动时指定工作区: ```bash -neocode --workdir /path/to/project +neocode -w /path/to/project ``` 工作区决定 NeoCode 能读取、搜索、编辑和执行命令的项目范围。切换到另一个项目时,建议新建会话,避免旧上下文混入。 diff --git a/www/guide/feishu-remote-setup.md b/www/guide/feishu-remote-setup.md index c666a88b..1e9963b0 100644 --- a/www/guide/feishu-remote-setup.md +++ b/www/guide/feishu-remote-setup.md @@ -138,18 +138,18 @@ Adapter 负责桥接飞书长连接与本地 Gateway,把飞书消息翻译为 ```bash # 开发模式 (go run) -go run ./cmd/neocode feishu-adapter --ingress sdk --gateway-listen "127.0.0.1:8080" +go run ./cmd/neocode adapter feishu --ingress sdk --gateway-listen "127.0.0.1:8080" # 安装模式 (neocode) -neocode feishu-adapter --ingress sdk --gateway-listen "127.0.0.1:8080" +neocode adapter feishu --ingress sdk --gateway-listen "127.0.0.1:8080" ``` ```powershell # 开发模式 (go run) -go run ./cmd/neocode feishu-adapter --ingress sdk --gateway-listen "\\.\pipe\neocode-gateway" +go run ./cmd/neocode adapter feishu --ingress sdk --gateway-listen "\\.\pipe\neocode-gateway" # 安装模式 (neocode) -neocode feishu-adapter --ingress sdk --gateway-listen "\\.\pipe\neocode-gateway" +neocode adapter feishu --ingress sdk --gateway-listen "\\.\pipe\neocode-gateway" ``` **Adapter 启动参数说明:** @@ -265,10 +265,10 @@ neocode gateway --listen "127.0.0.1:8080" --http-listen "127.0.0.1:18181" --work ```bash # 开发模式 (go run) -go run ./cmd/neocode feishu-adapter --ingress webhook --gateway-listen "127.0.0.1:8080" --listen "127.0.0.1:18080" +go run ./cmd/neocode adapter feishu --ingress webhook --gateway-listen "127.0.0.1:8080" --listen "127.0.0.1:18080" # 安装模式 (neocode) -neocode feishu-adapter --ingress webhook --gateway-listen "127.0.0.1:8080" --listen "127.0.0.1:18080" +neocode adapter feishu --ingress webhook --gateway-listen "127.0.0.1:8080" --listen "127.0.0.1:18080" ``` 然后用 ngrok / cloudflared 把 `18080` 暴露公网,在飞书后台配置: diff --git a/www/guide/install.md b/www/guide/install.md index 252b516e..ee856c21 100644 --- a/www/guide/install.md +++ b/www/guide/install.md @@ -62,7 +62,7 @@ neocode 如果要直接打开指定项目: ```bash -neocode --workdir /path/to/your/project +neocode -w /path/to/your/project ``` 启动后会进入终端界面,底部是输入框。直接输入自然语言即可开始对话;输入 `/` 可以打开本地控制命令建议。