Skip to content

Commit fe9940e

Browse files
authored
feat: add new netlify clone command, to clone an existing repo + connected Netlify site in one step (#7260)
* refactor(link): extract function for site by repo URL This also polishes the progress output and fixes some types. * feat: add `clone` command When a user already has an existing Netlify site connected to a hosted git repo, pulling this down and preparing it for use with Netlify CLI currently requires numerous steps: 1. find and copy the git remote URL 2. run `git clone <git url>` 3. run `cd <created dir>` 4. run `netlify link` 5. follow prompt to select "Use current git remote origin" This is a very common flow, e.g. when using a "Deploy to Netlify" button, creating a site from a Netlify template, creating a site from an AI creator... any workflow where the site is created outside Netlify CLI requires several steps to get it ready for use with Netlify CLI. This introduces a new command which vastly simplifies these steps: 1. find and copy the git remote URL 2. run `netlify clone <git url>` This will clone the git repo, find the corresponding Netlify site, and link the created directory automatically. It will only prompt the user if necessary - for example if the git repo URL matches multiple sites. For convenience and future integration into the Netlify platform and docs, I also included support for explicitly passing the known site id or name via `--id` or `--name`.
1 parent cb881e8 commit fe9940e

File tree

18 files changed

+958
-72
lines changed

18 files changed

+958
-72
lines changed

docs/commands/clone.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
---
2+
title: Netlify CLI clone command
3+
sidebar:
4+
label: clone
5+
description: Clone a remote repo and link it to an existing site on Netlify
6+
---
7+
8+
# `clone`
9+
10+
<!-- AUTO-GENERATED-CONTENT:START (GENERATE_COMMANDS_DOCS) -->
11+
Clone a remote repository and link it to an existing site on Netlify
12+
Use this command when the existing Netlify site is already configured to deploy from the existing repo.
13+
14+
If you specify a target directory, the repo will be cloned into that directory. By default, a directory will be created with the name of the repo.
15+
16+
To specify a site, use --id or --name. By default, the Netlify site to link will be automatically detected if exactly one site found is found with a matching git URL. If we cannot find such a site, you will be interactively prompted to select one.
17+
18+
**Usage**
19+
20+
```bash
21+
netlify clone
22+
```
23+
24+
**Arguments**
25+
26+
- repo - URL of the repository to clone or Github `owner/repo` (required)
27+
- targetDir - directory in which to clone the repository - will be created if it does not exist
28+
29+
**Flags**
30+
31+
- `filter` (*string*) - For monorepos, specify the name of the application to run the command in
32+
- `id` (*string*) - ID of existing Netlify site to link to
33+
- `name` (*string*) - Name of existing Netlify site to link to
34+
- `debug` (*boolean*) - Print debugging information
35+
- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in
36+
37+
**Examples**
38+
39+
```bash
40+
netlify clone vibecoder/next-unicorn
41+
netlify clone https://github.com/vibecoder/next-unicorn.git
42+
netlify clone git@github.com:vibecoder/next-unicorn.git
43+
netlify clone vibecoder/next-unicorn ./next-unicorn-shh-secret
44+
netlify clone --id 123-123-123-123 vibecoder/next-unicorn
45+
netlify clone --name my-site-name vibecoder/next-unicorn
46+
```
47+
48+
49+
<!-- AUTO-GENERATED-CONTENT:END -->

docs/commands/link.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ netlify link
2020

2121
- `filter` (*string*) - For monorepos, specify the name of the application to run the command in
2222
- `git-remote-name` (*string*) - Name of Git remote to use. e.g. "origin"
23+
- `git-remote-url` (*string*) - URL of the repository (or Github `owner/repo`) to link to
2324
- `id` (*string*) - ID of site to link to
2425
- `name` (*string*) - Name of site to link to
2526
- `debug` (*boolean*) - Print debugging information
@@ -31,6 +32,7 @@ netlify link
3132
netlify link
3233
netlify link --id 123-123-123-123
3334
netlify link --name my-site-name
35+
netlify link --git-remote-url https://github.com/vibecoder/my-unicorn.git
3436
```
3537

3638

docs/index.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ Manage objects in Netlify Blobs
3838

3939
Build on your local machine
4040

41+
### [clone](/commands/clone)
42+
43+
Clone a remote repository and link it to an existing site on Netlify
44+
4145
### [completion](/commands/completion)
4246

4347
Generate shell completion script

src/commands/clone/clone.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import inquirer from 'inquirer'
2+
3+
import { normalizeRepoUrl } from '../../utils/normalize-repo-url.js'
4+
import { chalk, logAndThrowError, log } from '../../utils/command-helpers.js'
5+
import { runGit } from '../../utils/run-git.js'
6+
import type BaseCommand from '../base-command.js'
7+
import { link } from '../link/link.js'
8+
import type { CloneOptionValues } from './option_values.js'
9+
import { startSpinner } from '../../lib/spinner.js'
10+
11+
const getTargetDir = async (defaultDir: string): Promise<string> => {
12+
const { selectedDir } = await inquirer.prompt<{ selectedDir: string }>([
13+
{
14+
type: 'input',
15+
name: 'selectedDir',
16+
message: 'Where should we clone the repository?',
17+
default: defaultDir,
18+
},
19+
])
20+
21+
return selectedDir
22+
}
23+
24+
const cloneRepo = async (repoUrl: string, targetDir: string, debug: boolean): Promise<void> => {
25+
try {
26+
await runGit(['clone', repoUrl, targetDir], !debug)
27+
} catch (error) {
28+
throw new Error(`Failed to clone repository: ${error instanceof Error ? error.message : error?.toString() ?? ''}`)
29+
}
30+
}
31+
32+
export const clone = async (
33+
options: CloneOptionValues,
34+
command: BaseCommand,
35+
args: { repo: string; targetDir?: string },
36+
) => {
37+
await command.authenticate()
38+
39+
const { repoUrl, httpsUrl, repoName } = normalizeRepoUrl(args.repo)
40+
41+
const targetDir = args.targetDir ?? (await getTargetDir(`./${repoName}`))
42+
43+
const cloneSpinner = startSpinner({ text: `Cloning repository to ${chalk.cyan(targetDir)}` })
44+
try {
45+
await cloneRepo(repoUrl, targetDir, options.debug ?? false)
46+
} catch (error) {
47+
return logAndThrowError(error)
48+
}
49+
cloneSpinner.success(`Cloned repository to ${chalk.cyan(targetDir)}`)
50+
51+
command.workingDir = targetDir
52+
// TODO(serhalp): This shouldn't be necessary but `getPathInProject` does not take
53+
// `command.workingDir` into account. Carefully fix this and remove this line.
54+
process.chdir(targetDir)
55+
56+
const { id, name, ...globalOptions } = options
57+
const linkOptions = {
58+
...globalOptions,
59+
id,
60+
name,
61+
// Use the normalized HTTPS URL as the canonical git URL for linking to ensure
62+
// we have a consistent URL format for looking up sites.
63+
gitRemoteUrl: httpsUrl,
64+
}
65+
await link(linkOptions, command)
66+
67+
log()
68+
log(chalk.green('✔ Your project is ready to go!'))
69+
log(`→ Next, enter your project directory using ${chalk.cyanBright(`cd ${targetDir}`)}`)
70+
log()
71+
log(`→ You can now run other ${chalk.cyanBright('netlify')} CLI commands in this directory`)
72+
log(`→ To build and deploy your site: ${chalk.cyanBright('netlify deploy')}`)
73+
if (command.netlify.config.dev?.command) {
74+
log(`→ To run your dev server: ${chalk.cyanBright(command.netlify.config.dev.command)}`)
75+
}
76+
log(`→ To see all available commands: ${chalk.cyanBright('netlify help')}`)
77+
log()
78+
}

src/commands/clone/index.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import terminalLink from 'terminal-link'
2+
3+
import type BaseCommand from '../base-command.js'
4+
import type { CloneOptionValues } from './option_values.js'
5+
6+
export const createCloneCommand = (program: BaseCommand) =>
7+
program
8+
.command('clone')
9+
.description(
10+
`Clone a remote repository and link it to an existing site on Netlify
11+
Use this command when the existing Netlify site is already configured to deploy from the existing repo.
12+
13+
If you specify a target directory, the repo will be cloned into that directory. By default, a directory will be created with the name of the repo.
14+
15+
To specify a site, use --id or --name. By default, the Netlify site to link will be automatically detected if exactly one site found is found with a matching git URL. If we cannot find such a site, you will be interactively prompted to select one.`,
16+
)
17+
.argument('<repo>', 'URL of the repository to clone or Github `owner/repo` (required)')
18+
.argument('[targetDir]', 'directory in which to clone the repository - will be created if it does not exist')
19+
.option('--id <id>', 'ID of existing Netlify site to link to')
20+
.option('--name <name>', 'Name of existing Netlify site to link to')
21+
.addExamples([
22+
'netlify clone vibecoder/next-unicorn',
23+
'netlify clone https://github.com/vibecoder/next-unicorn.git',
24+
'netlify clone git@github.com:vibecoder/next-unicorn.git',
25+
'netlify clone vibecoder/next-unicorn ./next-unicorn-shh-secret',
26+
'netlify clone --id 123-123-123-123 vibecoder/next-unicorn',
27+
'netlify clone --name my-site-name vibecoder/next-unicorn',
28+
])
29+
.addHelpText('after', () => {
30+
const docsUrl = 'https://docs.netlify.com/cli/get-started/#link-and-unlink-sites'
31+
return `For more information about linking sites, see ${terminalLink(docsUrl, docsUrl)}\n`
32+
})
33+
.action(async (repo: string, targetDir: string | undefined, options: CloneOptionValues, command: BaseCommand) => {
34+
const { clone } = await import('./clone.js')
35+
await clone(options, command, { repo, targetDir })
36+
})

src/commands/clone/option_values.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { BaseOptionValues } from '../base-command.js'
2+
3+
export type CloneOptionValues = BaseOptionValues & {
4+
// NOTE(serhalp): Think this would be better off as `siteId`? Beware, here be dragons.
5+
// There's some magical global state mutation dance going on somewhere when you call
6+
// an option `--site-id`. Good luck, friend.
7+
id?: string | undefined
8+
name?: string | undefined
9+
}

src/commands/link/index.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { OptionValues } from 'commander'
21
import terminalLink from 'terminal-link'
32

43
import BaseCommand from '../base-command.js'
4+
import type { LinkOptionValues } from './option_values.js'
55

66
export const createLinkCommand = (program: BaseCommand) =>
77
program
@@ -10,14 +10,20 @@ export const createLinkCommand = (program: BaseCommand) =>
1010
.option('--id <id>', 'ID of site to link to')
1111
.option('--name <name>', 'Name of site to link to')
1212
.option('--git-remote-name <name>', 'Name of Git remote to use. e.g. "origin"')
13-
.addExamples(['netlify link', 'netlify link --id 123-123-123-123', 'netlify link --name my-site-name'])
13+
.option('--git-remote-url <name>', 'URL of the repository (or Github `owner/repo`) to link to')
14+
.addExamples([
15+
'netlify link',
16+
'netlify link --id 123-123-123-123',
17+
'netlify link --name my-site-name',
18+
'netlify link --git-remote-url https://github.com/vibecoder/my-unicorn.git',
19+
])
1420
.addHelpText('after', () => {
1521
const docsUrl = 'https://docs.netlify.com/cli/get-started/#link-and-unlink-sites'
1622
return `
1723
For more information about linking sites, see ${terminalLink(docsUrl, docsUrl)}
1824
`
1925
})
20-
.action(async (options: OptionValues, command: BaseCommand) => {
26+
.action(async (options: LinkOptionValues, command: BaseCommand) => {
2127
const { link } = await import('./link.js')
2228
await link(options, command)
2329
})

0 commit comments

Comments
 (0)