Skip to content

Commit b750662

Browse files
authored
Merge pull request #13 from cloud-ru/feat/repo-auth-cmd
Feat/repo auth cmd
2 parents e5eca85 + 729f7ed commit b750662

3 files changed

Lines changed: 153 additions & 6 deletions

File tree

README.md

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ for secrets.
1717
- Reads the API key from `EDS_API_KEY` or from `~/.config/eds/config.json`.
1818
- Outputs JSON when piped, pretty tables on a TTY (`--json` to force).
1919
- Repo uses the local `git` CLI for clone. Both products authenticate with
20-
`X-API-KEY`.
20+
`X-API-KEY`. `eds repo clone` embeds the API key as HTTP Basic Auth
21+
credentials directly into the smart-HTTP URL it passes to `git clone`, so
22+
clone/push work standalone — no git credential helper, OS keychain, or
23+
`~/.netrc` needs to be pre-configured. This matters for CI and AI agent
24+
sandboxes, which typically have none of those.
2125

2226
## Installation
2327

@@ -112,6 +116,7 @@ eds repo create <name> create a repository
112116
eds repo show <id-or-name> show repository details
113117
eds repo delete <id-or-name> [--force] delete a repository
114118
eds repo clone <id-or-name> [dir] [--ssh] clone via local git CLI
119+
eds repo remote-add <id-or-name> [--name N] [--ssh] wire an existing local checkout to it (git remote add)
115120
116121
eds wf app create <name> --repository R|--repository-url URL --branch B create and auto-deploy a Workflow Studio application
117122
eds wf app list list applications
@@ -223,8 +228,26 @@ cd demo
223228
git add . && git commit -m "init" && git push origin main
224229
```
225230

226-
`git push` works out of the box because the API key is used as
227-
basic-auth credentials on the smart-HTTP endpoint exposed by the server.
231+
`git push` works out of the box because `eds repo clone` already embedded the
232+
API key as basic-auth credentials in `origin`'s URL (see `.git/config`) — no
233+
git credential helper or OS keychain is involved. Note this means the API
234+
key sits in plaintext in that repo's `.git/config`; treat the clone
235+
directory with the same care as the key itself.
236+
237+
If the code already exists locally (no `eds repo clone` involved) and you
238+
just created the remote repository, use `eds repo remote-add` instead of a
239+
plain `git remote add` to get the same embedded authentication:
240+
241+
```bash
242+
eds repo create demo
243+
cd path/to/existing/local/repo
244+
eds repo remote-add demo
245+
git push -u origin main
246+
```
247+
248+
`--ssh` is the one exception: it depends on the host's SSH public key being
249+
registered separately (not handled by this CLI), so it still needs whatever
250+
ambient SSH setup the environment provides.
228251

229252
## Distribution / publishing
230253

cmd/repo.go

Lines changed: 100 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bufio"
55
"context"
66
"fmt"
7+
"net/url"
78
"os"
89
"os/exec"
910
"strings"
@@ -26,6 +27,7 @@ func newRepoCmd() *cobra.Command {
2627
cmd.AddCommand(newRepoShowCmd())
2728
cmd.AddCommand(newRepoDeleteCmd())
2829
cmd.AddCommand(newRepoCloneCmd())
30+
cmd.AddCommand(newRepoRemoteAddCmd())
2931
return cmd
3032
}
3133

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

284+
useSSH := ssh && info.Clone.SSH != ""
282285
cloneURL := info.Clone.HTTPS
283-
if ssh && info.Clone.SSH != "" {
286+
if useSSH {
284287
cloneURL = info.Clone.SSH
285288
}
286289
if cloneURL == "" {
287290
return fmt.Errorf("repository has no clone url")
288291
}
289292

293+
// The smart-HTTP endpoint authenticates via HTTP Basic Auth using
294+
// the API key as the password (any username works). Embed it in
295+
// the URL so clone/push work standalone, without relying on a
296+
// git credential helper or ~/.netrc being pre-configured in the
297+
// environment (agents/CI have neither).
298+
displayCloneURL := cloneURL
299+
if !useSSH {
300+
cloneURL = withBasicAuth(cloneURL, ctx.Cfg.APIKey)
301+
}
302+
290303
dst := target
291304
if dst == "" && len(args) == 2 {
292305
dst = args[1]
293306
}
294307

295308
gitArgs := []string{"clone", cloneURL}
309+
displayArgs := []string{"clone", displayCloneURL}
296310
if dst != "" {
297311
gitArgs = append(gitArgs, dst)
312+
displayArgs = append(displayArgs, dst)
298313
}
299314

300315
if !ctx.Quiet {
301-
fmt.Fprintf(cmd.OutOrStdout(), "Running: git %s\n", strings.Join(gitArgs, " "))
316+
fmt.Fprintf(cmd.OutOrStdout(), "Running: git %s\n", strings.Join(displayArgs, " "))
302317
}
303318

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

332+
func newRepoRemoteAddCmd() *cobra.Command {
333+
var (
334+
remoteName string
335+
ssh bool
336+
)
337+
338+
cmd := &cobra.Command{
339+
Use: "remote-add <repository-id-or-name>",
340+
Short: "Wire an existing local git checkout to a Repo-product remote",
341+
Long: `remote-add looks up the repository via the API and runs
342+
"git remote add" in the current directory against its smart-HTTP (or SSH)
343+
URL, with the API key embedded as HTTP Basic Auth credentials - same
344+
authentication "eds repo clone" sets up, just for a local checkout that
345+
already exists (e.g. code was scaffolded locally, then "eds repo create"
346+
made the remote). If you don't have a local checkout yet, use
347+
"eds repo clone" instead.`,
348+
Args: cobra.ExactArgs(1),
349+
Example: ` eds repo remote-add my-repo
350+
eds repo remote-add my-repo --name upstream
351+
eds repo remote-add my-repo --ssh`,
352+
RunE: func(cmd *cobra.Command, args []string) error {
353+
ctx, err := resolveContext(cmd)
354+
if err != nil {
355+
return err
356+
}
357+
if err := ctx.requireAPIKey(); err != nil {
358+
return err
359+
}
360+
361+
// Try to resolve the argument as either an id or a name.
362+
id, err := resolveRepoID(cmd.Context(), ctx, args[0])
363+
if err != nil {
364+
return err
365+
}
366+
367+
info, err := ctx.API.GetRepository(cmd.Context(), id)
368+
if err != nil {
369+
return fmt.Errorf("api get repository: %w", err)
370+
}
371+
372+
useSSH := ssh && info.Clone.SSH != ""
373+
remoteURL := info.Clone.HTTPS
374+
if useSSH {
375+
remoteURL = info.Clone.SSH
376+
}
377+
if remoteURL == "" {
378+
return fmt.Errorf("repository has no clone url")
379+
}
380+
381+
displayURL := remoteURL
382+
if !useSSH {
383+
remoteURL = withBasicAuth(remoteURL, ctx.Cfg.APIKey)
384+
}
385+
386+
if !ctx.Quiet {
387+
fmt.Fprintf(cmd.OutOrStdout(), "Running: git remote add %s %s\n", remoteName, displayURL)
388+
}
389+
390+
c := exec.CommandContext(cmd.Context(), "git", "remote", "add", remoteName, remoteURL)
391+
c.Stdout = cmd.OutOrStdout()
392+
c.Stderr = cmd.ErrOrStderr()
393+
c.Stdin = os.Stdin
394+
return c.Run()
395+
},
396+
}
397+
398+
cmd.Flags().StringVar(&remoteName, "name", "origin", "git remote name to create")
399+
cmd.Flags().BoolVar(&ssh, "ssh", false, "use the SSH remote instead of HTTPS")
400+
return cmd
401+
}
402+
403+
// withBasicAuth embeds apiKey as the password of an HTTP Basic Auth userinfo
404+
// component in rawURL (username is arbitrary; the server only checks the
405+
// password). Returns rawURL unchanged if it doesn't parse as a URL.
406+
func withBasicAuth(rawURL, apiKey string) string {
407+
u, err := url.Parse(rawURL)
408+
if err != nil || apiKey == "" {
409+
return rawURL
410+
}
411+
u.User = url.UserPassword("eds", apiKey)
412+
return u.String()
413+
}
414+
317415
// resolveRepoID accepts either a raw repository id (UUID) or a repository
318416
// name and returns the id. Names are resolved against the configured
319417
// project's listing.

skill/SKILL.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ Errors go to stderr and the process exits non-zero.
5959
| `eds repo show <id-or-name> [--json]` | Show details: id, default_branch, size, clone URLs |
6060
| `eds repo delete <id-or-name> [--force] [--json]` | Delete (irreversible; requires confirmation unless `--force`) |
6161
| `eds repo clone <id-or-name> [dir] [--ssh] [--target DIR]` | Clone via local `git` CLI |
62+
| `eds repo remote-add <id-or-name> [--name N] [--ssh]` | Wire an existing local checkout to it (`git remote add`, authenticated) |
6263
| `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) |
6364
| `eds wf app list [--search S] [--sort created_at_asc\|created_at_desc] [--json]` | List applications |
6465
| `eds wf app show <id> [--json]` | Show application details (status, run_id, pipeline_id, ...) |
@@ -161,15 +162,40 @@ cd ./work/my-new-repo
161162
git status
162163
```
163164

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

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

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

185+
### Wire up code that already exists locally (no clone involved)
186+
187+
If the code was scaffolded locally first and the repository was created
188+
after the fact, use `eds repo remote-add` instead of a plain
189+
`git remote add` — it embeds the same authenticated URL `eds repo clone`
190+
would have:
191+
192+
```bash
193+
eds repo create my-new-repo
194+
cd path/to/existing/local/repo
195+
eds repo remote-add my-new-repo
196+
git push -u origin main
197+
```
198+
173199
### Delete a repository (with confirmation)
174200

175201
```bash

0 commit comments

Comments
 (0)