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
6 changes: 4 additions & 2 deletions packages/cli/internal/platform/errors/codes.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,9 @@ const (
INFISICAL_FOLDER_NOT_FOUND Code = "INFISICAL_FOLDER_NOT_FOUND"

// Run.
RUN_DOTENV_MISSING Code = "RUN_DOTENV_MISSING"
RUN_COMMAND_NOT_FOUND Code = "RUN_COMMAND_NOT_FOUND"
RUN_DOTENV_MISSING Code = "RUN_DOTENV_MISSING"
RUN_USAGE_INVALID Code = "RUN_USAGE_INVALID"

// Serve — local HTTP UI for editing profiles.
SERVE_PORT_BUSY Code = "SERVE_PORT_BUSY"
Expand Down Expand Up @@ -288,8 +289,9 @@ var Codes = map[Code]Definition{
INFISICAL_API_ERROR: {Summary: "Infisical API returned an unexpected error. See error.context for details."},
INFISICAL_FOLDER_NOT_FOUND: {Summary: "The requested Infisical folder does not exist in the requested environment.", Remediation: []output.Remediation{{Action: "check-env-name", Hint: "确认 --env 名是否拼对(dev / staging / prod 等)"}, {Action: "create-folder", Hint: "在该 folder 下写入第一个环境变量值时会自动创建", Command: "one env set --env <env> -p <name|path> KEY value"}, {Action: "verify-path", Hint: "或在 Infisical UI 里确认 folder 是否存在"}}},

RUN_DOTENV_MISSING: {Summary: "one run could not find a .env file for the resolved subproject.", Remediation: []output.Remediation{{Action: "pull-secrets", Hint: "先把 Infisical 环境变量拉到项目 .env", Command: "one env pull"}, {Action: "specify-subproject", Hint: "或显式指定项目(按 manifest 里的 name 或相对路径)", Command: "one run -p <name|path> -- <cmd>"}}},
RUN_COMMAND_NOT_FOUND: {Summary: "one run could not locate the requested executable on PATH.", Remediation: []output.Remediation{{Action: "check-spelling", Hint: "确认命令名拼写正确"}, {Action: "use-package-runner", Hint: "对于 npm script,使用包管理器调用", Command: "one run -- npm run <script>"}}},
RUN_DOTENV_MISSING: {Summary: "one run could not find a .env file for the resolved subproject.", Remediation: []output.Remediation{{Action: "pull-secrets", Hint: "先把 Infisical 环境变量拉到项目 .env", Command: "one env pull"}, {Action: "specify-subproject", Hint: "或显式指定项目(按 manifest 里的 name 或相对路径)", Command: "one run <name|path> -- <cmd>"}}},
RUN_USAGE_INVALID: {Summary: "one run arguments do not match `one run [project] -- <cmd> [args...]`.", Remediation: []output.Remediation{{Action: "use-run-separator", Hint: "用 -- 分隔 One CLI 参数和子进程命令", Command: "one run [project] -- <cmd> [args...]"}}},

SERVE_PORT_BUSY: {Summary: "one serve 无法绑定请求的端口(被占用或权限不足)。", Remediation: []output.Remediation{{Action: "use-random-port", Hint: "改用随机端口(让内核分配空闲端口)", Command: "one serve --port 0"}, {Action: "pick-different-port", Hint: "或显式换一个空闲端口", Command: "one serve --port 17900"}}},
SERVE_BIND_FORBIDDEN: {Summary: "one serve 拒绝绑定到非 loopback 地址(profile 文件含敏感凭据,仅 127.0.0.1 / localhost 才安全)。", Remediation: []output.Remediation{{Action: "use-loopback", Hint: "改用 127.0.0.1(默认)", Command: "one serve --host 127.0.0.1"}}},
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/internal/platform/i18n/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,10 @@
"run.short": "Load environment variables and run a command",
"run.env_empty": "⚠ No .env file was found, or it is empty; continuing.",
"run.injected": "✓ Loaded %d environment variables (source: %s)",
"run.separator_required": "Missing -- separator. Use `one run [project] -- <command> [args...]`.",
"run.command_required": "No command was provided after --. Use `one run [project] -- <command> [args...]`.",
"run.project_args_invalid": "Only one project name or path may appear before --.",
"run.selector_conflict": "The positional project and --project refer to different projects; keep only one form.",
"serve.short": "Open the local settings page",
"serve.started": "✓ Local settings page started: %s",
"serve.instructions": " Manage local connections in the browser; press Ctrl-C to stop.",
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/internal/platform/i18n/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,10 @@
"run.short": "载入环境变量并运行命令",
"run.env_empty": "⚠ 未找到 .env 或文件为空;继续执行。",
"run.injected": "✓ 已载入 %d 个环境变量(来源:%s)",
"run.separator_required": "缺少 -- 分隔符。请使用 `one run [project] -- <command> [args...]`。",
"run.command_required": "-- 后没有要执行的命令。请使用 `one run [project] -- <command> [args...]`。",
"run.project_args_invalid": "-- 前最多只能提供一个项目名或路径。",
"run.selector_conflict": "位置参数和 --project 指向了不同项目;请只保留一种写法。",
"serve.short": "打开本地设置页面",
"serve.started": "✓ 本地设置页面已启动:%s",
"serve.instructions": " 在浏览器中管理本机连接;按 Ctrl-C 停止。",
Expand Down
74 changes: 64 additions & 10 deletions packages/cli/internal/transport/cobra/run/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,19 @@ type runFlags struct {
func newRunCmd(loaders *secrets.Registry) *cobra.Command {
flags := &runFlags{}
cmd := &cobra.Command{
Use: "run [-p <name|path>] [--env-provider dotenv|infisical] [--env <name>] -- <cmd> [args...]",
Use: "run [project] [-p <name|path>] [--env-provider dotenv|infisical] [--env <name>] -- <cmd> [args...]",
DisableFlagsInUseLine: true,
Long: `把项目环境变量注入到任意命令,类似 infisical run。

项目解析:
- 不传 -p:从当前目录推导(必须 cd 到某个项目目录里)
- 不传 project / -p:从当前目录推导(必须 cd 到某个项目目录里)
- 传位置参数:按 manifest.projects[].name 或相对路径选择(如 web、apps/web)
- 传 -p <name>:按 manifest.projects[].name 选(如 -p web)
- 传 -p <relativeDir>:按相对路径选(如 -p apps/web)
- 位置参数和 -p 是等价写法;同时使用时必须指向同一个项目

命令分隔:
必须使用 -- 分隔 One CLI 参数和要执行的命令。

密钥来源(--env-provider,默认取 workspace 在 one create 时选择的 provider):
- dotenv :读 <project>/.env 文件
Expand All @@ -79,34 +84,83 @@ func newRunCmd(loaders *secrets.Registry) *cobra.Command {

示例:
one run -- npm run dev # 用 workspace 默认 provider
one run web -- npm start # 用位置参数选择项目
one run apps/web -- npm start # 位置参数也接受相对路径
one run -p web -- npm start # 按 manifest 里的 name 选
one run -p apps/web -- npm start # 按相对路径选
one run --env-provider dotenv -- npm test # 强制走 dotenv(离线场景)
one run --env staging -- npm run e2e # 用 staging 环境的密钥`,
Args: cobra.ArbitraryArgs,
DisableFlagParsing: false,
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
commandArgs, err := parseRunArgs(cmd, flags, args)
if err != nil {
return err
}
if len(commandArgs) == 0 {
// 用户没传命令视为请求帮助:走 stdout(方便 `one run | less`),
// 父命令空参那条路径在 cobra 里默认走 stderr,这里显式纠偏。
cmd.SetOut(os.Stdout)
return cmd.Help()
}
return runRun(cmd.Context(), loaders, flags, args)
return runRun(cmd.Context(), loaders, flags, commandArgs)
},
}
cmd.Flags().StringVarP(&flags.project, "project", "p", "", "项目名(manifest.projects[].name)或相对路径;默认从 cwd 推导")
cmd.Flags().StringVar(&flags.envName, "env", "", "环境名(默认取 manifest.environments.default)")
cmd.Flags().StringVar(&flags.envProvider, "env-provider", "", "env provider: dotenv | infisical(默认取 workspace manifest 中已选的值)")
// SetInterspersed(false) lets users skip the `--` separator: any token
// after the first positional is treated as a positional, including
// things that look like flags. Both `one run -- npm start --foo` and
// `one run npm start --foo` work; `--foo` reaches npm verbatim.
cmd.Flags().SetInterspersed(false)
i18n.MarkShort(cmd, "run.short")
return cmd
}

func parseRunArgs(cmd *cobra.Command, flags *runFlags, args []string) ([]string, error) {
dashIndex := cmd.ArgsLenAtDash()
if dashIndex < 0 {
if len(args) == 0 {
return nil, nil
}
Comment thread
caorushizi marked this conversation as resolved.
return nil, cliErrors.New(cliErrors.RUN_USAGE_INVALID,
i18n.T("run.separator_required")).
WithContext(map[string]any{"reason": "separator-required"})
}
if dashIndex > 1 {
return nil, cliErrors.New(cliErrors.RUN_USAGE_INVALID,
i18n.T("run.project_args_invalid")).
WithContext(map[string]any{
"reason": "too-many-projects",
"project_args": args[:dashIndex],
"project_count": dashIndex,
})
}
if dashIndex == 1 {
positional := args[0]
if flags.project != "" {
normalize := func(v string) string {
v = strings.TrimSpace(v)
v = strings.TrimSuffix(strings.TrimPrefix(v, "./"), "/")
return workspace.ToPosixPath(v)
}
if normalize(positional) != normalize(flags.project) {
return nil, cliErrors.New(cliErrors.RUN_USAGE_INVALID,
i18n.T("run.selector_conflict")).
WithContext(map[string]any{
"reason": "selector-conflict",
"positional_project": positional,
"flag_project": flags.project,
})
}
}
flags.project = positional
}
Comment thread
Copilot marked this conversation as resolved.

commandArgs := args[dashIndex:]
if len(commandArgs) == 0 {
return nil, cliErrors.New(cliErrors.RUN_USAGE_INVALID,
i18n.T("run.command_required")).
WithContext(map[string]any{"reason": "command-required"})
}
return commandArgs, nil
}

func runRun(ctx context.Context, loaders *secrets.Registry, flags *runFlags, args []string) error {
activeWorkspace, err := execution.ResolveWorkspace(ctx)
if err != nil {
Expand Down
12 changes: 9 additions & 3 deletions packages/cli/testdata/reference/help/run.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,20 @@ DESCRIPTION
Load environment variables and run a command

USAGE
one run [-p <name|path>] [--env-provider dotenv|infisical] [--env <name>] -- <cmd> [args...]
one run [project] [-p <name|path>] [--env-provider dotenv|infisical] [--env <name>] -- <cmd> [args...]

TIPS
把项目环境变量注入到任意命令,类似 infisical run。

项目解析:
- 不传 -p:从当前目录推导(必须 cd 到某个项目目录里)
- 不传 project / -p:从当前目录推导(必须 cd 到某个项目目录里)
- 传位置参数:按 manifest.projects[].name 或相对路径选择(如 web、apps/web)
- 传 -p <name>:按 manifest.projects[].name 选(如 -p web)
- 传 -p <relativeDir>:按相对路径选(如 -p apps/web)
- 位置参数和 -p 是等价写法;同时使用时必须指向同一个项目

命令分隔:
必须使用 -- 分隔 One CLI 参数和要执行的命令。

密钥来源(--env-provider,默认取 workspace 在 one create 时选择的 provider):
- dotenv :读 <project>/.env 文件
Expand All @@ -27,8 +32,9 @@ TIPS

示例:
one run -- npm run dev # 用 workspace 默认 provider
one run web -- npm start # 用位置参数选择项目
one run apps/web -- npm start # 位置参数也接受相对路径
one run -p web -- npm start # 按 manifest 里的 name 选
one run -p apps/web -- npm start # 按相对路径选
one run --env-provider dotenv -- npm test # 强制走 dotenv(离线场景)
one run --env staging -- npm run e2e # 用 staging 环境的密钥

Expand Down
53 changes: 53 additions & 0 deletions packages/cli/tests/e2e/snapshot_e2e_run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,20 @@ func TestSnapshot_E2E_Run_HappyPath(t *testing.T) {
}
}

func TestSnapshot_E2E_Run_PositionalProjectSelector(t *testing.T) {
ws, _ := addSubprojectWithDotenv(t, "auth",
"RUN_TEST_KEY=hello-from-dotenv\n")

args := append([]string{"run", "auth", "--"}, environmentEchoCommand("RUN_TEST_KEY")...)
stdout, stderr, code := runBinaryIn(t, ws, args...)
if code != 0 {
t.Fatalf("positional project run failed: exit %d\n stdout: %q\n stderr: %q", code, stdout, stderr)
}
if got := strings.TrimSpace(stdout); got != "hello-from-dotenv" {
t.Errorf("stdout: want %q, got %q", "hello-from-dotenv", got)
}
}

func TestSnapshot_E2E_Run_ExitCodePassthrough(t *testing.T) {
_, subDir := addSubprojectWithDotenv(t, "billing", "K=v\n")

Expand Down Expand Up @@ -117,6 +131,45 @@ func TestSnapshot_E2E_Run_MissingDotenv_RunsLeniently(t *testing.T) {
}
}

func TestSnapshot_E2E_Run_MissingSeparator_ReturnsStructuredError(t *testing.T) {
ws, _ := addSubprojectWithDotenv(t, "auth", "K=v\n")

_, stderr, code := runBinaryIn(t, ws, "run", "-o", "json", "auth", "echo", "hi")
if code == 0 {
t.Fatal("expected missing -- separator to fail")
}
got := mustParseJSON(t, firstJSONLine(stderr))
errMap := got["error"].(map[string]any)
if errMap["code"] != "RUN_USAGE_INVALID" {
t.Fatalf("expected RUN_USAGE_INVALID, got %v", got)
}
ctx := errMap["context"].(map[string]any)
if ctx["reason"] != "separator-required" {
t.Fatalf("expected separator-required context, got %v", ctx)
}
}

func TestSnapshot_E2E_Run_SelectorConflict_ReturnsStructuredError(t *testing.T) {
ws, _ := addSubprojectWithDotenv(t, "auth", "K=v\n")

_, stderr, code := runBinaryIn(t, ws, "run", "-o", "json", "auth", "-p", "billing", "--", "echo", "hi")
if code == 0 {
t.Fatal("expected conflicting project selectors to fail")
}
got := mustParseJSON(t, firstJSONLine(stderr))
errMap := got["error"].(map[string]any)
if errMap["code"] != "RUN_USAGE_INVALID" {
t.Fatalf("expected RUN_USAGE_INVALID, got %v", got)
}
ctx := errMap["context"].(map[string]any)
if ctx["reason"] != "selector-conflict" {
t.Fatalf("expected selector-conflict context, got %v", ctx)
}
if ctx["positional_project"] != "auth" || ctx["flag_project"] != "billing" {
t.Fatalf("expected conflicting selectors in context, got %v", ctx)
}
}

func TestSnapshot_E2E_Run_UnknownCommand_ReturnsStructuredError(t *testing.T) {
_, subDir := addSubprojectWithDotenv(t, "billing", "K=v\n")

Expand Down
16 changes: 10 additions & 6 deletions packages/skills/one-cli/references/REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -398,15 +398,18 @@ Injects project env vars and executes a command.
Usage:

```bash
one run [-p <name|path>] [--env-provider dotenv|infisical] \
one run [project] [-p <name|path>] [--env-provider dotenv|infisical] \
[--env <name>] -- <cmd> [args...]
```

`-p / --project` selects a project by manifest name (`-p web`) or relative
path (`-p apps/web`); without it, the cwd is used (must be inside a
project). `--env-provider` can force `dotenv` or `infisical`; when omitted,
the workspace's selected env backend is used. The child process always runs
from the resolved project directory.
The optional positional `project` selects a project by manifest name
(`one run web -- npm start`) or relative path
(`one run apps/web -- npm start`). `-p / --project` remains an equivalent,
explicit form. Without either selector, the cwd is used and must be inside a
project. The `--` separator is required; everything after it is passed to the
child process unchanged. `--env-provider` can force `dotenv` or `infisical`;
when omitted, the workspace's selected env backend is used. The child process
always runs from the resolved project directory.

## `one configure`

Expand Down Expand Up @@ -614,6 +617,7 @@ Install flags:
| `UNKNOWN_COMMAND` | root | use the current command catalog above |
| `INFISICAL_AUTH_MISSING` | env/remote run | configure env profile or credentials |
| `ENV_FILE_NOT_FOUND` | `one run --env-provider dotenv` | create project `.env` or use `--env-provider infisical` |
| `RUN_USAGE_INVALID` | `one run` | use `one run [project] -- <cmd> [args...]`; if both positional `project` and `-p/--project` are provided, they must refer to the same project |

Always prefer `error.context` and `error.remediation[]` over hard-coded
recovery text.
Loading