netlify-cli 8.17.4 → 8.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -203,6 +203,7 @@ Handle various site operations
|
|
|
203
203
|
| Subcommand | description |
|
|
204
204
|
|:--------------------------- |:-----|
|
|
205
205
|
| [`sites:create`](/docs/commands/sites.md#sitescreate) | Create an empty site (advanced) |
|
|
206
|
+
| [`sites:create-template`](/docs/commands/sites.md#sitescreate-template) | (Beta) Create a site from a starter template |
|
|
206
207
|
| [`sites:delete`](/docs/commands/sites.md#sitesdelete) | Delete a site |
|
|
207
208
|
| [`sites:list`](/docs/commands/sites.md#siteslist) | List all sites you have access to |
|
|
208
209
|
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "netlify-cli",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.18.0",
|
|
4
4
|
"lockfileVersion": 2,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "netlify-cli",
|
|
9
|
-
"version": "8.
|
|
9
|
+
"version": "8.18.0",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"dependencies": {
|
package/package.json
CHANGED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
const inquirer = require('inquirer')
|
|
4
|
+
const pick = require('lodash/pick')
|
|
5
|
+
const prettyjson = require('prettyjson')
|
|
6
|
+
|
|
7
|
+
const { chalk, error, getRepoData, log, logJson, track, warn } = require('../../utils')
|
|
8
|
+
const { configureRepo } = require('../../utils/init/config')
|
|
9
|
+
const { getGitHubToken } = require('../../utils/init/config-github')
|
|
10
|
+
const { createRepo, getTemplatesFromGitHub } = require('../../utils/sites/utils')
|
|
11
|
+
|
|
12
|
+
const { getSiteNameInput } = require('./sites-create')
|
|
13
|
+
|
|
14
|
+
const fetchTemplates = async (token) => {
|
|
15
|
+
const templatesFromGithubOrg = await getTemplatesFromGitHub(token)
|
|
16
|
+
|
|
17
|
+
return templatesFromGithubOrg
|
|
18
|
+
.filter((repo) => !repo.archived && !repo.private && !repo.disabled)
|
|
19
|
+
.map((template) => ({
|
|
20
|
+
name: template.name,
|
|
21
|
+
sourceCodeUrl: template.html_url,
|
|
22
|
+
slug: template.full_name,
|
|
23
|
+
}))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The sites:create-template command
|
|
28
|
+
* @param {import('commander').OptionValues} options
|
|
29
|
+
* @param {import('../base-command').BaseCommand} command
|
|
30
|
+
*/
|
|
31
|
+
const sitesCreateTemplate = async (options, command) => {
|
|
32
|
+
const { api } = command.netlify
|
|
33
|
+
|
|
34
|
+
await command.authenticate()
|
|
35
|
+
|
|
36
|
+
const { globalConfig } = command.netlify
|
|
37
|
+
const ghToken = await getGitHubToken({ globalConfig })
|
|
38
|
+
|
|
39
|
+
let { url: templateUrl } = options
|
|
40
|
+
|
|
41
|
+
if (templateUrl) {
|
|
42
|
+
const urlFromOptions = new URL(templateUrl)
|
|
43
|
+
templateUrl = { templateName: urlFromOptions.pathname.slice(1) }
|
|
44
|
+
} else {
|
|
45
|
+
const templates = await fetchTemplates(ghToken)
|
|
46
|
+
|
|
47
|
+
log(`Choose one of our starter templates. Netlify will create a new repo for this template in your GitHub account.`)
|
|
48
|
+
|
|
49
|
+
templateUrl = await inquirer.prompt([
|
|
50
|
+
{
|
|
51
|
+
type: 'list',
|
|
52
|
+
name: 'templateName',
|
|
53
|
+
message: 'Template:',
|
|
54
|
+
choices: templates.map((template) => ({
|
|
55
|
+
value: template.slug,
|
|
56
|
+
name: template.name,
|
|
57
|
+
})),
|
|
58
|
+
},
|
|
59
|
+
])
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const accounts = await api.listAccountsForUser()
|
|
63
|
+
|
|
64
|
+
let { accountSlug } = options
|
|
65
|
+
|
|
66
|
+
if (!accountSlug) {
|
|
67
|
+
const { accountSlug: accountSlugInput } = await inquirer.prompt([
|
|
68
|
+
{
|
|
69
|
+
type: 'list',
|
|
70
|
+
name: 'accountSlug',
|
|
71
|
+
message: 'Team:',
|
|
72
|
+
choices: accounts.map((account) => ({
|
|
73
|
+
value: account.slug,
|
|
74
|
+
name: account.name,
|
|
75
|
+
})),
|
|
76
|
+
},
|
|
77
|
+
])
|
|
78
|
+
accountSlug = accountSlugInput
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const { name: nameFlag } = options
|
|
82
|
+
let user
|
|
83
|
+
let site
|
|
84
|
+
|
|
85
|
+
// Allow the user to reenter site name if selected one isn't available
|
|
86
|
+
const inputSiteName = async (name) => {
|
|
87
|
+
const { name: inputName, siteSuggestion } = await getSiteNameInput(name, user, api)
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const siteName = inputName ? inputName.trim() : siteSuggestion
|
|
91
|
+
|
|
92
|
+
// Create new repo from template
|
|
93
|
+
const repoResp = await createRepo(templateUrl, ghToken, siteName)
|
|
94
|
+
|
|
95
|
+
if (repoResp.errors) {
|
|
96
|
+
if (repoResp.errors[0].includes('Name already exists on this account')) {
|
|
97
|
+
warn(
|
|
98
|
+
`Oh no! We found already a repository with this name. It seems you have already created a template with the name ${templateUrl.templateName}. Please try to run the command again and provide a different name.`,
|
|
99
|
+
)
|
|
100
|
+
await inputSiteName()
|
|
101
|
+
} else {
|
|
102
|
+
throw new Error(
|
|
103
|
+
`Oops! Seems like something went wrong trying to create the repository. We're getting the following error: '${repoResp.errors[0]}'. You can try to re-run this command again or open an issue in our repository: https://github.com/netlify/cli/issues`,
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
site = await api.createSiteInTeam({
|
|
108
|
+
accountSlug,
|
|
109
|
+
body: {
|
|
110
|
+
repo: {
|
|
111
|
+
provider: 'github',
|
|
112
|
+
repo: repoResp.full_name,
|
|
113
|
+
private: repoResp.private,
|
|
114
|
+
branch: repoResp.default_branch,
|
|
115
|
+
},
|
|
116
|
+
name: siteName,
|
|
117
|
+
},
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
} catch (error_) {
|
|
121
|
+
if (error_.status === 422 || error_.message === 'Duplicate repo') {
|
|
122
|
+
warn(
|
|
123
|
+
`${name}.netlify.app already exists or a repository named ${name} already exists on this account. Please try a different slug.`,
|
|
124
|
+
)
|
|
125
|
+
await inputSiteName()
|
|
126
|
+
} else {
|
|
127
|
+
error(`createSiteInTeam error: ${error_.status}: ${error_.message}`)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
await inputSiteName(nameFlag)
|
|
133
|
+
|
|
134
|
+
log()
|
|
135
|
+
log(chalk.greenBright.bold.underline(`Site Created`))
|
|
136
|
+
log()
|
|
137
|
+
|
|
138
|
+
const siteUrl = site.ssl_url || site.url
|
|
139
|
+
log(
|
|
140
|
+
prettyjson.render({
|
|
141
|
+
'Admin URL': site.admin_url,
|
|
142
|
+
URL: siteUrl,
|
|
143
|
+
'Site ID': site.id,
|
|
144
|
+
'Repo URL': site.build_settings.repo_url,
|
|
145
|
+
}),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
track('sites_createdFromTemplate', {
|
|
149
|
+
siteId: site.id,
|
|
150
|
+
adminUrl: site.admin_url,
|
|
151
|
+
siteUrl,
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
if (options.withCi) {
|
|
155
|
+
log('Configuring CI')
|
|
156
|
+
const repoData = await getRepoData()
|
|
157
|
+
await configureRepo({ command, siteId: site.id, repoData, manual: options.manual })
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (options.json) {
|
|
161
|
+
logJson(
|
|
162
|
+
pick(site, [
|
|
163
|
+
'id',
|
|
164
|
+
'state',
|
|
165
|
+
'plan',
|
|
166
|
+
'name',
|
|
167
|
+
'custom_domain',
|
|
168
|
+
'domain_aliases',
|
|
169
|
+
'url',
|
|
170
|
+
'ssl_url',
|
|
171
|
+
'admin_url',
|
|
172
|
+
'screenshot_url',
|
|
173
|
+
'created_at',
|
|
174
|
+
'updated_at',
|
|
175
|
+
'user_id',
|
|
176
|
+
'ssl',
|
|
177
|
+
'force_ssl',
|
|
178
|
+
'managed_dns',
|
|
179
|
+
'deploy_url',
|
|
180
|
+
'account_name',
|
|
181
|
+
'account_slug',
|
|
182
|
+
'git_provider',
|
|
183
|
+
'deploy_hook',
|
|
184
|
+
'capabilities',
|
|
185
|
+
'id_domain',
|
|
186
|
+
]),
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return site
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Creates the `netlify sites:create-template` command
|
|
195
|
+
* @param {import('../base-command').BaseCommand} program
|
|
196
|
+
* @returns
|
|
197
|
+
*/
|
|
198
|
+
const createSitesFromTemplateCommand = (program) =>
|
|
199
|
+
program
|
|
200
|
+
.command('sites:create-template')
|
|
201
|
+
.description(
|
|
202
|
+
`(Beta) Create a site from a starter template
|
|
203
|
+
Create a site from a starter template.`,
|
|
204
|
+
)
|
|
205
|
+
.option('-n, --name [name]', 'name of site')
|
|
206
|
+
.option('-u, --url [url]', 'template url')
|
|
207
|
+
.option('-a, --account-slug [slug]', 'account slug to create the site under')
|
|
208
|
+
.option('-c, --with-ci', 'initialize CI hooks during site creation')
|
|
209
|
+
.addHelpText('after', `(Beta) Create a site from starter template.`)
|
|
210
|
+
.action(sitesCreateTemplate)
|
|
211
|
+
|
|
212
|
+
module.exports = { createSitesFromTemplateCommand, fetchTemplates }
|
|
@@ -13,6 +13,49 @@ const { link } = require('../link')
|
|
|
13
13
|
|
|
14
14
|
const SITE_NAME_SUGGESTION_SUFFIX_LENGTH = 5
|
|
15
15
|
|
|
16
|
+
const getSiteNameInput = async (name, user, api) => {
|
|
17
|
+
let siteSuggestion
|
|
18
|
+
if (!user) user = await api.getCurrentUser()
|
|
19
|
+
|
|
20
|
+
if (!name) {
|
|
21
|
+
let { slug } = user
|
|
22
|
+
let suffix = ''
|
|
23
|
+
|
|
24
|
+
// If the user doesn't have a slug, we'll compute one. Because `full_name` is not guaranteed to be unique, we
|
|
25
|
+
// append a short randomly-generated ID to reduce the likelihood of a conflict.
|
|
26
|
+
if (!slug) {
|
|
27
|
+
slug = slugify(user.full_name || user.email)
|
|
28
|
+
suffix = `-${uuidv4().slice(0, SITE_NAME_SUGGESTION_SUFFIX_LENGTH)}`
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const suggestions = [
|
|
32
|
+
`super-cool-site-by-${slug}${suffix}`,
|
|
33
|
+
`the-awesome-${slug}-site${suffix}`,
|
|
34
|
+
`${slug}-makes-great-sites${suffix}`,
|
|
35
|
+
`netlify-thinks-${slug}-is-great${suffix}`,
|
|
36
|
+
`the-great-${slug}-site${suffix}`,
|
|
37
|
+
`isnt-${slug}-awesome${suffix}`,
|
|
38
|
+
]
|
|
39
|
+
siteSuggestion = sample(suggestions)
|
|
40
|
+
|
|
41
|
+
console.log(
|
|
42
|
+
`Choose a unique site name (e.g. ${siteSuggestion}.netlify.app) or leave it blank for a random name. You can update the site name later.`,
|
|
43
|
+
)
|
|
44
|
+
const { name: nameInput } = await inquirer.prompt([
|
|
45
|
+
{
|
|
46
|
+
type: 'input',
|
|
47
|
+
name: 'name',
|
|
48
|
+
message: 'Site name (optional):',
|
|
49
|
+
filter: (val) => (val === '' ? undefined : val),
|
|
50
|
+
validate: (input) => /^[a-zA-Z\d-]+$/.test(input) || 'Only alphanumeric characters and hyphens are allowed',
|
|
51
|
+
},
|
|
52
|
+
])
|
|
53
|
+
name = nameInput
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { name, siteSuggestion }
|
|
57
|
+
}
|
|
58
|
+
|
|
16
59
|
/**
|
|
17
60
|
* The sites:create command
|
|
18
61
|
* @param {import('commander').OptionValues} options
|
|
@@ -47,47 +90,11 @@ const sitesCreate = async (options, command) => {
|
|
|
47
90
|
|
|
48
91
|
// Allow the user to reenter site name if selected one isn't available
|
|
49
92
|
const inputSiteName = async (name) => {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (!name) {
|
|
53
|
-
let { slug } = user
|
|
54
|
-
let suffix = ''
|
|
55
|
-
|
|
56
|
-
// If the user doesn't have a slug, we'll compute one. Because `full_name` is not guaranteed to be unique, we
|
|
57
|
-
// append a short randomly-generated ID to reduce the likelihood of a conflict.
|
|
58
|
-
if (!slug) {
|
|
59
|
-
slug = slugify(user.full_name || user.email)
|
|
60
|
-
suffix = `-${uuidv4().slice(0, SITE_NAME_SUGGESTION_SUFFIX_LENGTH)}`
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const suggestions = [
|
|
64
|
-
`super-cool-site-by-${slug}${suffix}`,
|
|
65
|
-
`the-awesome-${slug}-site${suffix}`,
|
|
66
|
-
`${slug}-makes-great-sites${suffix}`,
|
|
67
|
-
`netlify-thinks-${slug}-is-great${suffix}`,
|
|
68
|
-
`the-great-${slug}-site${suffix}`,
|
|
69
|
-
`isnt-${slug}-awesome${suffix}`,
|
|
70
|
-
]
|
|
71
|
-
const siteSuggestion = sample(suggestions)
|
|
72
|
-
|
|
73
|
-
console.log(
|
|
74
|
-
`Choose a unique site name (e.g. ${siteSuggestion}.netlify.app) or leave it blank for a random name. You can update the site name later.`,
|
|
75
|
-
)
|
|
76
|
-
const { name: nameInput } = await inquirer.prompt([
|
|
77
|
-
{
|
|
78
|
-
type: 'input',
|
|
79
|
-
name: 'name',
|
|
80
|
-
message: 'Site name (optional):',
|
|
81
|
-
filter: (val) => (val === '' ? undefined : val),
|
|
82
|
-
validate: (input) => /^[a-zA-Z\d-]+$/.test(input) || 'Only alphanumeric characters and hyphens are allowed',
|
|
83
|
-
},
|
|
84
|
-
])
|
|
85
|
-
name = nameInput
|
|
86
|
-
}
|
|
93
|
+
const { name: siteName } = await getSiteNameInput(name, user, api)
|
|
87
94
|
|
|
88
95
|
const body = {}
|
|
89
|
-
if (typeof
|
|
90
|
-
body.name =
|
|
96
|
+
if (typeof siteName === 'string') {
|
|
97
|
+
body.name = siteName.trim()
|
|
91
98
|
}
|
|
92
99
|
try {
|
|
93
100
|
site = await api.createSiteInTeam({
|
|
@@ -96,7 +103,7 @@ const sitesCreate = async (options, command) => {
|
|
|
96
103
|
})
|
|
97
104
|
} catch (error_) {
|
|
98
105
|
if (error_.status === 422) {
|
|
99
|
-
warn(`${
|
|
106
|
+
warn(`${siteName}.netlify.app already exists. Please try a different slug.`)
|
|
100
107
|
await inputSiteName()
|
|
101
108
|
} else {
|
|
102
109
|
error(`createSiteInTeam error: ${error_.status}: ${error_.message}`)
|
|
@@ -191,4 +198,4 @@ Create a blank site that isn't associated with any git remote. Will link the sit
|
|
|
191
198
|
)
|
|
192
199
|
.action(sitesCreate)
|
|
193
200
|
|
|
194
|
-
module.exports = { createSitesCreateCommand, sitesCreate }
|
|
201
|
+
module.exports = { createSitesCreateCommand, sitesCreate, getSiteNameInput }
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
const { createSitesCreateCommand } = require('./sites-create')
|
|
3
|
+
const { createSitesFromTemplateCommand } = require('./sites-create-template')
|
|
3
4
|
const { createSitesDeleteCommand } = require('./sites-delete')
|
|
4
5
|
const { createSitesListCommand } = require('./sites-list')
|
|
5
6
|
|
|
@@ -19,6 +20,7 @@ const sites = (options, command) => {
|
|
|
19
20
|
*/
|
|
20
21
|
const createSitesCommand = (program) => {
|
|
21
22
|
createSitesCreateCommand(program)
|
|
23
|
+
createSitesFromTemplateCommand(program)
|
|
22
24
|
createSitesListCommand(program)
|
|
23
25
|
createSitesDeleteCommand(program)
|
|
24
26
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const fetch = require('node-fetch')
|
|
2
|
+
|
|
3
|
+
const getTemplatesFromGitHub = async (token) => {
|
|
4
|
+
const templates = await fetch(`https://api.github.com/orgs/netlify-templates/repos`, {
|
|
5
|
+
method: 'GET',
|
|
6
|
+
headers: {
|
|
7
|
+
Authorization: `token ${token}`,
|
|
8
|
+
},
|
|
9
|
+
})
|
|
10
|
+
const allTemplates = await templates.json()
|
|
11
|
+
|
|
12
|
+
return allTemplates
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const createRepo = async (templateUrl, ghToken, siteName) => {
|
|
16
|
+
const resp = await fetch(`https://api.github.com/repos/${templateUrl.templateName}/generate`, {
|
|
17
|
+
method: 'POST',
|
|
18
|
+
headers: {
|
|
19
|
+
Authorization: `token ${ghToken}`,
|
|
20
|
+
},
|
|
21
|
+
body: JSON.stringify({
|
|
22
|
+
name: siteName,
|
|
23
|
+
}),
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const data = await resp.json()
|
|
27
|
+
return data
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = { getTemplatesFromGitHub, createRepo }
|