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
29 changes: 26 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ for secrets.
- Reads the API key from `EDS_API_KEY` or from `~/.config/eds/config.json`.
- Outputs JSON when piped, pretty tables on a TTY (`--json` to force).
- Repo uses the local `git` CLI for clone. Both products authenticate with
`X-API-KEY`.
`X-API-KEY`. `eds repo clone` embeds the API key as HTTP Basic Auth
credentials directly into the smart-HTTP URL it passes to `git clone`, so
clone/push work standalone — no git credential helper, OS keychain, or
`~/.netrc` needs to be pre-configured. This matters for CI and AI agent
sandboxes, which typically have none of those.

## Installation

Expand Down Expand Up @@ -112,6 +116,7 @@ eds repo create <name> create a repository
eds repo show <id-or-name> show repository details
eds repo delete <id-or-name> [--force] delete a repository
eds repo clone <id-or-name> [dir] [--ssh] clone via local git CLI
eds repo remote-add <id-or-name> [--name N] [--ssh] wire an existing local checkout to it (git remote add)

eds wf app create <name> --repository R|--repository-url URL --branch B create and auto-deploy a Workflow Studio application
eds wf app list list applications
Expand Down Expand Up @@ -223,8 +228,26 @@ cd demo
git add . && git commit -m "init" && git push origin main
```

`git push` works out of the box because the API key is used as
basic-auth credentials on the smart-HTTP endpoint exposed by the server.
`git push` works out of the box because `eds repo clone` already embedded the
API key as basic-auth credentials in `origin`'s URL (see `.git/config`) — no
git credential helper or OS keychain is involved. Note this means the API
key sits in plaintext in that repo's `.git/config`; treat the clone
directory with the same care as the key itself.

If the code already exists locally (no `eds repo clone` involved) and you
just created the remote repository, use `eds repo remote-add` instead of a
plain `git remote add` to get the same embedded authentication:

```bash
eds repo create demo
cd path/to/existing/local/repo
eds repo remote-add demo
git push -u origin main
```

`--ssh` is the one exception: it depends on the host's SSH public key being
registered separately (not handled by this CLI), so it still needs whatever
ambient SSH setup the environment provides.

## Distribution / publishing

Expand Down
102 changes: 100 additions & 2 deletions cmd/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bufio"
"context"
"fmt"
"net/url"
"os"
"os/exec"
"strings"
Expand All @@ -26,6 +27,7 @@ func newRepoCmd() *cobra.Command {
cmd.AddCommand(newRepoShowCmd())
cmd.AddCommand(newRepoDeleteCmd())
cmd.AddCommand(newRepoCloneCmd())
cmd.AddCommand(newRepoRemoteAddCmd())
return cmd
}

Expand Down Expand Up @@ -279,26 +281,39 @@ the clone, as you would with any other git server.`,
return fmt.Errorf("api get repository: %w", err)
}

