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
3 changes: 3 additions & 0 deletions apps/docs/content/docs/en/env-vars.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,12 @@ The manifest records only binding information and environment slugs. Values stay
one env
one env set DATABASE_URL -p api
one env set API_URL=https://example.com -p web --env dev --yes
one env set SIGNING_PRIVATE_KEY -p api --env dev --stdin < /secure/private-key.pem
one env list -p web --env dev
```

Use `--stdin` for multiline PEM files and other secrets without passing their contents as arguments. It works for both project variables and `--global` shared credentials, preserves embedded line breaks, and removes only one final line terminator. Input is limited to 1 MiB; combining `--stdin` with an argument value is rejected. Existing-variable overwrites still require confirmation or `--yes`.

`set KEY` prompts for a hidden value in a terminal. Scripts pass a value explicitly. `--yes` confirms overwrites and new environment names. `list` returns names only. The CLI has no plaintext read command; use `one exec` to inject values into a child process. `-o json` or `-o yaml` provides structured output with the stable schemas `one-cli/env-set/v1` and `one-cli/env-list/v1`.

`-p / --project` accepts a project name or workspace-relative path. Without it, One CLI infers the project from the current directory. At the workspace root, variables are shared; interactive `set` offers a scope selection.
Expand Down
3 changes: 3 additions & 0 deletions apps/docs/content/docs/zh/env-vars.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,12 @@ Manifest 只记录绑定信息和环境 slug。值保存在 Infisical,登录
one env
one env set DATABASE_URL -p api
one env set API_URL=https://example.com -p web --env dev --yes
one env set SIGNING_PRIVATE_KEY -p api --env dev --stdin < /secure/private-key.pem
one env list -p web --env dev
```

多行 PEM 和其他秘密可以通过 `--stdin` 读取,无需把内容放进命令参数。项目变量和 `--global` 共享凭据都支持该方式,保留内部换行,只去掉一个结尾换行符。输入上限为 1 MiB,不能同时提供参数值。覆盖已有变量仍需确认或显式指定 `--yes`。

终端中的 `set KEY` 会隐藏输入值;脚本显式传入值。`--yes` 确认覆盖已有值或新增环境名。`list` 只返回变量名。CLI 不提供明文读取命令;通过 `one exec` 将值注入子进程。`-o json` 或 `-o yaml` 输出结构化结果,保留 `one-cli/env-set/v1`、`one-cli/env-list/v1` 协议。

`-p / --project` 接受项目名或工作区相对路径。不传时按当前目录推断项目;在工作区根目录操作共享变量,交互式 `set` 会提供作用域选择。
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/internal/platform/i18n/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@
"env.bind.select_environment": "Default browsing environment",
"env.unset.short": "Delete an Infisical environment variable",
"env.flag.project_selector": "Project name or path",
"env.flag.stdin": "Read the value from standard input to keep it out of shell history (shared credentials)",
"env.flag.stdin": "Read a project variable or shared credential from standard input to keep it out of shell history",
"env.path_global_required": "--path is only available with --global.",
"env.global_project_conflict": "--global cannot be combined with --project.",
"env.stdin_value_conflict": "--stdin cannot be combined with a value argument.",
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/internal/platform/i18n/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@
"env.bind.select_environment": "默认浏览环境",
"env.unset.short": "删除一个 Infisical 环境变量",
"env.flag.project_selector": "项目名或路径",
"env.flag.stdin": "从标准输入读取值,避免写入命令历史(共享凭据)",
"env.flag.stdin": "从标准输入读取项目变量或共享凭据,避免写入命令历史",
"env.path_global_required": "--path 仅用于 --global",
"env.global_project_conflict": "--global 不能与 --project 同时使用",
"env.stdin_value_conflict": "--stdin 不能同时提供参数值",
Expand Down
28 changes: 6 additions & 22 deletions packages/cli/internal/transport/cobra/env/global.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package envcmd

