Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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"
```

---
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,可以直接运行:
Expand Down Expand Up @@ -179,10 +179,10 @@ neocode model set gpt-4.1
neocode use <provider>

# 切换 provider 并指定模型(会做模型归属校验)
neocode use <provider> --model <model-id>
neocode use <provider> -m <model-id>

# 示例
neocode use openai --model gpt-4.1
neocode use openai -m gpt-4.1
```

#### Local Runner
Expand Down
4 changes: 2 additions & 2 deletions docs/guides/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

说明:
Expand Down Expand Up @@ -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
```

说明:
Expand Down
12 changes: 6 additions & 6 deletions docs/guides/feishu-adapter.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,本地无公网)
Expand All @@ -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`。
Expand Down
34 changes: 34 additions & 0 deletions internal/cli/adapter_command.go
Original file line number Diff line number Diff line change
@@ -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`")
},
}
}
103 changes: 103 additions & 0 deletions internal/cli/cli_ux_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}

2 changes: 1 addition & 1 deletion internal/cli/feishu_adapter_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
35 changes: 23 additions & 12 deletions internal/cli/feishu_adapter_command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}

Expand Down
12 changes: 10 additions & 2 deletions internal/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ type GlobalFlags struct {
Workdir string
Session string
WakeInputB64 string
Version bool
}

// Execute 执行 NeoCode 根命令入口,并在退出前等待静默更新检查收尾。
Expand Down Expand Up @@ -79,6 +80,10 @@ func NewRootCommand() *cobra.Command {
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
flags.Version = settings.GetBool("version")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-v/--version is wired in RunE, which means the root PersistentPreRunE has already run by the time this shortcut executes. As a result, neocode -v no longer matches neocode version: it still performs global preload, starts the background silent update check, and then runVersionCommand probes releases again. That makes the shortcut observable slower, can fail on preload paths that version intentionally skips, and contradicts the PR goal of keeping the same behavior/exit semantics. This needs to short-circuit before PersistentPreRunE, or the pre-run logic needs to explicitly skip when the root version flag is set.

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"))
Expand All @@ -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(),
Expand Down
2 changes: 1 addition & 1 deletion internal/cli/use_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
21 changes: 15 additions & 6 deletions internal/cli/version_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,19 +41,28 @@ 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)
},
}

cmd.Flags().BoolVar(&options.IncludePrerelease, "prerelease", false, "include prerelease versions")
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()
Expand Down
2 changes: 1 addition & 1 deletion www/en/guide/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion www/en/guide/context-session-workspace.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading
Loading