useSSH := ssh && info.Clone.SSH != ""
cloneURL := info.Clone.HTTPS
if ssh && info.Clone.SSH != "" {
if useSSH {
cloneURL = info.Clone.SSH
}
if cloneURL == "" {
return fmt.Errorf("repository has no clone url")
}

// The smart-HTTP endpoint authenticates via HTTP Basic Auth using
// the API key as the password (any username works). Embed it in
// the URL so clone/push work standalone, without relying on a
// git credential helper or ~/.netrc being pre-configured in the
// environment (agents/CI have neither).
displayCloneURL := cloneURL
if !useSSH {
cloneURL = withBasicAuth(cloneURL, ctx.Cfg.APIKey)
}

dst := target
if dst == "" && len(args) == 2 {
dst = args[1]
}

gitArgs := []string{"clone", cloneURL}
displayArgs := []string{"clone", displayCloneURL}
if dst != "" {
gitArgs = append(gitArgs, dst)
displayArgs = append(displayArgs, dst)
}

if !ctx.Quiet {
fmt.Fprintf(cmd.OutOrStdout(), "Running: git %s\n", strings.Join(gitArgs, " "))
fmt.Fprintf(cmd.OutOrStdout(), "Running: git %s\n", strings.Join(displayArgs, " "))
}

c := exec.CommandContext(cmd.Context(), "git", gitArgs...)
Expand All @@ -314,6 +329,89 @@ the clone, as you would with any other git server.`,
return cmd
}

func newRepoRemoteAddCmd() *cobra.Command {
var (
remoteName string
ssh bool
)

cmd := &cobra.Command{
Use: "remote-add <repository-id-or-name>",
Short: "Wire an existing local git checkout to a Repo-product remote",
Long: `remote-add looks up the repository via the API and runs
"git remote add" in the current directory against its smart-HTTP (or SSH)
URL, with the API key embedded as HTTP Basic Auth credentials - same
authentication "eds repo clone" sets up, just for a local checkout that
already exists (e.g. code was scaffolded locally, then "eds repo create"
made the remote). If you don't have a local checkout yet, use
"eds repo clone" instead.`,
Args: cobra.ExactArgs(1),
Example: ` eds repo remote-add my-repo
eds repo remote-add my-repo --name upstream
eds repo remote-add my-repo --ssh`,
RunE: func(cmd *cobra.Command, args []string) error {
ctx, err := resolveContext(cmd)
if err != nil {
return err
}
if err := ctx.requireAPIKey(); err != nil {
return err
}

// Try to resolve the argument as either an id or a name.
id, err := resolveRepoID(cmd.Context(), ctx, args[0])
if err != nil {
return err
}

info, err := ctx.API.GetRepository(cmd.Context(), id)
if err != nil {
return fmt.Errorf("api get repository: %w", err)
}

useSSH := ssh && info.Clone.SSH != ""
remoteURL := info.Clone.HTTPS
if useSSH {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SSH остался, так задумано?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Ну он может быть, если на хосте настроен ssh.

remoteURL = info.Clone.SSH
}
if remoteURL == "" {
return fmt.Errorf("repository has no clone url")
}

displayURL := remoteURL
if !useSSH {
remoteURL = withBasicAuth(remoteURL, ctx.Cfg.APIKey)
}

if !ctx.Quiet {
fmt.Fprintf(cmd.OutOrStdout(), "Running: git remote add %s %s\n", remoteName, displayURL)
}

c := exec.CommandContext(cmd.Context(), "git", "remote", "add", remoteName, remoteURL)
c.Stdout = cmd.OutOrStdout()
c.Stderr = cmd.ErrOrStderr()
c.Stdin = os.Stdin
return c.Run()
},
}

cmd.Flags().StringVar(&remoteName, "name", "origin", "git remote name to create")
cmd.Flags().BoolVar(&ssh, "ssh", false, "use the SSH remote instead of HTTPS")
return cmd
}

// withBasicAuth embeds apiKey as the password of an HTTP Basic Auth userinfo
// component in rawURL (username is arbitrary; the server only checks the
// password). Returns rawURL unchanged if it doesn't parse as a URL.
func withBasicAuth(rawURL, apiKey string) string {
u, err := url.Parse(rawURL)
if err != nil || apiKey == "" {
return rawURL
}
u.User = url.UserPassword("eds", apiKey)
return u.String()
}

// resolveRepoID accepts either a raw repository id (UUID) or a repository
// name and returns the id. Names are resolved against the configured
// project's listing.
Expand Down
28 changes: 27 additions & 1 deletion skill/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ Errors go to stderr and the process exits non-zero.
| `eds repo show <id-or-name> [--json]` | Show details: id, default_branch, size, clone URLs |
| `eds repo delete <id-or-name> [--force] [--json]` | Delete (irreversible; requires confirmation unless `--force`) |
| `eds repo clone <id-or-name> [dir] [--ssh] [--target DIR]` | Clone via local `git` CLI |
| `eds repo remote-add <id-or-name> [--name N] [--ssh]` | Wire an existing local checkout to it (`git remote add`, authenticated) |
| `eds wf app create <name> --repository R\|--repository-url URL --branch B [--json]` | Create a Workflow Studio application from a repo + branch (auto-triggers first deploy) |
| `eds wf app list [--search S] [--sort created_at_asc\|created_at_desc] [--json]` | List applications |
| `eds wf app show <id> [--json]` | Show application details (status, run_id, pipeline_id, ...) |
Expand Down Expand Up @@ -161,15 +162,40 @@ cd ./work/my-new-repo
git status
```

`eds repo clone` (HTTPS mode, the default) embeds `EDS_API_KEY` as HTTP
Basic Auth credentials directly in the clone URL it hands to `git clone`.
It does **not** need or use a git credential helper, an OS keychain, or
`~/.netrc` — none of which exist in a typical agent sandbox. If a plain
`git clone <url>` (bypassing this CLI) ever fails with something like
`could not read Username ... terminal prompts disabled` or a keychain/
credential-helper error, that's this exact gap — use `eds repo clone`
instead of shelling out to `git clone` directly.

### Push code (after clone)

The CLI does not implement a custom upload path — use git directly:
The CLI does not implement a custom upload path — use git directly. Because
`eds repo clone` already wrote the API key into `origin`'s URL, `git push`
authenticates the same way, no extra setup needed:

```bash
cd ./work/my-new-repo
git add . && git commit -m "init" && git push origin main
```

### Wire up code that already exists locally (no clone involved)

If the code was scaffolded locally first and the repository was created
after the fact, use `eds repo remote-add` instead of a plain
`git remote add` — it embeds the same authenticated URL `eds repo clone`
would have:

```bash
eds repo create my-new-repo
cd path/to/existing/local/repo
eds repo remote-add my-new-repo
git push -u origin main
```

### Delete a repository (with confirmation)

```bash
Expand Down
Loading