import (
"io"
"strings"

"github.com/spf13/cobra"

environmentmodule "github.com/torchstellar-team/one-cli/packages/cli/internal/modules/environment"
Expand Down Expand Up @@ -94,10 +91,6 @@ func configureGlobal(parent *cobra.Command, deps Dependencies) {
if c == bind {
continue
}
if c.Name() == "set" {
c.Flags().Bool("stdin", false, i18n.T("env.flag.stdin"))
i18n.MarkFlagUsage(c, "stdin", "env.flag.stdin")
}
original := c.RunE
if original == nil {
continue
Expand Down Expand Up @@ -137,21 +130,12 @@ func configureGlobal(parent *cobra.Command, deps Dependencies) {
case "unset":
result, e = remote.GlobalSecret(cmd.Context(), cmd.Name(), env, folder, args[0], "")
case "set":
key, value := parseSetArgs(args)
stdin, _ := cmd.Flags().GetBool("stdin")
if stdin {
if setValueProvided(args) {
return i18n.Errorf("env.stdin_value_conflict")
}
b, err := io.ReadAll(io.LimitReader(cmd.InOrStdin(), (1<<20)+1))
if err != nil {
return err
}
if len(b) > 1<<20 {
return i18n.Errorf("env.value_too_large")
}
value = strings.TrimSuffix(strings.TrimSuffix(string(b), "\n"), "\r")
} else if !setValueProvided(args) {
key, _ := parseSetArgs(args)
value, provided, err := readSetValue(cmd, args)
if err != nil {
return err
}
if !provided {
if !output.CanPrompt() {
return i18n.Errorf("env.stdin_required")
}
Expand Down
31 changes: 31 additions & 0 deletions packages/cli/internal/transport/cobra/env/input.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package envcmd

import (
"io"
"strings"

"github.com/spf13/cobra"
"github.com/torchstellar-team/one-cli/packages/cli/internal/platform/i18n"
)

// readSetValue handles both project variables and shared credentials. Limit the
// read before planning remote writes, and remove only the final line terminator.
func readSetValue(cmd *cobra.Command, args []string) (string, bool, error) {
_, value := parseSetArgs(args)
provided := setValueProvided(args)
stdin, _ := cmd.Flags().GetBool("stdin")
if !stdin {
return value, provided, nil
}
if provided {
return "", false, i18n.Errorf("env.stdin_value_conflict")
}
b, err := io.ReadAll(io.LimitReader(cmd.InOrStdin(), (1<<20)+1))
if err != nil {
return "", false, err
}
if len(b) > 1<<20 {
return "", false, i18n.Errorf("env.value_too_large")
}
return strings.TrimSuffix(strings.TrimSuffix(string(b), "\n"), "\r"), true, nil
}
85 changes: 85 additions & 0 deletions packages/cli/internal/transport/cobra/env/input_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package envcmd

import (
"errors"
"strings"
"testing"

"github.com/spf13/cobra"
)

func TestReadSetValuePreservesMultilineInput(t *testing.T) {
t.Parallel()
for _, test := range []struct{ name, input, want string }{
{"LF", "-----BEGIN PRIVATE KEY-----\nfixture\n-----END PRIVATE KEY-----\n", "-----BEGIN PRIVATE KEY-----\nfixture\n-----END PRIVATE KEY-----"},
{"CRLF", "header\r\nbody\r\n", "header\r\nbody"},
{"two trailing lines", "body\n\n", "body\n"},
{"literal escape", "header\\nbody", "header\\nbody"},
{"empty", "", ""},
{"limit", strings.Repeat("a", 1<<20), strings.Repeat("a", 1<<20)},
} {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
cmd := &cobra.Command{}
cmd.Flags().Bool("stdin", true, "")
cmd.SetIn(strings.NewReader(test.input))
got, provided, err := readSetValue(cmd, []string{"PRIVATE_KEY"})
if err != nil || !provided || got != test.want {
t.Fatalf("stdin value was not preserved (provided=%v, error=%v)", provided, err)
}
})
}
}

func TestSetRejectsStdinConflictAndOversizeBeforeRemoteAccess(t *testing.T) {
for _, global := range []bool{false, true} {
for _, test := range []struct {
args []string
input string
}{
{[]string{"PRIVATE_KEY", "private-argument-marker", "--stdin"}, ""},
{[]string{"PRIVATE_KEY=private-argument-marker", "--stdin"}, ""},
{[]string{"PRIVATE_KEY", "--stdin"}, strings.Repeat("private-input-marker", 60_000)},
} {
// A nil service proves these failures cannot reach project planning or writes.
cmd := Commands(Dependencies{})[0]
cmd.SilenceUsage = true
cmd.SilenceErrors = true
args := append([]string{"set"}, test.args...)
if global {
args = append(args, "--global")
}
cmd.SetArgs(args)
cmd.SetIn(strings.NewReader(test.input))
err := cmd.Execute()
if err == nil || strings.Contains(err.Error(), "private-argument-marker") || strings.Contains(err.Error(), "private-input-marker") {
t.Fatalf("unsafe stdin rejection (global=%v): %v", global, err)
}
}
}
}

type brokenInput struct{ err error }

func (input brokenInput) Read([]byte) (int, error) { return 0, input.err }

func TestReadSetValuePreservesReadErrorAndExplicitValueBehavior(t *testing.T) {
cmd := &cobra.Command{}
cmd.Flags().Bool("stdin", true, "")
want := errors.New("stdin transport failed")
cmd.SetIn(brokenInput{want})
if _, _, err := readSetValue(cmd, []string{"TOKEN"}); !errors.Is(err, want) {
t.Fatalf("lost stdin error: %v", err)
}
if err := cmd.Flags().Set("stdin", "false"); err != nil {
t.Fatal(err)
}
value, provided, err := readSetValue(cmd, []string{"TOKEN="})
if err != nil || !provided || value != "" {
t.Fatal("explicit empty values changed")
}
_, provided, err = readSetValue(cmd, []string{"TOKEN"})
if err != nil || provided {
t.Fatal("hidden-prompt selection changed")
}
}
10 changes: 8 additions & 2 deletions packages/cli/internal/transport/cobra/env/set.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,18 @@ func newSetCmd(deps Dependencies) *cobra.Command {
Long: i18n.T("env.set.tip"),
Args: validateSetArgs,
RunE: func(cmd *cobra.Command, args []string) error {
value, provided, err := readSetValue(cmd, args)
if err != nil {
return err
}
plan, err := deps.Service.PlanSet(environmentmodule.PlanSetInput{
Scope: commandScope(cmd), Environment: environment, Project: project,
})
if err != nil {
return err
}
key, value := parseSetArgs(args)
if !setValueProvided(args) {
key, _ := parseSetArgs(args)
if !provided {
if !output.CanPrompt() {
return cliErrors.New(cliErrors.ENV_SET_VALUE_REQUIRED, i18n.T("env.value_required"))
}
Expand Down Expand Up @@ -87,6 +91,8 @@ func newSetCmd(deps Dependencies) *cobra.Command {
i18n.MarkFlagUsage(cmd, "env", "env.flag.environment")
cmd.Flags().BoolVarP(&yes, "yes", "y", false, i18n.T("env.flag.yes"))
i18n.MarkFlagUsage(cmd, "yes", "env.flag.yes")
cmd.Flags().Bool("stdin", false, i18n.T("env.flag.stdin"))
i18n.MarkFlagUsage(cmd, "stdin", "env.flag.stdin")
markEnvFlagUsage(cmd, "project", "env", "yes")
i18n.MarkShort(cmd, "env.set.short")
i18n.MarkLong(cmd, "env.set.tip")
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/testdata/reference/help/env_set.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ COMMON OPTIONS
-o, --output <value> Output format: json | yaml | text (default: terminal-friendly text, JSON when piped)
--path <value> Shared credential folder (current level only, not recursive)
-p, --project <value> Project name or relative path; defaults to the current project
--stdin Read the value from standard input to keep it out of shell history (shared credentials)
--stdin Read a project variable or shared credential from standard input to keep it out of shell history
-y, --yes Confirm overwrites and new environments non-interactively
Loading